@band-app/server 0.47.0 → 0.48.0

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-DkQbk-aU.js} +1 -1
  3. package/dist/client/assets/{_basePickBy-D64wD5rn.js → _basePickBy-Dpp3dGrC.js} +1 -1
  4. package/dist/client/assets/{_baseUniq-pND9PMLH.js → _baseUniq-CL7CCXzw.js} +1 -1
  5. package/dist/client/assets/{arc-Bilmh-jP.js → arc-Dgc_RxlI.js} +1 -1
  6. package/dist/client/assets/{architectureDiagram-VXUJARFQ-KfIpeO-l.js → architectureDiagram-VXUJARFQ-CgG43jHE.js} +1 -1
  7. package/dist/client/assets/{blockDiagram-VD42YOAC-DtuyRI-v.js → blockDiagram-VD42YOAC-Bt7XXELu.js} +1 -1
  8. package/dist/client/assets/{c4Diagram-YG6GDRKO-CBuSt_Sq.js → c4Diagram-YG6GDRKO-BQwG7Gi0.js} +1 -1
  9. package/dist/client/assets/channel-BhaORH-2.js +1 -0
  10. package/dist/client/assets/{chunk-4BX2VUAB-CWIj0g_M.js → chunk-4BX2VUAB-BwFghqiW.js} +1 -1
  11. package/dist/client/assets/{chunk-55IACEB6-YWkCYv9O.js → chunk-55IACEB6-C7yjJLc1.js} +1 -1
  12. package/dist/client/assets/{chunk-B4BG7PRW-muM18MGT.js → chunk-B4BG7PRW-BaJd9PuL.js} +1 -1
  13. package/dist/client/assets/{chunk-DI55MBZ5-DUKyYScZ.js → chunk-DI55MBZ5-CElPgENm.js} +1 -1
  14. package/dist/client/assets/{chunk-FMBD7UC4-YTfHsy8G.js → chunk-FMBD7UC4-D3qe2ywl.js} +1 -1
  15. package/dist/client/assets/{chunk-QN33PNHL-BdIATH5l.js → chunk-QN33PNHL-DStfzqTU.js} +1 -1
  16. package/dist/client/assets/{chunk-QZHKN3VN-DqRnJnIm.js → chunk-QZHKN3VN-B1rMgEw-.js} +1 -1
  17. package/dist/client/assets/{chunk-TZMSLE5B-DOL-jYtj.js → chunk-TZMSLE5B-BEcRDS9V.js} +1 -1
  18. package/dist/client/assets/classDiagram-2ON5EDUG-iB9V-fQK.js +1 -0
  19. package/dist/client/assets/classDiagram-v2-WZHVMYZB-iB9V-fQK.js +1 -0
  20. package/dist/client/assets/clone-UwjnYJkb.js +1 -0
  21. package/dist/client/assets/{cose-bilkent-S5V4N54A-Cffv5KWs.js → cose-bilkent-S5V4N54A-DQnvhn8W.js} +1 -1
  22. package/dist/client/assets/{dagre-6UL2VRFP-lIXclAkR.js → dagre-6UL2VRFP-Y6UFkfVj.js} +1 -1
  23. package/dist/client/assets/{diagram-PSM6KHXK-BBQxBoNY.js → diagram-PSM6KHXK-Cn_p_oe9.js} +1 -1
  24. package/dist/client/assets/{diagram-QEK2KX5R-BkC_Y-i9.js → diagram-QEK2KX5R-CWHFM00-.js} +1 -1
  25. package/dist/client/assets/{diagram-S2PKOQOG-Culf_egi.js → diagram-S2PKOQOG-C0TgWCYA.js} +1 -1
  26. package/dist/client/assets/{erDiagram-Q2GNP2WA-D8qVM8rI.js → erDiagram-Q2GNP2WA-BP1po3AK.js} +1 -1
  27. package/dist/client/assets/{flowDiagram-NV44I4VS-D_CW6QJO.js → flowDiagram-NV44I4VS-B5oUkhMT.js} +1 -1
  28. package/dist/client/assets/{ganttDiagram-JELNMOA3-DeYroA3a.js → ganttDiagram-JELNMOA3-C7DXX2RL.js} +1 -1
  29. package/dist/client/assets/{gitGraphDiagram-V2S2FVAM-a_sS5cSL.js → gitGraphDiagram-V2S2FVAM-Dn30v3Fe.js} +1 -1
  30. package/dist/client/assets/{graph-BiyE3_7j.js → graph-E5kkHB_s.js} +1 -1
  31. package/dist/client/assets/{highlighted-body-B3W2YXNL-HVIXx0Ob.js → highlighted-body-B3W2YXNL-B2bPrtWU.js} +1 -1
  32. package/dist/client/assets/{index-LKhS8iLe.js → index-A7maATW7.js} +1 -1
  33. package/dist/client/assets/{index-B1Qa5REm.js → index-B-miY5Sp.js} +1 -1
  34. package/dist/client/assets/{index-Cnvsv3TN.js → index-B2f2Xih0.js} +1 -1
  35. package/dist/client/assets/{index-WgCV2JjC.js → index-BScD8646.js} +1 -1
  36. package/dist/client/assets/{index-BKkkwDGt.js → index-BlpyLCQK.js} +1 -1
  37. package/dist/client/assets/{index-CMHYIEyK.js → index-BuqiLymH.js} +1 -1
  38. package/dist/client/assets/{index-DOFpEx7l.js → index-CVOnvTFc.js} +1 -1
  39. package/dist/client/assets/{index-NNpxvI8j.js → index-CYUAdSi5.js} +1 -1
  40. package/dist/client/assets/{index-REiX7RF3.js → index-D50A2A_K.js} +1 -1
  41. package/dist/client/assets/{index-CqPtl_El.js → index-D6MDOO4K.js} +1 -1
  42. package/dist/client/assets/{index-BuC5LY4k.js → index-DKIwN46L.js} +1 -1
  43. package/dist/client/assets/{index-DyvxDAAr.js → index-DOWIe80D.js} +1 -1
  44. package/dist/client/assets/{index-CcH0k4fG.js → index-DzUf0Wwy.js} +1 -1
  45. package/dist/client/assets/{infoDiagram-HS3SLOUP-BfzBdZLM.js → infoDiagram-HS3SLOUP-CoxVQgxs.js} +1 -1
  46. package/dist/client/assets/{journeyDiagram-XKPGCS4Q-CUsUym-8.js → journeyDiagram-XKPGCS4Q-BUPqESWB.js} +1 -1
  47. package/dist/client/assets/{kanban-definition-3W4ZIXB7-b54i6dX1.js → kanban-definition-3W4ZIXB7-DDooJc-Z.js} +1 -1
  48. package/dist/client/assets/{layout-ztlxnOA9.js → layout-DQ2tNoC7.js} +1 -1
  49. package/dist/client/assets/{main-CCOutd_8.js → main-BbHA32iD.js} +273 -273
  50. package/dist/client/assets/main-Bump8gDQ.css +1 -0
  51. package/dist/client/assets/{mindmap-definition-VGOIOE7T-p7poZwfk.js → mindmap-definition-VGOIOE7T-BBoouni0.js} +1 -1
  52. package/dist/client/assets/{pieDiagram-ADFJNKIX-DNua1MUn.js → pieDiagram-ADFJNKIX-DAO1Cf3O.js} +1 -1
  53. package/dist/client/assets/{quadrantDiagram-AYHSOK5B-CminxoD5.js → quadrantDiagram-AYHSOK5B-CjJp6wOV.js} +1 -1
  54. package/dist/client/assets/{requirementDiagram-UZGBJVZJ-D3aBt82J.js → requirementDiagram-UZGBJVZJ-CX3MPDiG.js} +1 -1
  55. package/dist/client/assets/{sankeyDiagram-TZEHDZUN-B65x-vl8.js → sankeyDiagram-TZEHDZUN-Cqnsrnu8.js} +1 -1
  56. package/dist/client/assets/{sequenceDiagram-WL72ISMW-DswVb1N3.js → sequenceDiagram-WL72ISMW-CTJ2qJj_.js} +1 -1
  57. package/dist/client/assets/{stateDiagram-FKZM4ZOC-BScRlCMH.js → stateDiagram-FKZM4ZOC-Cy-jjqJZ.js} +1 -1
  58. package/dist/client/assets/stateDiagram-v2-4FDKWEC3-CT80STo9.js +1 -0
  59. package/dist/client/assets/{timeline-definition-IT6M3QCI-C5AHVri5.js → timeline-definition-IT6M3QCI-Bbb0ib0X.js} +1 -1
  60. package/dist/client/assets/{treemap-GDKQZRPO-B4F1SV97.js → treemap-GDKQZRPO-DUB2cMCc.js} +1 -1
  61. package/dist/client/assets/{worktree._worktreeId-DOkSR4Iv.js → worktree._worktreeId-CZWwGpYU.js} +1 -1
  62. package/dist/client/assets/{worktree._worktreeId-Da_9lTBD.js → worktree._worktreeId-DSiEfzZW.js} +1 -1
  63. package/dist/client/assets/{xychartDiagram-PRI3JC2R-dZFFrnv9.js → xychartDiagram-PRI3JC2R-CI6Pnd76.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 +1503 -551
  68. package/dist/terminal-daemon.mjs +149 -55
  69. package/package.json +7 -7
  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);
@@ -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(
@@ -25752,28 +25845,28 @@ var init_to_json_schema = __esm({
25752
25845
  }
25753
25846
  case "file": {
25754
25847
  const json3 = _json;
25755
- const file2 = {
25848
+ const file3 = {
25756
25849
  type: "string",
25757
25850
  format: "binary",
25758
25851
  contentEncoding: "binary"
25759
25852
  };
25760
25853
  const { minimum, maximum, mime } = schema._zod.bag;
25761
25854
  if (minimum !== void 0)
25762
- file2.minLength = minimum;
25855
+ file3.minLength = minimum;
25763
25856
  if (maximum !== void 0)
25764
- file2.maxLength = maximum;
25857
+ file3.maxLength = maximum;
25765
25858
  if (mime) {
25766
25859
  if (mime.length === 1) {
25767
- file2.contentMediaType = mime[0];
25768
- Object.assign(json3, file2);
25860
+ file3.contentMediaType = mime[0];
25861
+ Object.assign(json3, file3);
25769
25862
  } else {
25770
25863
  json3.anyOf = mime.map((m2) => {
25771
- const mFile = { ...file2, contentMediaType: m2 };
25864
+ const mFile = { ...file3, contentMediaType: m2 };
25772
25865
  return mFile;
25773
25866
  });
25774
25867
  }
25775
25868
  } else {
25776
- Object.assign(json3, file2);
25869
+ Object.assign(json3, file3);
25777
25870
  }
25778
25871
  break;
25779
25872
  }
@@ -26681,7 +26774,7 @@ function literal(value, params) {
26681
26774
  ...util_exports.normalizeParams(params)
26682
26775
  });
26683
26776
  }
26684
- function file(params) {
26777
+ function file2(params) {
26685
26778
  return _file(ZodFile, params);
26686
26779
  }
26687
26780
  function transform(fn) {
@@ -27539,7 +27632,7 @@ __export(external_exports, {
27539
27632
  emoji: () => emoji2,
27540
27633
  endsWith: () => _endsWith,
27541
27634
  enum: () => _enum2,
27542
- file: () => file,
27635
+ file: () => file2,
27543
27636
  flattenError: () => flattenError,
27544
27637
  float32: () => float32,
27545
27638
  float64: () => float64,
@@ -40205,7 +40298,7 @@ var init_node_sqlite = __esm({
40205
40298
 
40206
40299
  // ../../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
40300
  import crypto2 from "node:crypto";
40208
- import fs2, { existsSync as existsSync6, readdirSync as readdirSync6 } from "node:fs";
40301
+ import fs2, { existsSync as existsSync7, readdirSync as readdirSync6 } from "node:fs";
40209
40302
  import { join as join10 } from "node:path";
40210
40303
  function readMigrationFiles(config2) {
40211
40304
  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 +40307,7 @@ function readMigrationFiles(config2) {
40214
40307
  const migrations = readdirSync6(migrationFolderTo).map((subdir) => ({
40215
40308
  path: join10(migrationFolderTo, subdir, "migration.sql"),
40216
40309
  name: subdir
40217
- })).filter((it) => existsSync6(it.path));
40310
+ })).filter((it) => existsSync7(it.path));
40218
40311
  migrations.sort((a, b2) => a.name.localeCompare(b2.name));
40219
40312
  for (const migration of migrations) {
40220
40313
  const migrationPath = migration.path;
@@ -40253,9 +40346,9 @@ var init_migrator2 = __esm({
40253
40346
 
40254
40347
  // src/server/infra/db/queries/settings.ts
40255
40348
  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";
40349
+ import { mkdirSync as mkdirSync4, readFileSync as readFileSync5, renameSync as renameSync5, statSync as statSync8, writeFileSync as writeFileSync3 } from "node:fs";
40257
40350
  import { homedir as homedir2 } from "node:os";
40258
- import { dirname as dirname3, join as join11 } from "node:path";
40351
+ import { dirname as dirname4, join as join11 } from "node:path";
40259
40352
  function bandHome() {
40260
40353
  if (process.env.BAND_HOME) return process.env.BAND_HOME;
40261
40354
  return join11(homedir2(), ".band");
@@ -40294,7 +40387,7 @@ var init_settings = __esm({
40294
40387
  const filePath = settingsFile();
40295
40388
  let mtimeMs;
40296
40389
  try {
40297
- mtimeMs = statSync7(filePath).mtimeMs;
40390
+ mtimeMs = statSync8(filePath).mtimeMs;
40298
40391
  } catch {
40299
40392
  settingsCache.delete(filePath);
40300
40393
  return {};
@@ -40337,11 +40430,11 @@ var init_settings = __esm({
40337
40430
  const merged = { ...existing, ...patch };
40338
40431
  const data = `${JSON.stringify(merged, null, 2)}
40339
40432
  `;
40340
- const dir = dirname3(filePath);
40341
- mkdirSync3(dir, { recursive: true });
40433
+ const dir = dirname4(filePath);
40434
+ mkdirSync4(dir, { recursive: true });
40342
40435
  const tmpPath = `${filePath}.tmp.${process.pid}`;
40343
40436
  writeFileSync3(tmpPath, data, "utf-8");
40344
- renameSync4(tmpPath, filePath);
40437
+ renameSync5(tmpPath, filePath);
40345
40438
  settingsCache.delete(filePath);
40346
40439
  }
40347
40440
  /**
@@ -41075,13 +41168,13 @@ var init_schema2 = __esm({
41075
41168
  });
41076
41169
 
41077
41170
  // src/server/infra/db/connection.ts
41078
- import { mkdirSync as mkdirSync4 } from "node:fs";
41171
+ import { mkdirSync as mkdirSync5 } from "node:fs";
41079
41172
  import { join as join12 } from "node:path";
41080
41173
  import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
41081
41174
  function getDb() {
41082
41175
  if (_db) return _db;
41083
41176
  const home = bandHome();
41084
- mkdirSync4(home, { recursive: true });
41177
+ mkdirSync5(home, { recursive: true });
41085
41178
  const dbPath = join12(home, "band.db");
41086
41179
  _sqlite = new DatabaseSync2(dbPath);
41087
41180
  _sqlite.exec("PRAGMA journal_mode = WAL");
@@ -41411,16 +41504,63 @@ var init_cli_invocation = __esm({
41411
41504
  });
41412
41505
 
41413
41506
  // ../../packages/coding-agent/src/hook-status.ts
41507
+ function hasBackgroundWork(payload) {
41508
+ const crons = payload.session_crons;
41509
+ if (Array.isArray(crons) && crons.length > 0) return true;
41510
+ const tasks3 = payload.background_tasks;
41511
+ if (!Array.isArray(tasks3)) return false;
41512
+ return tasks3.some((task) => {
41513
+ if (typeof task !== "object" || task === null) return true;
41514
+ const { type, status } = task;
41515
+ if (typeof type === "string" && type.trim().toLowerCase() === "teammate") return false;
41516
+ const state2 = typeof status === "string" ? status.trim().toLowerCase() : "";
41517
+ return !TERMINAL_TASK_STATUSES.has(state2);
41518
+ });
41519
+ }
41414
41520
  function mapClaudeCodeHookStatus(payload) {
41415
41521
  const hookEvent = typeof payload.hook_event_name === "string" ? payload.hook_event_name : "";
41416
41522
  const toolName2 = typeof payload.tool_name === "string" ? payload.tool_name : "";
41417
- if (hookEvent === "Stop") {
41418
- return "needs_attention";
41523
+ switch (hookEvent) {
41524
+ case "PermissionRequest":
41525
+ return "question";
41526
+ case "PreToolUse":
41527
+ return CLAUDE_CODE_INTERACTIVE_TOOLS.has(toolName2) ? "question" : "working";
41528
+ case "Stop":
41529
+ return hasBackgroundWork(payload) ? "monitoring" : "done";
41530
+ case "StopFailure":
41531
+ return "failed";
41532
+ case "PostCompact":
41533
+ return payload.trigger === "manual" ? "done" : "working";
41534
+ default:
41535
+ return "working";
41419
41536
  }
41420
- if ((hookEvent === "PreToolUse" || hookEvent === "PermissionRequest") && CLAUDE_CODE_INTERACTIVE_TOOLS.has(toolName2)) {
41421
- return "needs_attention";
41537
+ }
41538
+ function foldSubagents(hold, payload, mapped) {
41539
+ const hookEvent = typeof payload.hook_event_name === "string" ? payload.hook_event_name : "";
41540
+ const agentId = typeof payload.agent_id === "string" ? payload.agent_id : "";
41541
+ if (hookEvent === "SubagentStart") {
41542
+ if (agentId) hold.running.add(agentId);
41543
+ return hold.deferred ? "working" : mapped;
41544
+ }
41545
+ if (hookEvent === "SubagentStop") {
41546
+ hold.running.delete(agentId);
41547
+ if (hold.running.size === 0 && hold.deferred) {
41548
+ const settled = hold.deferred;
41549
+ hold.deferred = null;
41550
+ return settled;
41551
+ }
41552
+ return mapped;
41553
+ }
41554
+ if ((hookEvent === "Stop" || hookEvent === "StopFailure") && !agentId) {
41555
+ if (hold.running.size > 0 && (mapped === "done" || mapped === "monitoring")) {
41556
+ hold.deferred = mapped;
41557
+ return "working";
41558
+ }
41559
+ hold.deferred = null;
41560
+ return mapped;
41422
41561
  }
41423
- return "working";
41562
+ if (hookEvent === "UserPromptSubmit") hold.deferred = null;
41563
+ return mapped;
41424
41564
  }
41425
41565
  async function mapHookPayloadToStatus(agentType, payload) {
41426
41566
  switch (agentType) {
@@ -41441,10 +41581,31 @@ function isHookSessionEnd(agentType, payload) {
41441
41581
  function hookSessionId(payload) {
41442
41582
  return typeof payload.session_id === "string" && payload.session_id ? payload.session_id : null;
41443
41583
  }
41444
- var CLAUDE_CODE_INTERACTIVE_TOOLS;
41584
+ var CLAUDE_CODE_INTERACTIVE_TOOLS, TERMINAL_TASK_STATUSES;
41445
41585
  var init_hook_status = __esm({
41446
41586
  "../../packages/coding-agent/src/hook-status.ts"() {
41447
41587
  CLAUDE_CODE_INTERACTIVE_TOOLS = /* @__PURE__ */ new Set(["AskUserQuestion", "ExitPlanMode"]);
41588
+ TERMINAL_TASK_STATUSES = /* @__PURE__ */ new Set([
41589
+ "idle",
41590
+ "done",
41591
+ "success",
41592
+ "succeeded",
41593
+ "complete",
41594
+ "completed",
41595
+ "finished",
41596
+ "failed",
41597
+ "error",
41598
+ "terminated",
41599
+ "exited",
41600
+ "aborted",
41601
+ "expired",
41602
+ "skipped",
41603
+ "crashed",
41604
+ "killed",
41605
+ "cancelled",
41606
+ "canceled",
41607
+ "timed_out"
41608
+ ]);
41448
41609
  }
41449
41610
  });
41450
41611
 
@@ -41510,8 +41671,8 @@ var init_pricing = __esm({
41510
41671
  // ../../packages/coding-agent/src/usage/read-lines.ts
41511
41672
  import { createReadStream as createReadStream2 } from "node:fs";
41512
41673
  import { createInterface } from "node:readline";
41513
- async function* readLines(file2) {
41514
- const input = createReadStream2(file2);
41674
+ async function* readLines(file3) {
41675
+ const input = createReadStream2(file3);
41515
41676
  const rl = createInterface({ input, crlfDelay: Number.POSITIVE_INFINITY });
41516
41677
  try {
41517
41678
  for await (const line2 of rl) yield line2;
@@ -41568,10 +41729,10 @@ async function repoDirsFor(canonical) {
41568
41729
  }
41569
41730
  return dirs;
41570
41731
  }
41571
- async function readSessionCwd(file2) {
41732
+ async function readSessionCwd(file3) {
41572
41733
  let handle;
41573
41734
  try {
41574
- handle = await open(file2, "r");
41735
+ handle = await open(file3, "r");
41575
41736
  const buf = Buffer.alloc(SESSION_HEAD_BYTES);
41576
41737
  const { bytesRead } = await handle.read(buf, 0, SESSION_HEAD_BYTES, 0);
41577
41738
  if (bytesRead === 0) return null;
@@ -41617,13 +41778,13 @@ async function listSessions(dir) {
41617
41778
  if (!name4.endsWith(".jsonl")) return;
41618
41779
  const sessionId = name4.slice(0, -".jsonl".length);
41619
41780
  if (!SESSION_ID_RE.test(sessionId)) return;
41620
- const file2 = join14(repoDir, name4);
41621
- const cwd = await readSessionCwd(file2);
41781
+ const file3 = join14(repoDir, name4);
41782
+ const cwd = await readSessionCwd(file3);
41622
41783
  if (cwd === null) return;
41623
41784
  if (cwd !== void 0 && cwd !== dir && cwd !== canonical) return;
41624
41785
  let lastModified;
41625
41786
  try {
41626
- lastModified = (await stat(file2)).mtimeMs;
41787
+ lastModified = (await stat(file3)).mtimeMs;
41627
41788
  } catch {
41628
41789
  return;
41629
41790
  }
@@ -41637,9 +41798,9 @@ async function listSessions(dir) {
41637
41798
  }
41638
41799
  async function findSessionFile(sessionId, dir) {
41639
41800
  for (const repoDir of await repoDirsFor(await canonicalDir(dir))) {
41640
- const file2 = join14(repoDir, `${sessionId}.jsonl`);
41801
+ const file3 = join14(repoDir, `${sessionId}.jsonl`);
41641
41802
  try {
41642
- if ((await stat(file2)).size > 0) return file2;
41803
+ if ((await stat(file3)).size > 0) return file3;
41643
41804
  } catch {
41644
41805
  }
41645
41806
  }
@@ -41647,15 +41808,15 @@ async function findSessionFile(sessionId, dir) {
41647
41808
  }
41648
41809
  async function getSessionUsage(sessionId, dir) {
41649
41810
  if (!SESSION_ID_RE.test(sessionId)) return null;
41650
- const file2 = await findSessionFile(sessionId, dir);
41651
- if (!file2) return null;
41811
+ const file3 = await findSessionFile(sessionId, dir);
41812
+ if (!file3) return null;
41652
41813
  const turns = [];
41653
41814
  const turnByMessageId = /* @__PURE__ */ new Map();
41654
41815
  let conversationRecords = 0;
41655
41816
  let startedAt = Number.POSITIVE_INFINITY;
41656
41817
  let updatedAt = 0;
41657
41818
  let modelFallback = "";
41658
- for await (const line2 of readLines(file2)) {
41819
+ for await (const line2 of readLines(file3)) {
41659
41820
  if (!line2.trim()) continue;
41660
41821
  let record3;
41661
41822
  try {
@@ -41768,9 +41929,9 @@ async function findSessionFiles() {
41768
41929
  }
41769
41930
  return results;
41770
41931
  }
41771
- async function readSessionMeta(file2) {
41932
+ async function readSessionMeta(file3) {
41772
41933
  let first;
41773
- for await (const line2 of readLines(file2)) {
41934
+ for await (const line2 of readLines(file3)) {
41774
41935
  if (!line2.trim()) continue;
41775
41936
  first = line2;
41776
41937
  break;
@@ -41782,27 +41943,27 @@ async function readSessionMeta(file2) {
41782
41943
  }
41783
41944
  return void 0;
41784
41945
  }
41785
- async function getSessionMeta(file2) {
41786
- const cached2 = metaCache.get(file2.path);
41787
- if (cached2 && cached2.mtimeMs === file2.mtimeMs) return cached2.meta;
41946
+ async function getSessionMeta(file3) {
41947
+ const cached2 = metaCache.get(file3.path);
41948
+ if (cached2 && cached2.mtimeMs === file3.mtimeMs) return cached2.meta;
41788
41949
  try {
41789
- const meta = await readSessionMeta(file2.path);
41790
- metaCache.set(file2.path, { mtimeMs: file2.mtimeMs, meta });
41950
+ const meta = await readSessionMeta(file3.path);
41951
+ metaCache.set(file3.path, { mtimeMs: file3.mtimeMs, meta });
41791
41952
  return meta;
41792
41953
  } catch (err) {
41793
- metaCache.set(file2.path, { mtimeMs: file2.mtimeMs, meta: void 0 });
41954
+ metaCache.set(file3.path, { mtimeMs: file3.mtimeMs, meta: void 0 });
41794
41955
  throw err;
41795
41956
  }
41796
41957
  }
41797
41958
  async function listSessions2(dir) {
41798
41959
  const sessions = [];
41799
- for (const file2 of await findSessionFiles()) {
41960
+ for (const file3 of await findSessionFiles()) {
41800
41961
  try {
41801
- const meta = await getSessionMeta(file2);
41962
+ const meta = await getSessionMeta(file3);
41802
41963
  if (!meta || meta.cwd !== dir) continue;
41803
- sessions.push({ sessionId: meta.id, lastModified: file2.mtimeMs });
41964
+ sessions.push({ sessionId: meta.id, lastModified: file3.mtimeMs });
41804
41965
  } catch (err) {
41805
- log7.debug({ err, file: file2.path }, "failed to parse codex session file");
41966
+ log7.debug({ err, file: file3.path }, "failed to parse codex session file");
41806
41967
  }
41807
41968
  }
41808
41969
  return sessions.sort((a, b2) => b2.lastModified - a.lastModified);
@@ -42064,10 +42225,10 @@ var init_src3 = __esm({
42064
42225
  });
42065
42226
 
42066
42227
  // ../../packages/host-local/src/agents/acp-launch.ts
42067
- import { accessSync, constants, existsSync as existsSync7, statSync as statSync8 } from "node:fs";
42228
+ import { accessSync, constants, existsSync as existsSync8, statSync as statSync9 } from "node:fs";
42068
42229
  import { createRequire } from "node:module";
42069
42230
  import { homedir as homedir6 } from "node:os";
42070
- import { delimiter as delimiter2, dirname as dirname4, join as join16, resolve as resolvePath } from "node:path";
42231
+ import { delimiter as delimiter2, dirname as dirname5, join as join16, resolve as resolvePath } from "node:path";
42071
42232
  import { fileURLToPath } from "node:url";
42072
42233
  function findOnPath(name4, path2, extra = []) {
42073
42234
  for (const dir of [...path2.split(delimiter2), ...extra]) {
@@ -42075,24 +42236,24 @@ function findOnPath(name4, path2, extra = []) {
42075
42236
  const candidate = join16(dir, name4);
42076
42237
  try {
42077
42238
  accessSync(candidate, constants.X_OK);
42078
- if (statSync8(candidate).isFile()) return candidate;
42239
+ if (statSync9(candidate).isFile()) return candidate;
42079
42240
  } catch {
42080
42241
  }
42081
42242
  }
42082
42243
  return null;
42083
42244
  }
42084
42245
  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");
42246
+ const bundled = join16(dirname5(fileURLToPath(import.meta.url)), "agents", `${bundledName}.mjs`);
42247
+ if (existsSync8(bundled)) return bundled;
42248
+ return join16(dirname5(require2.resolve(`${pkg}/package.json`)), "dist", "index.js");
42088
42249
  }
42089
42250
  function nodeFor(path2) {
42090
42251
  return findOnPath("node", path2) ?? process.execPath;
42091
42252
  }
42092
- function isExecutableFile(file2) {
42253
+ function isExecutableFile(file3) {
42093
42254
  try {
42094
- accessSync(file2, constants.X_OK);
42095
- return statSync8(file2).isFile();
42255
+ accessSync(file3, constants.X_OK);
42256
+ return statSync9(file3).isFile();
42096
42257
  } catch {
42097
42258
  return false;
42098
42259
  }
@@ -42165,7 +42326,7 @@ var init_acp_launch = __esm({
42165
42326
 
42166
42327
  // ../../packages/host-local/src/agents/claude-defaults.ts
42167
42328
  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";
42329
+ import { closeSync as closeSync3, openSync as openSync3, readFileSync as readFileSync6, readSync, realpathSync as realpathSync2, statSync as statSync10 } from "node:fs";
42169
42330
  import { homedir as homedir7 } from "node:os";
42170
42331
  import { join as join17, resolve as resolvePath2 } from "node:path";
42171
42332
  function claudeConfigDir2(env) {
@@ -42258,16 +42419,16 @@ function transcriptPath(env, cwd, sessionId) {
42258
42419
  function readTail(path2, bytes) {
42259
42420
  let fd;
42260
42421
  try {
42261
- const size = statSync9(path2).size;
42422
+ const size = statSync10(path2).size;
42262
42423
  const length = Math.min(size, bytes);
42263
42424
  const buf = Buffer.alloc(length);
42264
- fd = openSync2(path2, "r");
42425
+ fd = openSync3(path2, "r");
42265
42426
  readSync(fd, buf, 0, length, size - length);
42266
42427
  return buf.toString("utf8");
42267
42428
  } catch {
42268
42429
  return null;
42269
42430
  } finally {
42270
- if (fd !== void 0) closeSync2(fd);
42431
+ if (fd !== void 0) closeSync3(fd);
42271
42432
  }
42272
42433
  }
42273
42434
  function reportedClaudeDefaults(opts) {
@@ -42368,113 +42529,225 @@ var init_claude_defaults = __esm({
42368
42529
  });
42369
42530
 
42370
42531
  // ../../packages/host-local/src/agents/hooks-install.ts
42371
- import { mkdirSync as mkdirSync5, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "node:fs";
42532
+ import {
42533
+ mkdirSync as mkdirSync6,
42534
+ readFileSync as readFileSync7,
42535
+ realpathSync as realpathSync3,
42536
+ renameSync as renameSync6,
42537
+ statSync as statSync11,
42538
+ unlinkSync as unlinkSync4,
42539
+ writeFileSync as writeFileSync4
42540
+ } from "node:fs";
42372
42541
  import { homedir as homedir8 } from "node:os";
42373
- import { dirname as dirname5, join as join18 } from "node:path";
42542
+ import { dirname as dirname6, join as join18 } from "node:path";
42374
42543
  function claudeSettingsPath() {
42375
42544
  return join18(homedir8(), ".claude", "settings.json");
42376
42545
  }
42377
42546
  function loadClaudeSettings() {
42547
+ let text3;
42378
42548
  try {
42379
- const data = readFileSync7(claudeSettingsPath(), "utf-8");
42380
- return JSON.parse(data);
42549
+ text3 = readFileSync7(claudeSettingsPath(), "utf-8");
42550
+ } catch (err) {
42551
+ if (err.code === "ENOENT") {
42552
+ return { settings: {}, indent: 2, trailingNewline: false };
42553
+ }
42554
+ throw err;
42555
+ }
42556
+ if (text3.trim() === "") return { settings: {}, indent: 2, trailingNewline: false };
42557
+ let parsed;
42558
+ try {
42559
+ parsed = JSON.parse(text3);
42560
+ } catch {
42561
+ throw new Error(`${claudeSettingsPath()} is not valid JSON; fix it before installing hooks.`);
42562
+ }
42563
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
42564
+ throw new Error(
42565
+ `${claudeSettingsPath()} is not a JSON object; fix it before installing hooks.`
42566
+ );
42567
+ }
42568
+ const indentMatch = /^([ \t]+)\S/m.exec(text3);
42569
+ return {
42570
+ settings: parsed,
42571
+ indent: indentMatch ? indentMatch[1] : 2,
42572
+ trailingNewline: text3.endsWith("\n")
42573
+ };
42574
+ }
42575
+ function saveClaudeSettings({ settings: settings3, indent, trailingNewline }) {
42576
+ let path2 = claudeSettingsPath();
42577
+ mkdirSync6(dirname6(path2), { recursive: true });
42578
+ let mode = 384;
42579
+ try {
42580
+ path2 = realpathSync3(path2);
42581
+ mode = statSync11(path2).mode & 511;
42381
42582
  } catch {
42382
- return {};
42583
+ }
42584
+ const temp = `${path2}.band-${process.pid}-${Date.now()}.tmp`;
42585
+ try {
42586
+ writeFileSync4(temp, JSON.stringify(settings3, null, indent) + (trailingNewline ? "\n" : ""), {
42587
+ encoding: "utf-8",
42588
+ mode,
42589
+ flag: "wx"
42590
+ });
42591
+ renameSync6(temp, path2);
42592
+ } catch (err) {
42593
+ try {
42594
+ unlinkSync4(temp);
42595
+ } catch {
42596
+ }
42597
+ throw err;
42383
42598
  }
42384
42599
  }
42385
42600
  function isBandHook(command2) {
42386
- return command2.includes("band") && command2.includes("notify");
42601
+ return BAND_NOTIFY.test(command2);
42387
42602
  }
42388
- async function checkHooks() {
42389
- const settings3 = loadClaudeSettings();
42603
+ function isBandEntry(entry) {
42604
+ const hooks = entry?.hooks;
42605
+ return Array.isArray(hooks) && hooks.some(
42606
+ (h) => h?.type === "command" && typeof h.command === "string" && isBandHook(h.command)
42607
+ );
42608
+ }
42609
+ function isCurrentCommand(hook) {
42610
+ const command2 = hook.command ?? "";
42611
+ return command2.includes("--agent claude-code") && command2.includes(">/dev/null 2>&1") && command2.trimEnd().endsWith("|| true") && hook.async === true;
42612
+ }
42613
+ function hooksOf(settings3) {
42390
42614
  const hooks = settings3.hooks;
42391
- if (!hooks) {
42392
- return { installed: false, other_hooks_exist: false };
42615
+ return hooks !== null && typeof hooks === "object" && !Array.isArray(hooks) ? hooks : {};
42616
+ }
42617
+ async function checkHooks() {
42618
+ let settings3;
42619
+ try {
42620
+ settings3 = loadClaudeSettings().settings;
42621
+ } catch {
42622
+ return statusOf(0, 0, false);
42393
42623
  }
42394
- let bandHookCount = 0;
42624
+ const hooks = hooksOf(settings3);
42625
+ let current3 = 0;
42626
+ let bandEvents = 0;
42395
42627
  let otherHookCount = 0;
42396
- for (const [, eventHooks] of Object.entries(hooks)) {
42628
+ for (const [event, eventHooks] of Object.entries(hooks)) {
42397
42629
  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
- }
42630
+ let eventHasBand = false;
42631
+ let eventIsCurrent = false;
42632
+ for (const entry of eventHooks) {
42633
+ const inner = entry?.hooks;
42634
+ if (!Array.isArray(inner)) continue;
42635
+ for (const h of inner) {
42636
+ if (h?.type !== "command" || typeof h.command !== "string") continue;
42637
+ if (isBandHook(h.command)) {
42638
+ eventHasBand = true;
42639
+ if (HOOK_EVENTS.includes(event) && isCurrentCommand(h)) eventIsCurrent = true;
42640
+ } else {
42641
+ otherHookCount++;
42408
42642
  }
42409
42643
  }
42410
42644
  }
42645
+ if (eventHasBand) bandEvents++;
42646
+ if (eventIsCurrent) current3++;
42411
42647
  }
42648
+ return statusOf(bandEvents, current3, otherHookCount > 0);
42649
+ }
42650
+ function statusOf(bandEvents, currentEvents, otherHooksExist) {
42651
+ const installed = currentEvents >= HOOK_EVENTS.length && bandEvents === currentEvents;
42412
42652
  return {
42413
- installed: bandHookCount >= HOOK_EVENTS.length,
42414
- other_hooks_exist: otherHookCount > 0
42653
+ installed,
42654
+ outdated: !installed && bandEvents > 0,
42655
+ other_hooks_exist: otherHooksExist,
42656
+ version: HOOKS_VERSION
42415
42657
  };
42416
42658
  }
42417
- async function installHooks() {
42418
- let bandPath = null;
42659
+ function hookCommand(bandPath) {
42660
+ return `"${bandPath}" notify --agent claude-code >/dev/null 2>&1 || true`;
42661
+ }
42662
+ async function findBand() {
42419
42663
  if (process.platform !== "win32") {
42420
42664
  try {
42421
42665
  await import("node:fs/promises").then((m2) => m2.stat("/usr/local/bin/band"));
42422
- bandPath = "/usr/local/bin/band";
42666
+ return "/usr/local/bin/band";
42423
42667
  } catch {
42424
42668
  }
42425
42669
  }
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 || {};
42670
+ const found = await whichBinary("band");
42671
+ if (!found) throw new Error("Could not find band CLI binary. Please install it first.");
42672
+ return found;
42673
+ }
42674
+ function withoutBandEntries(hooks) {
42675
+ const result = {};
42436
42676
  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) => {
42677
+ if (!Array.isArray(eventHooks)) {
42678
+ result[event] = eventHooks;
42679
+ continue;
42680
+ }
42681
+ const kept = [];
42682
+ for (const entry of eventHooks) {
42683
+ if (!isBandEntry(entry)) {
42684
+ kept.push(entry);
42685
+ continue;
42686
+ }
42440
42687
  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];
42688
+ const rest = (e.hooks ?? []).filter(
42689
+ (h) => !(h?.type === "command" && typeof h.command === "string" && isBandHook(h.command))
42690
+ );
42691
+ if (rest.length > 0) kept.push({ ...e, hooks: rest });
42448
42692
  }
42693
+ if (kept.length > 0 || eventHooks.length === 0) result[event] = kept;
42449
42694
  }
42695
+ return result;
42696
+ }
42697
+ async function installHooks() {
42698
+ const bandPath = await findBand();
42699
+ const loaded = loadClaudeSettings();
42700
+ const hooks = withoutBandEntries(hooksOf(loaded.settings));
42450
42701
  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;
42702
+ const existing = Array.isArray(hooks[event]) ? hooks[event] : [];
42703
+ hooks[event] = [
42704
+ ...existing,
42705
+ {
42706
+ hooks: [
42707
+ {
42708
+ type: "command",
42709
+ command: hookCommand(bandPath),
42710
+ async: true,
42711
+ timeout: HOOK_TIMEOUT_SECONDS
42712
+ }
42713
+ ]
42714
+ }
42715
+ ];
42461
42716
  }
42462
- settings3.hooks = hooks;
42463
- writeFileSync4(settingsPath, JSON.stringify(settings3, null, 2), "utf-8");
42717
+ loaded.settings.hooks = hooks;
42718
+ saveClaudeSettings(loaded);
42719
+ }
42720
+ async function removeHooks() {
42721
+ const loaded = loadClaudeSettings();
42722
+ if (!("hooks" in loaded.settings)) return;
42723
+ const before = hooksOf(loaded.settings);
42724
+ const after = withoutBandEntries(before);
42725
+ if (JSON.stringify(after) === JSON.stringify(before)) return;
42726
+ if (Object.keys(after).length > 0) loaded.settings.hooks = after;
42727
+ else delete loaded.settings.hooks;
42728
+ saveClaudeSettings(loaded);
42464
42729
  }
42465
- var HOOK_EVENTS;
42730
+ var HOOK_EVENTS, HOOKS_VERSION, HOOK_TIMEOUT_SECONDS, BAND_NOTIFY;
42466
42731
  var init_hooks_install = __esm({
42467
42732
  "../../packages/host-local/src/agents/hooks-install.ts"() {
42468
42733
  "use strict";
42469
42734
  init_path();
42470
42735
  HOOK_EVENTS = [
42471
- "PreToolUse",
42472
- "PermissionRequest",
42473
42736
  "UserPromptSubmit",
42737
+ "PreToolUse",
42474
42738
  "PostToolUse",
42739
+ "PostToolUseFailure",
42740
+ "PermissionRequest",
42475
42741
  "Stop",
42742
+ "StopFailure",
42743
+ "SubagentStart",
42744
+ "SubagentStop",
42745
+ "PostCompact",
42476
42746
  "SessionEnd"
42477
42747
  ];
42748
+ HOOKS_VERSION = 2;
42749
+ HOOK_TIMEOUT_SECONDS = 5;
42750
+ BAND_NOTIFY = /(^|[\s"'/\\])band(\.exe|\.cmd)?["']?\s+notify(\s|$)/;
42478
42751
  }
42479
42752
  });
42480
42753
 
@@ -42553,11 +42826,11 @@ var init_mcp_stdio = __esm({
42553
42826
  });
42554
42827
 
42555
42828
  // ../../packages/host-local/src/process/cli-binary.ts
42556
- import { lstatSync as lstatSync3, realpathSync as realpathSync3 } from "node:fs";
42829
+ import { lstatSync as lstatSync3, realpathSync as realpathSync4 } from "node:fs";
42557
42830
  import { platform } from "node:os";
42558
42831
  import { delimiter as delimiter3, join as join19, resolve as resolve4 } from "node:path";
42559
42832
  function findCliBinaryAt(opts) {
42560
- const { cwd, dirname: dirname16 } = opts;
42833
+ const { cwd, dirname: dirname17 } = opts;
42561
42834
  const exe = platform() === "win32" ? "band.exe" : "band";
42562
42835
  const explicit = process.env.BAND_CLI_PATH;
42563
42836
  if (explicit) {
@@ -42573,7 +42846,7 @@ function findCliBinaryAt(opts) {
42573
42846
  // cwd = repo root (fallback)
42574
42847
  resolve4(cwd, "apps"),
42575
42848
  // From this source file in dev (packages/host-local/src/process/ → apps/)
42576
- resolve4(dirname16, "..", "..", "..", "..", "apps"),
42849
+ resolve4(dirname17, "..", "..", "..", "..", "apps"),
42577
42850
  // From bundled `dist/` file (<Resources>/web/dist/ → <Resources>/) only
42578
42851
  // — in dev mode this resolves to `packages/`, which has no
42579
42852
  // `cli/target/<profile>/band` and is harmless; the walk above is the
@@ -42581,7 +42854,7 @@ function findCliBinaryAt(opts) {
42581
42854
  // layout under <Resources>/cli/ would still resolve. Today's Electron
42582
42855
  // bundle ships the binary under `binaries/` (handled by Strategy B),
42583
42856
  // so this strategy never hits in production either.
42584
- resolve4(dirname16, "..", "..", "..")
42857
+ resolve4(dirname17, "..", "..", "..")
42585
42858
  ];
42586
42859
  for (const appsDir of appsStrategies) {
42587
42860
  for (const profile of ["release", "debug"]) {
@@ -42598,7 +42871,7 @@ function findCliBinaryAt(opts) {
42598
42871
  resolve4(cwd, "..", "binaries", exe),
42599
42872
  // From the bundled dist file (<Resources>/web/dist/start-server.mjs)
42600
42873
  // → <Resources>/binaries/band
42601
- resolve4(dirname16, "..", "..", "binaries", exe)
42874
+ resolve4(dirname17, "..", "..", "binaries", exe)
42602
42875
  ];
42603
42876
  for (const p2 of electronCandidates) {
42604
42877
  try {
@@ -42610,7 +42883,7 @@ function findCliBinaryAt(opts) {
42610
42883
  for (const dir of (process.env.PATH ?? "").split(delimiter3)) {
42611
42884
  if (!dir) continue;
42612
42885
  try {
42613
- return realpathSync3(join19(dir, exe));
42886
+ return realpathSync4(join19(dir, exe));
42614
42887
  } catch {
42615
42888
  }
42616
42889
  }
@@ -42627,21 +42900,21 @@ var init_cli_binary = __esm({
42627
42900
 
42628
42901
  // ../../packages/host-local/src/agents/skills-install.ts
42629
42902
  import { execFile as execFile4 } from "node:child_process";
42630
- import { statSync as statSync10 } from "node:fs";
42903
+ import { statSync as statSync12 } from "node:fs";
42631
42904
  import { homedir as homedir10 } from "node:os";
42632
42905
  import { join as join20 } from "node:path";
42633
42906
  async function findBandBinary() {
42634
42907
  const override = process.env.BAND_CLI_BIN;
42635
42908
  if (override) {
42636
42909
  try {
42637
- statSync10(override);
42910
+ statSync12(override);
42638
42911
  return override;
42639
42912
  } catch {
42640
42913
  }
42641
42914
  }
42642
42915
  if (process.platform !== "win32") {
42643
42916
  try {
42644
- statSync10("/usr/local/bin/band");
42917
+ statSync12("/usr/local/bin/band");
42645
42918
  return "/usr/local/bin/band";
42646
42919
  } catch {
42647
42920
  }
@@ -42743,7 +43016,7 @@ var init_skills_install = __esm({
42743
43016
 
42744
43017
  // ../../packages/host-local/src/browser/chromium.ts
42745
43018
  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";
43019
+ import { closeSync as closeSync4, existsSync as existsSync9, openSync as openSync4, readdirSync as readdirSync7 } from "node:fs";
42747
43020
  import { mkdir as mkdir2, readFile as readFile2, rm } from "node:fs/promises";
42748
43021
  import { homedir as homedir11 } from "node:os";
42749
43022
  import { join as join21 } from "node:path";
@@ -42752,7 +43025,7 @@ function playwrightCandidates() {
42752
43025
  process.env.PLAYWRIGHT_BROWSERS_PATH,
42753
43026
  join21(homedir11(), "Library/Caches/ms-playwright"),
42754
43027
  join21(homedir11(), ".cache/ms-playwright")
42755
- ].filter((root) => !!root && existsSync8(root));
43028
+ ].filter((root) => !!root && existsSync9(root));
42756
43029
  const found = [];
42757
43030
  for (const root of roots) {
42758
43031
  const dirs = readdirSync7(root).filter((d) => /^chromium-\d+$/.test(d)).sort().reverse();
@@ -42782,7 +43055,7 @@ function playwrightCandidates() {
42782
43055
  }
42783
43056
  function findChromium() {
42784
43057
  const fromEnv = process.env.BAND_CHROMIUM_BIN;
42785
- if (fromEnv) return existsSync8(fromEnv) ? fromEnv : void 0;
43058
+ if (fromEnv) return existsSync9(fromEnv) ? fromEnv : void 0;
42786
43059
  const candidates = [
42787
43060
  "/usr/bin/chromium",
42788
43061
  "/usr/bin/chromium-browser",
@@ -42792,10 +43065,10 @@ function findChromium() {
42792
43065
  "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
42793
43066
  ...playwrightCandidates()
42794
43067
  ];
42795
- return candidates.find((c) => existsSync8(c));
43068
+ return candidates.find((c) => existsSync9(c));
42796
43069
  }
42797
43070
  async function readDevToolsPort(profileDir, proc, logPath, timeoutMs) {
42798
- const file2 = join21(profileDir, "DevToolsActivePort");
43071
+ const file3 = join21(profileDir, "DevToolsActivePort");
42799
43072
  const deadline = Date.now() + timeoutMs;
42800
43073
  while (Date.now() < deadline) {
42801
43074
  if (proc.exitCode !== null || proc.signalCode !== null) {
@@ -42805,7 +43078,7 @@ async function readDevToolsPort(profileDir, proc, logPath, timeoutMs) {
42805
43078
  throw new Error("Chromium hit a fatal error at startup");
42806
43079
  }
42807
43080
  try {
42808
- const [port, path2] = (await readFile2(file2, "utf8")).split("\n");
43081
+ const [port, path2] = (await readFile2(file3, "utf8")).split("\n");
42809
43082
  if (port && path2) return { port: Number(port), path: path2.trim() };
42810
43083
  } catch {
42811
43084
  }
@@ -42955,13 +43228,13 @@ var init_chromium = __esm({
42955
43228
  if (attempt > 1) {
42956
43229
  await Promise.all(SINGLETON_FILES.map((f2) => rm(join21(profileDir, f2), { force: true })));
42957
43230
  }
42958
- const logFd = openSync3(logPath, "w", 384);
43231
+ const logFd = openSync4(logPath, "w", 384);
42959
43232
  const proc = spawn2(bin, args, {
42960
43233
  stdio: ["ignore", "ignore", logFd],
42961
43234
  detached: true,
42962
43235
  env: { ...env, HOME: home }
42963
43236
  });
42964
- closeSync3(logFd);
43237
+ closeSync4(logFd);
42965
43238
  const exited = new Promise((resolve15) => {
42966
43239
  proc.once("exit", () => resolve15());
42967
43240
  proc.once("error", () => resolve15());
@@ -43380,7 +43653,7 @@ var init_git_client = __esm({
43380
43653
 
43381
43654
  // ../../packages/host-local/src/lsp/lsp-manager.ts
43382
43655
  import { spawn as spawn3 } from "node:child_process";
43383
- import { dirname as dirname6, join as join24, resolve as resolve5 } from "node:path";
43656
+ import { dirname as dirname7, join as join24, resolve as resolve5 } from "node:path";
43384
43657
  import { fileURLToPath as fileURLToPath2 } from "node:url";
43385
43658
  function toServerId(worktreeId4, lang) {
43386
43659
  return `${worktreeId4}:${lang}`;
@@ -43505,7 +43778,7 @@ var init_lsp_manager = __esm({
43505
43778
  "use strict";
43506
43779
  init_src();
43507
43780
  init_path();
43508
- __dirname2 = dirname6(fileURLToPath2(import.meta.url));
43781
+ __dirname2 = dirname7(fileURLToPath2(import.meta.url));
43509
43782
  log11 = createLogger("lsp");
43510
43783
  LANG_SERVER_CONFIG = {
43511
43784
  typescript: { command: "typescript-language-server", args: ["--stdio"] }
@@ -47803,27 +48076,27 @@ function parseEnvironment(text3) {
47803
48076
  if (parsed.success) return { ok: true, environment: parsed.data };
47804
48077
  return { ok: false, issues: zodIssues(parsed.error) };
47805
48078
  }
47806
- function repoRelative(file2) {
47807
- if (posix.isAbsolute(file2) || /^[A-Za-z]:[\\/]/.test(file2)) return null;
47808
- const normal = posix.normalize(file2.replaceAll("\\", "/"));
48079
+ function repoRelative(file3) {
48080
+ if (posix.isAbsolute(file3) || /^[A-Za-z]:[\\/]/.test(file3)) return null;
48081
+ const normal = posix.normalize(file3.replaceAll("\\", "/"));
47809
48082
  if (normal === ".." || normal.startsWith("../")) return null;
47810
48083
  return normal;
47811
48084
  }
47812
48085
  async function checkReferences(environment, exists3) {
47813
48086
  const issues = [];
47814
48087
  for (const key2 of ["devcontainer", "dockerfile"]) {
47815
- const file2 = environment.build?.[key2];
47816
- if (file2 === void 0) continue;
47817
- const relative2 = repoRelative(file2);
48088
+ const file3 = environment.build?.[key2];
48089
+ if (file3 === void 0) continue;
48090
+ const relative2 = repoRelative(file3);
47818
48091
  if (relative2 === null) {
47819
48092
  issues.push({
47820
48093
  path: `build.${key2}`,
47821
- message: `"${file2}" must be a path inside the repository`
48094
+ message: `"${file3}" must be a path inside the repository`
47822
48095
  });
47823
48096
  } else if (!await exists3(relative2)) {
47824
48097
  issues.push({
47825
48098
  path: `build.${key2}`,
47826
- message: `file "${file2}" does not exist in the repository`
48099
+ message: `file "${file3}" does not exist in the repository`
47827
48100
  });
47828
48101
  }
47829
48102
  }
@@ -47912,9 +48185,9 @@ function keyInputs(environment) {
47912
48185
  const inputs = [{ path: ENVIRONMENT_FILE, required: true }];
47913
48186
  const build = environment.build;
47914
48187
  for (const key2 of ["devcontainer", "dockerfile"]) {
47915
- const file2 = build?.[key2];
47916
- if (file2 === void 0) continue;
47917
- const relative2 = repoRelative(file2);
48188
+ const file3 = build?.[key2];
48189
+ if (file3 === void 0) continue;
48190
+ const relative2 = repoRelative(file3);
47918
48191
  if (relative2 === null) continue;
47919
48192
  inputs.push({ path: relative2, required: true });
47920
48193
  const dir = posix2.dirname(relative2);
@@ -48463,10 +48736,10 @@ var init_remote_url = __esm({
48463
48736
  // ../../packages/host-local/src/repos/host-repos.ts
48464
48737
  import { execFile as execFile10 } from "node:child_process";
48465
48738
  import { createHash as createHash3, randomBytes as randomBytes2 } from "node:crypto";
48466
- import { existsSync as existsSync9 } from "node:fs";
48739
+ import { existsSync as existsSync10 } from "node:fs";
48467
48740
  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
48741
  import { homedir as homedir13 } from "node:os";
48469
- import { dirname as dirname7, join as join27, sep as sep3 } from "node:path";
48742
+ import { dirname as dirname8, join as join27, sep as sep3 } from "node:path";
48470
48743
  function defaultReposDir() {
48471
48744
  return process.env.BAND_REPOS_DIR?.trim() || join27(homedir13(), "band", "repos");
48472
48745
  }
@@ -48517,10 +48790,10 @@ async function browseFolder(path2) {
48517
48790
  let isDir = d.isDirectory();
48518
48791
  if (d.isSymbolicLink()) isDir = await isDirectory(full);
48519
48792
  if (!isDir) continue;
48520
- entries.push({ name: d.name, path: full, isGit: existsSync9(join27(full, ".git")) });
48793
+ entries.push({ name: d.name, path: full, isGit: existsSync10(join27(full, ".git")) });
48521
48794
  }
48522
48795
  entries.sort((a, b2) => a.name.localeCompare(b2.name));
48523
- const parent = dirname7(target);
48796
+ const parent = dirname8(target);
48524
48797
  return {
48525
48798
  path: target,
48526
48799
  parent: parent === target ? null : parent,
@@ -48556,13 +48829,13 @@ var init_host_repos = __esm({
48556
48829
  return this.cache;
48557
48830
  }
48558
48831
  save(mappings) {
48559
- const file2 = this.options.mappingsFile();
48832
+ const file3 = this.options.mappingsFile();
48560
48833
  this.writing = this.writing.then(async () => {
48561
- await mkdir3(dirname7(file2), { recursive: true, mode: 448 });
48562
- const tmp = `${file2}.${randomBytes2(4).toString("hex")}.tmp`;
48834
+ await mkdir3(dirname8(file3), { recursive: true, mode: 448 });
48835
+ const tmp = `${file3}.${randomBytes2(4).toString("hex")}.tmp`;
48563
48836
  await writeFile2(tmp, `${JSON.stringify(mappings, null, 2)}
48564
48837
  `, { mode: 384 });
48565
- await rename2(tmp, file2);
48838
+ await rename2(tmp, file3);
48566
48839
  });
48567
48840
  return this.writing;
48568
48841
  }
@@ -48619,12 +48892,12 @@ var init_host_repos = __esm({
48619
48892
  async doEnsure(key2, url2, owner, name4) {
48620
48893
  const mappings = await this.load();
48621
48894
  const mapped = mappings[key2];
48622
- if (mapped && existsSync9(mapped.path) && await isDirectory(mapped.path)) {
48895
+ if (mapped && existsSync10(mapped.path) && await isDirectory(mapped.path)) {
48623
48896
  return { path: mapped.path, cloned: false };
48624
48897
  }
48625
48898
  const base = join27(this.options.reposDir(), owner, name4);
48626
48899
  let dest = base;
48627
- if (existsSync9(dest)) {
48900
+ if (existsSync10(dest)) {
48628
48901
  const origin = await git(["remote", "get-url", "origin"], dest);
48629
48902
  const there = origin ? parseRemoteUrl(origin)?.key : void 0;
48630
48903
  if (there === key2) {
@@ -48634,7 +48907,7 @@ var init_host_repos = __esm({
48634
48907
  const empty = (await readdir4(dest).catch(() => ["x"])).length === 0;
48635
48908
  if (!empty) dest = `${base}-${createHash3("sha256").update(key2).digest("hex").slice(0, 6)}`;
48636
48909
  }
48637
- await mkdir3(dirname7(dest), { recursive: true });
48910
+ await mkdir3(dirname8(dest), { recursive: true });
48638
48911
  await clone2(url2, dest);
48639
48912
  const canonical = await realpath2(dest);
48640
48913
  await this.map(url2, canonical);
@@ -48696,9 +48969,9 @@ function createIterator(options) {
48696
48969
  if (!rawFile || typeof lineNumber !== "number" || rawContent === void 0) {
48697
48970
  continue;
48698
48971
  }
48699
- const file2 = rawFile.startsWith("./") ? rawFile.slice(2) : rawFile;
48972
+ const file3 = rawFile.startsWith("./") ? rawFile.slice(2) : rawFile;
48700
48973
  const content = rawContent.endsWith("\n") ? rawContent.slice(0, -1) : rawContent;
48701
- matches2.push({ file: file2, line: lineNumber, content });
48974
+ matches2.push({ file: file3, line: lineNumber, content });
48702
48975
  }
48703
48976
  signal();
48704
48977
  });
@@ -48804,15 +49077,15 @@ async function loadRepoConfig(host, worktreePath, repoPath) {
48804
49077
  }
48805
49078
  async function loadEnvironment(host, worktreePath, repoPath) {
48806
49079
  for (const base of [worktreePath, repoPath]) {
48807
- const file2 = join28(base, ENVIRONMENT_FILE);
48808
- const text3 = await readConfig(host, file2);
49080
+ const file3 = join28(base, ENVIRONMENT_FILE);
49081
+ const text3 = await readConfig(host, file3);
48809
49082
  if (text3 === null) continue;
48810
49083
  const exists3 = (relative2) => host.fs.stat(join28(base, relative2)).then(
48811
49084
  () => true,
48812
49085
  () => false
48813
49086
  );
48814
49087
  const result = await validateEnvironment(text3, exists3);
48815
- return result.ok ? { source: file2, environment: result.environment, issues: [] } : { source: file2, environment: null, issues: result.issues };
49088
+ return result.ok ? { source: file3, environment: result.environment, issues: [] } : { source: file3, environment: null, issues: result.issues };
48816
49089
  }
48817
49090
  return { source: null, environment: null, issues: [] };
48818
49091
  }
@@ -48949,7 +49222,7 @@ var init_script_run = __esm({
48949
49222
  });
48950
49223
 
48951
49224
  // ../../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";
49225
+ import { dirname as dirname9, isAbsolute, join as join30, relative, resolve as resolve6, sep as sep4 } from "node:path";
48953
49226
  async function copyWorktreeFiles(host, repoPath, worktreePath) {
48954
49227
  const byAbs = /* @__PURE__ */ new Map();
48955
49228
  const missingFromConfig = [];
@@ -48992,7 +49265,7 @@ async function copyWorktreeFiles(host, repoPath, worktreePath) {
48992
49265
  );
48993
49266
  continue;
48994
49267
  }
48995
- await host.fs.mkdir(dirname8(dest), { recursive: true });
49268
+ await host.fs.mkdir(dirname9(dest), { recursive: true });
48996
49269
  await host.fs.copy(absSource, dest);
48997
49270
  log14.debug({ source, file: rel }, "copied worktree file");
48998
49271
  copied.push(rel);
@@ -49329,7 +49602,8 @@ var init_local_host = __esm({
49329
49602
  usageReader: async (agent) => getUsageReader(agent.agentType, { command: agent.command }),
49330
49603
  installSkills: (options) => installSkills(options),
49331
49604
  hooksStatus: () => checkHooks(),
49332
- installHooks: () => installHooks()
49605
+ installHooks: () => installHooks(),
49606
+ removeHooks: () => removeHooks()
49333
49607
  };
49334
49608
  get pty() {
49335
49609
  return this.options.terminalBackend();
@@ -49505,11 +49779,11 @@ var init_src5 = __esm({
49505
49779
  });
49506
49780
 
49507
49781
  // src/server/infra/db/queries/repos.ts
49508
- import { existsSync as existsSync10 } from "node:fs";
49782
+ import { existsSync as existsSync11 } from "node:fs";
49509
49783
  import { join as join32 } from "node:path";
49510
49784
  function reconcileKindForRepo(repo) {
49511
- if (!repo.path || !existsSync10(repo.path)) return false;
49512
- const detectedKind = existsSync10(join32(repo.path, ".git")) ? "git" : "plain";
49785
+ if (!repo.path || !existsSync11(repo.path)) return false;
49786
+ const detectedKind = existsSync11(join32(repo.path, ".git")) ? "git" : "plain";
49513
49787
  if (detectedKind === repo.kind) return false;
49514
49788
  repo.kind = detectedKind;
49515
49789
  if (detectedKind === "plain") {
@@ -49852,12 +50126,18 @@ var init_repos = __esm({
49852
50126
  * Records where a worktree was started from, or clears it with `null`. Focused UPDATE, because
49853
50127
  * `saveAll` keeps the stored origin and never writes the one in its snapshot.
49854
50128
  */
49855
- setOrigin(repoName, name4, origin) {
50129
+ setOrigin(repoName, name4, origin, hostId2) {
49856
50130
  getDb().update(worktrees).set({
49857
50131
  originWorktreeId: origin?.worktreeId ?? null,
49858
50132
  originChatId: origin?.chatId ?? null,
49859
50133
  originTerminalId: origin?.terminalId ?? null
49860
- }).where(and(eq(worktrees.repoName, repoName), eq(worktrees.name, name4))).run();
50134
+ }).where(
50135
+ and(
50136
+ eq(worktrees.repoName, repoName),
50137
+ eq(worktrees.name, name4),
50138
+ ...hostId2 === void 0 ? [] : [eq(worktrees.hostId, hostId2)]
50139
+ )
50140
+ ).run();
49861
50141
  }
49862
50142
  /**
49863
50143
  * Path and kind of one repo, by primary key. For per-request lookups
@@ -50053,16 +50333,17 @@ var init_worktree_statuses = __esm({
50053
50333
  db.delete(worktreeStatuses).where(eq(worktreeStatuses.worktreeId, worktreeId4)).run();
50054
50334
  }
50055
50335
  /**
50056
- * Reset every "working" / "needs_attention" row back to "waiting" and
50336
+ * Reset every row that is not "done" or "failed" back to "idle" and
50057
50337
  * return the count of affected rows. Used at server startup to clean
50058
50338
  * up stale state from the previous run.
50059
50339
  */
50060
- resetActiveToWaiting(now) {
50340
+ resetActiveToIdle(now) {
50061
50341
  const db = getDb();
50062
- const result = db.update(worktreeStatuses).set({ agentStatus: "waiting", updatedAt: now }).where(
50342
+ const result = db.update(worktreeStatuses).set({ agentStatus: "idle", updatedAt: now }).where(
50063
50343
  or(
50064
50344
  eq(worktreeStatuses.agentStatus, "working"),
50065
- eq(worktreeStatuses.agentStatus, "needs_attention")
50345
+ eq(worktreeStatuses.agentStatus, "monitoring"),
50346
+ eq(worktreeStatuses.agentStatus, "question")
50066
50347
  )
50067
50348
  ).run();
50068
50349
  return Number(result.changes);
@@ -50108,22 +50389,22 @@ var init_worktree_statuses = __esm({
50108
50389
  const db = getDb();
50109
50390
  db.delete(worktreeStatusSources).where(eq(worktreeStatusSources.worktreeId, worktreeId4)).run();
50110
50391
  }
50111
- /** Delete every source. Used at server startup: no agent state survives it. */
50112
- removeAll() {
50113
- getDb().delete(worktreeStatusSources).run();
50114
- }
50115
50392
  /**
50116
- * Set the worktree's `needs_attention` sources to `waiting`, except the
50117
- * ones in `keep`.
50393
+ * Delete every source that is not `done` or `failed`. Used at server
50394
+ * startup: no running agent survives it, but a finished one stays.
50118
50395
  */
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();
50396
+ removeActive() {
50397
+ getDb().delete(worktreeStatusSources).where(notInArray(worktreeStatusSources.status, ["done", "failed"])).run();
50398
+ }
50399
+ /** Set the worktree's `done` and `failed` sources to `idle`. Returns how many changed. */
50400
+ acknowledge(worktreeId4, now) {
50401
+ const result = getDb().update(worktreeStatusSources).set({ status: "idle", updatedAt: now }).where(
50402
+ and(
50403
+ eq(worktreeStatusSources.worktreeId, worktreeId4),
50404
+ inArray(worktreeStatusSources.status, ["done", "failed"])
50405
+ )
50406
+ ).run();
50407
+ return Number(result.changes);
50127
50408
  }
50128
50409
  };
50129
50410
  }
@@ -50183,8 +50464,8 @@ var init_isolation = __esm({
50183
50464
  });
50184
50465
 
50185
50466
  // 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";
50467
+ import { existsSync as existsSync12 } from "node:fs";
50468
+ import { dirname as dirname10, isAbsolute as isAbsolute2, join as join33, resolve as resolve7 } from "node:path";
50188
50469
  function parseRunners(raw) {
50189
50470
  if (raw === void 0 || raw === null) return { runners: [], errors: [] };
50190
50471
  if (!Array.isArray(raw)) return { runners: [], errors: ["runners must be a list"] };
@@ -50210,8 +50491,8 @@ function bundledRunnersDir() {
50210
50491
  let dir = import.meta.dirname;
50211
50492
  for (let i = 0; i < 10; i++) {
50212
50493
  const candidate = join33(dir, "runners");
50213
- if (existsSync11(join33(candidate, "local", "spawn.sh"))) return candidate;
50214
- const parent = dirname9(dir);
50494
+ if (existsSync12(join33(candidate, "local", "spawn.sh"))) return candidate;
50495
+ const parent = dirname10(dir);
50215
50496
  if (parent === dir) break;
50216
50497
  dir = parent;
50217
50498
  }
@@ -50691,16 +50972,20 @@ function upsertWorktreeStatus(worktreeId4, agent) {
50691
50972
  tabStatuses: listTabStatuses(worktreeId4)
50692
50973
  };
50693
50974
  }
50694
- function resolveWorktreeIdByCwd(cwd) {
50975
+ function resolveWorktreeIdByCwd(cwd, hostId2) {
50695
50976
  const state2 = loadState();
50977
+ let other = null;
50696
50978
  for (const proj of state2.repos) {
50697
50979
  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
- }
50980
+ const wtHost = wt.hostId ?? LOCAL_HOST_ID3;
50981
+ if (hostId2 !== void 0 && wtHost !== hostId2) continue;
50982
+ if (cwd !== wt.path && !cwd.startsWith(`${wt.path}/`)) continue;
50983
+ const id = toWorktreeId(proj.name, wt.name, wt.hostId);
50984
+ if (wtHost === LOCAL_HOST_ID3) return id;
50985
+ other ??= id;
50701
50986
  }
50702
50987
  }
50703
- return null;
50988
+ return other;
50704
50989
  }
50705
50990
  function chatStatusSource(chatId) {
50706
50991
  return `${CHAT_SOURCE_PREFIX}${chatId}`;
@@ -50718,20 +51003,21 @@ function deriveWorktreeStatus(worktreeId4) {
50718
51003
  best = status;
50719
51004
  }
50720
51005
  }
50721
- return best ?? "waiting";
51006
+ return best ?? "idle";
50722
51007
  }
50723
51008
  function toTabStatuses(sources) {
50724
51009
  const staleBefore = Date.now() - STALE_HOOK_WORKING_MS;
50725
51010
  const byTab = /* @__PURE__ */ new Map();
50726
51011
  for (const source of sources) {
50727
51012
  const { status } = source;
50728
- if (status !== "working" && status !== "needs_attention") continue;
51013
+ if (status === "idle" || !(status in STATUS_PRIORITY)) continue;
50729
51014
  if (isStaleSource(source, staleBefore)) continue;
51015
+ const tabStatus = status;
50730
51016
  let tab;
50731
51017
  if (source.sourceId.startsWith(CHAT_SOURCE_PREFIX)) {
50732
- tab = { chatId: source.sourceId.slice(CHAT_SOURCE_PREFIX.length), status };
51018
+ tab = { chatId: source.sourceId.slice(CHAT_SOURCE_PREFIX.length), status: tabStatus };
50733
51019
  } else if (source.terminalId) {
50734
- tab = { terminalId: source.terminalId, status };
51020
+ tab = { terminalId: source.terminalId, status: tabStatus };
50735
51021
  } else {
50736
51022
  continue;
50737
51023
  }
@@ -50765,10 +51051,10 @@ function removeWorktreeSource(worktreeId4, sourceId) {
50765
51051
  if (!statusSourceQueries.remove(worktreeId4, sourceId)) return null;
50766
51052
  return rederiveWorktreeStatus(worktreeId4);
50767
51053
  }
50768
- function acknowledgeWorktreeAttention(worktreeId4, pendingChatIds) {
51054
+ function acknowledgeWorktreeAttention(worktreeId4) {
50769
51055
  const existing = getWorktreeStatus(worktreeId4);
50770
- if (existing?.agent?.status !== "needs_attention") return existing;
50771
- statusSourceQueries.acknowledge(worktreeId4, pendingChatIds.map(chatStatusSource), Date.now());
51056
+ if (!existing) return null;
51057
+ if (statusSourceQueries.acknowledge(worktreeId4, Date.now()) === 0) return existing;
50772
51058
  return upsertWorktreeStatus(worktreeId4, { status: deriveWorktreeStatus(worktreeId4) });
50773
51059
  }
50774
51060
  function startStatusSourceCleanup() {
@@ -50786,17 +51072,33 @@ function startStatusSourceCleanup() {
50786
51072
  for (const status of changed) emit({ kind: "update", status });
50787
51073
  });
50788
51074
  }
51075
+ function subagentHold(worktreeId4, sourceId) {
51076
+ const key2 = `${worktreeId4}\0${sourceId}`;
51077
+ let hold = subagentHolds.get(key2);
51078
+ if (!hold) {
51079
+ hold = { running: /* @__PURE__ */ new Set(), deferred: null };
51080
+ subagentHolds.set(key2, hold);
51081
+ }
51082
+ return hold;
51083
+ }
51084
+ function dropSubagentHold(worktreeId4, sourceId) {
51085
+ subagentHolds.delete(`${worktreeId4}\0${sourceId}`);
51086
+ }
50789
51087
  async function applyHookNotification(notification) {
50790
51088
  const { payload } = notification;
50791
51089
  if (notification.dispatch === "chat") return null;
50792
- const worktreeId4 = resolveWorktreeIdByCwd(notification.cwd);
51090
+ const worktreeId4 = resolveWorktreeIdByCwd(notification.cwd, notification.hostId ?? LOCAL_HOST_ID3);
50793
51091
  if (!worktreeId4) return null;
50794
51092
  const agentType = notification.agent || detectHookAgentType(payload) || settingsService.getAgentDefinition(getWorktreeStatus(worktreeId4)?.agent?.codingAgentId).type;
50795
51093
  const sourceId = `hook:${hookSessionId(payload) ?? agentType}`;
50796
51094
  if (isHookSessionEnd(agentType, payload)) {
51095
+ dropSubagentHold(worktreeId4, sourceId);
50797
51096
  return removeWorktreeSource(worktreeId4, sourceId);
50798
51097
  }
50799
- const status = await mapHookPayloadToStatus(agentType, payload);
51098
+ let status = await mapHookPayloadToStatus(agentType, payload);
51099
+ if (agentType === "claude-code") {
51100
+ status = foldSubagents(subagentHold(worktreeId4, sourceId), payload, status);
51101
+ }
50800
51102
  return setWorktreeSourceStatus(worktreeId4, sourceId, {
50801
51103
  status,
50802
51104
  lastActivity: (/* @__PURE__ */ new Date()).toISOString(),
@@ -50804,8 +51106,9 @@ async function applyHookNotification(notification) {
50804
51106
  });
50805
51107
  }
50806
51108
  function resetAgentStatuses() {
50807
- statusSourceQueries.removeAll();
50808
- return worktreeStatusQueries.resetActiveToWaiting(Date.now());
51109
+ subagentHolds.clear();
51110
+ statusSourceQueries.removeActive();
51111
+ return worktreeStatusQueries.resetActiveToIdle(Date.now());
50809
51112
  }
50810
51113
  function resolveWorktreeIdentity(worktreeId4) {
50811
51114
  return worktreeQueriesForIdentity.findIdentity(worktreeId4);
@@ -50814,7 +51117,7 @@ function deleteWorktreeStatus(worktreeId4) {
50814
51117
  worktreeStatusQueries.remove(worktreeId4);
50815
51118
  statusSourceQueries.removeForWorktree(worktreeId4);
50816
51119
  }
50817
- var worktreeStatusQueries, statusSourceQueries, worktreeQueriesForIdentity, repoQueries, STALE_HOOK_WORKING_MS, STATUS_PRIORITY, CHAT_SOURCE_PREFIX, MANUAL_STATUS_SOURCE;
51120
+ var worktreeStatusQueries, statusSourceQueries, worktreeQueriesForIdentity, repoQueries, STALE_HOOK_WORKING_MS, LOCAL_HOST_ID3, STATUS_PRIORITY, CHAT_SOURCE_PREFIX, MANUAL_STATUS_SOURCE, subagentHolds;
50818
51121
  var init_state = __esm({
50819
51122
  "src/server/services/state.ts"() {
50820
51123
  "use strict";
@@ -50831,9 +51134,18 @@ var init_state = __esm({
50831
51134
  worktreeQueriesForIdentity = new WorktreeQueries();
50832
51135
  repoQueries = new RepoQueries();
50833
51136
  STALE_HOOK_WORKING_MS = 15 * 6e4;
50834
- STATUS_PRIORITY = { needs_attention: 3, working: 2, waiting: 1 };
51137
+ LOCAL_HOST_ID3 = "local";
51138
+ STATUS_PRIORITY = {
51139
+ question: 6,
51140
+ failed: 5,
51141
+ working: 4,
51142
+ monitoring: 3,
51143
+ done: 2,
51144
+ idle: 1
51145
+ };
50835
51146
  CHAT_SOURCE_PREFIX = "chat:";
50836
51147
  MANUAL_STATUS_SOURCE = "manual";
51148
+ subagentHolds = /* @__PURE__ */ new Map();
50837
51149
  }
50838
51150
  });
50839
51151
 
@@ -51733,8 +52045,8 @@ var init_map_limited = __esm({
51733
52045
 
51734
52046
  // src/server/infra/subscriptions/github.ts
51735
52047
  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";
52048
+ import { chmodSync as chmodSync2, existsSync as existsSync13, mkdirSync as mkdirSync7, readFileSync as readFileSync8, writeFileSync as writeFileSync5 } from "node:fs";
52049
+ import { dirname as dirname11, join as join36 } from "node:path";
51738
52050
  function secretPath() {
51739
52051
  return join36(bandHome(), "github-webhook-secret");
51740
52052
  }
@@ -51752,7 +52064,7 @@ function ensureWebhookSecret() {
51752
52064
  const existing = readWebhookSecret();
51753
52065
  if (existing) return existing;
51754
52066
  const path2 = secretPath();
51755
- if (!existsSync12(dirname10(path2))) mkdirSync6(dirname10(path2), { recursive: true });
52067
+ if (!existsSync13(dirname11(path2))) mkdirSync7(dirname11(path2), { recursive: true });
51756
52068
  const secret = randomBytes3(32).toString("hex");
51757
52069
  writeFileSync5(path2, `${secret}
51758
52070
  `, { mode: 384 });
@@ -52485,7 +52797,7 @@ var init_oauth_client = __esm({
52485
52797
 
52486
52798
  // src/server/services/_utils/vault-crypto.ts
52487
52799
  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";
52800
+ import { chmodSync as chmodSync3, existsSync as existsSync14, readFileSync as readFileSync9, renameSync as renameSync7, rmSync as rmSync3, writeFileSync as writeFileSync6 } from "node:fs";
52489
52801
  import { join as join37 } from "node:path";
52490
52802
  function vaultKeyFile() {
52491
52803
  return join37(bandHome(), "vault.key");
@@ -52507,21 +52819,21 @@ function generateKey() {
52507
52819
  function loadKey() {
52508
52820
  const fromEnv = process.env.BAND_VAULT_KEY?.trim();
52509
52821
  if (fromEnv) return parseKey(fromEnv);
52510
- const file2 = vaultKeyFile();
52511
- if (existsSync13(file2)) return parseKey(readFileSync9(file2, "utf8"));
52822
+ const file3 = vaultKeyFile();
52823
+ if (existsSync14(file3)) return parseKey(readFileSync9(file3, "utf8"));
52512
52824
  const key2 = generateKey();
52513
- writeFileSync6(file2, `${key2.toString("base64")}
52825
+ writeFileSync6(file3, `${key2.toString("base64")}
52514
52826
  `, { mode: 384, flag: "wx" });
52515
- chmodSync3(file2, 384);
52827
+ chmodSync3(file3, 384);
52516
52828
  return key2;
52517
52829
  }
52518
52830
  function stageKeyFile(key2) {
52519
- const file2 = vaultKeyFile();
52520
- const temp = `${file2}.${process.pid}.next`;
52831
+ const file3 = vaultKeyFile();
52832
+ const temp = `${file3}.${process.pid}.next`;
52521
52833
  writeFileSync6(temp, `${key2.toString("base64")}
52522
52834
  `, { mode: 384 });
52523
52835
  chmodSync3(temp, 384);
52524
- return { commit: () => renameSync5(temp, file2), discard: () => rmSync2(temp, { force: true }) };
52836
+ return { commit: () => renameSync7(temp, file3), discard: () => rmSync3(temp, { force: true }) };
52525
52837
  }
52526
52838
  function encrypt(key2, aad, plaintext) {
52527
52839
  const nonce = randomBytes5(NONCE_BYTES);
@@ -53736,9 +54048,15 @@ var init_mime_types = __esm({
53736
54048
  ".txt": "text/plain",
53737
54049
  ".md": "text/markdown",
53738
54050
  ".csv": "text/csv",
53739
- ".html": "text/html",
53740
- ".css": "text/css",
53741
- ".js": "application/javascript",
54051
+ ".html": "text/html; charset=utf-8",
54052
+ ".htm": "text/html; charset=utf-8",
54053
+ ".css": "text/css; charset=utf-8",
54054
+ ".js": "text/javascript; charset=utf-8",
54055
+ ".mjs": "text/javascript; charset=utf-8",
54056
+ ".woff": "font/woff",
54057
+ ".woff2": "font/woff2",
54058
+ ".ttf": "font/ttf",
54059
+ ".otf": "font/otf",
53742
54060
  ".ts": "application/typescript",
53743
54061
  ".xml": "application/xml",
53744
54062
  ".yaml": "application/x-yaml",
@@ -53765,6 +54083,132 @@ var init_turn_events = __esm({
53765
54083
  }
53766
54084
  });
53767
54085
 
54086
+ // src/server/services/chat-status-service.ts
54087
+ function quietMs() {
54088
+ const override = Number(process.env.BAND_AGENT_QUIET_MS);
54089
+ return Number.isFinite(override) && override > 0 ? override : QUIET_MS;
54090
+ }
54091
+ function setChatStatus(worktreeId4, chatId, status) {
54092
+ if (status === "monitoring") monitoringChats.add(chatId);
54093
+ else monitoringChats.delete(chatId);
54094
+ const updated = setWorktreeSourceStatus(worktreeId4, chatStatusSource(chatId), { status });
54095
+ emit({ kind: "update", status: updated });
54096
+ }
54097
+ function restingStatus(chatId) {
54098
+ if (agentSessionService.hasPendingRequest(chatId)) return "question";
54099
+ if (agentSessionService.nextWorkDeadline(chatId) !== null) return "monitoring";
54100
+ return "done";
54101
+ }
54102
+ function beginBandTurn(worktreeId4, chatId) {
54103
+ endAutonomous(chatId);
54104
+ clearMonitorTimer(chatId);
54105
+ setChatStatus(worktreeId4, chatId, "working");
54106
+ }
54107
+ function endBandTurn(worktreeId4, chatId, result) {
54108
+ const status = result === "done" ? restingStatus(chatId) : result;
54109
+ setChatStatus(worktreeId4, chatId, status);
54110
+ if (status === "monitoring") watchMonitoring(worktreeId4, chatId);
54111
+ }
54112
+ function endAutonomous(chatId) {
54113
+ const timer = autonomous.get(chatId);
54114
+ if (timer) clearTimeout(timer);
54115
+ autonomous.delete(chatId);
54116
+ }
54117
+ function clearMonitorTimer(chatId) {
54118
+ const timer = monitorTimers.get(chatId);
54119
+ if (timer) clearTimeout(timer);
54120
+ monitorTimers.delete(chatId);
54121
+ }
54122
+ function watchMonitoring(worktreeId4, chatId) {
54123
+ clearMonitorTimer(chatId);
54124
+ const deadline = agentSessionService.nextWorkDeadline(chatId);
54125
+ if (deadline === null) return;
54126
+ const timer = setTimeout(
54127
+ () => {
54128
+ monitorTimers.delete(chatId);
54129
+ if (runningTurn(chatId) || autonomous.has(chatId)) return;
54130
+ reconsiderMonitoring(worktreeId4, chatId);
54131
+ },
54132
+ Math.min(Math.max(deadline - Date.now(), 0) + 100, MAX_TIMER_MS)
54133
+ );
54134
+ timer.unref?.();
54135
+ monitorTimers.set(chatId, timer);
54136
+ }
54137
+ function reconsiderMonitoring(worktreeId4, chatId) {
54138
+ if (!chatService.get(chatId) || !monitoringChats.has(chatId)) return;
54139
+ const status = restingStatus(chatId);
54140
+ if (status === "monitoring") {
54141
+ watchMonitoring(worktreeId4, chatId);
54142
+ return;
54143
+ }
54144
+ setChatStatus(worktreeId4, chatId, status);
54145
+ }
54146
+ function settleAutonomous(worktreeId4, chatId) {
54147
+ autonomous.delete(chatId);
54148
+ if (!chatService.get(chatId) || runningTurn(chatId)) return;
54149
+ const status = restingStatus(chatId);
54150
+ setChatStatus(worktreeId4, chatId, status);
54151
+ if (status === "monitoring") watchMonitoring(worktreeId4, chatId);
54152
+ }
54153
+ function armQuietTimer(worktreeId4, chatId) {
54154
+ const previous = autonomous.get(chatId);
54155
+ if (previous) clearTimeout(previous);
54156
+ const timer = setTimeout(() => settleAutonomous(worktreeId4, chatId), quietMs());
54157
+ timer.unref?.();
54158
+ autonomous.set(chatId, timer);
54159
+ }
54160
+ function observeAgentStatus(hasRunningTurn) {
54161
+ if (observing) return;
54162
+ observing = true;
54163
+ runningTurn = hasRunningTurn;
54164
+ agentSessionService.observePending((chatId, worktreeId4) => {
54165
+ const waiting = agentSessionService.hasPendingRequest(chatId);
54166
+ if (runningTurn(chatId)) {
54167
+ setChatStatus(worktreeId4, chatId, waiting ? "question" : "working");
54168
+ return;
54169
+ }
54170
+ if (waiting) {
54171
+ const timer = autonomous.get(chatId);
54172
+ if (timer) clearTimeout(timer);
54173
+ autonomous.set(chatId, null);
54174
+ setChatStatus(worktreeId4, chatId, "question");
54175
+ } else if (autonomous.has(chatId)) {
54176
+ setChatStatus(worktreeId4, chatId, "working");
54177
+ armQuietTimer(worktreeId4, chatId);
54178
+ }
54179
+ });
54180
+ agentSessionService.observeActivity((chatId, worktreeId4, kind) => {
54181
+ if (runningTurn(chatId) || !chatService.get(chatId)) return;
54182
+ if (kind === "update") {
54183
+ if (agentSessionService.hasPendingRequest(chatId)) return;
54184
+ const wasWorking = autonomous.has(chatId);
54185
+ armQuietTimer(worktreeId4, chatId);
54186
+ clearMonitorTimer(chatId);
54187
+ if (!wasWorking) setChatStatus(worktreeId4, chatId, "working");
54188
+ return;
54189
+ }
54190
+ if (autonomous.has(chatId)) return;
54191
+ reconsiderMonitoring(worktreeId4, chatId);
54192
+ });
54193
+ }
54194
+ var QUIET_MS, MAX_TIMER_MS, autonomous, monitorTimers, monitoringChats, runningTurn, observing;
54195
+ var init_chat_status_service = __esm({
54196
+ "src/server/services/chat-status-service.ts"() {
54197
+ "use strict";
54198
+ init_agent_session_service();
54199
+ init_chat_service();
54200
+ init_state();
54201
+ init_watcher_service();
54202
+ QUIET_MS = 1e4;
54203
+ MAX_TIMER_MS = 2 ** 31 - 1;
54204
+ autonomous = /* @__PURE__ */ new Map();
54205
+ monitorTimers = /* @__PURE__ */ new Map();
54206
+ monitoringChats = /* @__PURE__ */ new Set();
54207
+ runningTurn = () => false;
54208
+ observing = false;
54209
+ }
54210
+ });
54211
+
53768
54212
  // ../../packages/host-local/src/git/git-refusals.ts
53769
54213
  async function pullRefusal(err, cwd, execGit2) {
53770
54214
  const stderr = errorText(err);
@@ -54212,7 +54656,7 @@ var init_worktree_checkpoints = __esm({
54212
54656
  });
54213
54657
 
54214
54658
  // src/server/services/_utils/checkpoint-store.ts
54215
- import { rmSync as rmSync3 } from "node:fs";
54659
+ import { rmSync as rmSync4 } from "node:fs";
54216
54660
  import { join as join38 } from "node:path";
54217
54661
  function worktreeCheckpointDir(worktreeId4) {
54218
54662
  return join38(bandHome(), "checkpoints", worktreeId4);
@@ -54222,7 +54666,7 @@ function checkpointDir(worktreeId4, checkpointId2) {
54222
54666
  }
54223
54667
  function forgetCheckpoints(worktreeId4) {
54224
54668
  queries2.deleteForWorktree(worktreeId4);
54225
- rmSync3(worktreeCheckpointDir(worktreeId4), { recursive: true, force: true });
54669
+ rmSync4(worktreeCheckpointDir(worktreeId4), { recursive: true, force: true });
54226
54670
  }
54227
54671
  var queries2;
54228
54672
  var init_checkpoint_store = __esm({
@@ -54342,13 +54786,13 @@ var init_remote_worktrees = __esm({
54342
54786
  // ../../packages/shared/src/repo-hosts.ts
54343
54787
  function isHostAllowed(allowed2, hostId2) {
54344
54788
  if (!allowed2) return true;
54345
- return allowed2.includes(hostId2 || LOCAL_HOST_ID3);
54789
+ return allowed2.includes(hostId2 || LOCAL_HOST_ID4);
54346
54790
  }
54347
54791
  function effectivePrimaryHost(primaryHostId, allowed2, hostsWithDefaultCheckout) {
54348
54792
  if (primaryHostId && isHostAllowed(allowed2, primaryHostId)) return primaryHostId;
54349
54793
  const candidates = hostsWithDefaultCheckout.filter((id) => isHostAllowed(allowed2, id));
54350
54794
  if (candidates.length === 0) return null;
54351
- if (candidates.includes(LOCAL_HOST_ID3)) return LOCAL_HOST_ID3;
54795
+ if (candidates.includes(LOCAL_HOST_ID4)) return LOCAL_HOST_ID4;
54352
54796
  return [...candidates].sort()[0] ?? null;
54353
54797
  }
54354
54798
  function onlyAvailableOnMessage(repo, hostNames) {
@@ -54358,10 +54802,10 @@ function checkpointsEnabledFor(stored, allowed2) {
54358
54802
  if (stored != null) return stored;
54359
54803
  return !allowed2;
54360
54804
  }
54361
- var LOCAL_HOST_ID3;
54805
+ var LOCAL_HOST_ID4;
54362
54806
  var init_repo_hosts = __esm({
54363
54807
  "../../packages/shared/src/repo-hosts.ts"() {
54364
- LOCAL_HOST_ID3 = "local";
54808
+ LOCAL_HOST_ID4 = "local";
54365
54809
  }
54366
54810
  });
54367
54811
 
@@ -56246,6 +56690,7 @@ var init_rpc = __esm({
56246
56690
  "scripts.copyFiles": 2 * 6e4,
56247
56691
  "agentEnv.installSkills": 2 * 6e4,
56248
56692
  "agentEnv.installHooks": 6e4,
56693
+ "agentEnv.removeHooks": 6e4,
56249
56694
  exec: 10 * 6e4
56250
56695
  };
56251
56696
  RemoteRpc = class {
@@ -56519,7 +56964,8 @@ var init_remote_host = __esm({
56519
56964
  // The worker takes no `home` override. It is for tests of the local install.
56520
56965
  installSkills: () => this.rpc.call("agentEnv.installSkills"),
56521
56966
  hooksStatus: () => this.rpc.call("agentEnv.hooksStatus"),
56522
- installHooks: () => this.rpc.call("agentEnv.installHooks")
56967
+ installHooks: () => this.rpc.call("agentEnv.installHooks"),
56968
+ removeHooks: () => this.rpc.call("agentEnv.removeHooks")
56523
56969
  };
56524
56970
  // ---- fs -----------------------------------------------------------------
56525
56971
  async writeFile(path2, data, options) {
@@ -56931,7 +57377,7 @@ function stringRecord(value) {
56931
57377
  function stringList2(value) {
56932
57378
  return Array.isArray(value) ? value.filter((v2) => typeof v2 === "string") : [];
56933
57379
  }
56934
- var log20, repoQueries4, LOCAL_HOST_ID4, DEFAULT_TIMEOUT_MS, DEFAULT_LEASE_MS, MAX_LEASE_MS, SWEEP_MS, leaseFilterInput, HostRequestError, PlacementService, placementService;
57380
+ var log20, repoQueries4, LOCAL_HOST_ID5, DEFAULT_TIMEOUT_MS, DEFAULT_LEASE_MS, MAX_LEASE_MS, SWEEP_MS, leaseFilterInput, HostRequestError, PlacementService, placementService;
56935
57381
  var init_placement_service = __esm({
56936
57382
  "src/server/services/placement-service.ts"() {
56937
57383
  "use strict";
@@ -56955,7 +57401,7 @@ var init_placement_service = __esm({
56955
57401
  init_worktree_service();
56956
57402
  log20 = createLogger("placement");
56957
57403
  repoQueries4 = new RepoQueries();
56958
- LOCAL_HOST_ID4 = "local";
57404
+ LOCAL_HOST_ID5 = "local";
56959
57405
  DEFAULT_TIMEOUT_MS = 10 * 6e4;
56960
57406
  DEFAULT_LEASE_MS = 6e4;
56961
57407
  MAX_LEASE_MS = 15 * 6e4;
@@ -57003,7 +57449,7 @@ var init_placement_service = __esm({
57003
57449
  for (const row of tokenService.listHosts(1e3)) {
57004
57450
  if (row.status !== "online" || !row.usable) continue;
57005
57451
  if (row.info?.mode === "ephemeral") continue;
57006
- const info = row.id === LOCAL_HOST_ID4 ? await hostRegistry.local.info().catch(() => null) : null;
57452
+ const info = row.id === LOCAL_HOST_ID5 ? await hostRegistry.local.info().catch(() => null) : null;
57007
57453
  const stored = row.info ?? {};
57008
57454
  const versions = stringRecord(info?.versions ?? stored.versions);
57009
57455
  const tools = stringRecord(info?.tools ?? stored.tools);
@@ -57035,7 +57481,7 @@ var init_placement_service = __esm({
57035
57481
  const fits = (await this.candidates()).filter(
57036
57482
  (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
57483
  // 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)
57484
+ (agent === void 0 || c.id === LOCAL_HOST_ID5 || c.agents.includes(agent)) && matches(c, placement)
57039
57485
  );
57040
57486
  if (fits.length === 0) return null;
57041
57487
  const load = this.queries.worktreeCounts();
@@ -57824,7 +58270,7 @@ ${this.text.slice(-MAX_LOG_BYTES)}`;
57824
58270
  const log77 = new BuildLog(row.id, (id, text3) => this.queries.setLog(id, text3));
57825
58271
  let tmp = null;
57826
58272
  const container = `band-env-${row.id.slice(0, 12)}`;
57827
- let failure = null;
58273
+ let failure2 = null;
57828
58274
  try {
57829
58275
  log77.add(`Building ${plan.tag} for ${plan.repo} at ${plan.ref} (${plan.commit.slice(0, 12)})`);
57830
58276
  log77.flush();
@@ -57836,8 +58282,8 @@ ${this.text.slice(-MAX_LOG_BYTES)}`;
57836
58282
  if (plan.registry) await this.push(plan, log77);
57837
58283
  log77.add(`Done: ${plan.tag}`);
57838
58284
  } catch (err) {
57839
- failure = err.message;
57840
- log77.add(`FAILED: ${failure}`);
58285
+ failure2 = err.message;
58286
+ log77.add(`FAILED: ${failure2}`);
57841
58287
  }
57842
58288
  await this.exec(plan.host, "docker", ["rm", "--force", container], log77, { quiet: true }).catch(
57843
58289
  () => void 0
@@ -57851,7 +58297,7 @@ ${this.text.slice(-MAX_LOG_BYTES)}`;
57851
58297
  ).catch(() => void 0);
57852
58298
  if (tmp) await plan.host.fs.rm(tmp, { recursive: true, force: true }).catch(() => void 0);
57853
58299
  try {
57854
- if (failure === null) {
58300
+ if (failure2 === null) {
57855
58301
  this.queries.finish(row.id, {
57856
58302
  status: "ready",
57857
58303
  image: plan.tag,
@@ -57861,7 +58307,7 @@ ${this.text.slice(-MAX_LOG_BYTES)}`;
57861
58307
  } else {
57862
58308
  this.queries.finish(row.id, {
57863
58309
  status: "failed",
57864
- error: failure.slice(0, 2e3),
58310
+ error: failure2.slice(0, 2e3),
57865
58311
  log: log77.toString(),
57866
58312
  at: Date.now()
57867
58313
  });
@@ -58204,8 +58650,8 @@ var init_git_credential_service = __esm({
58204
58650
  // src/server/services/runner-service.ts
58205
58651
  import { spawn as spawn6 } from "node:child_process";
58206
58652
  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";
58653
+ import { appendFileSync, mkdirSync as mkdirSync8, readFileSync as readFileSync10, rmSync as rmSync5, statSync as statSync13 } from "node:fs";
58654
+ import { dirname as dirname12, join as join39 } from "node:path";
58209
58655
  function snapshotSweepMs() {
58210
58656
  const raw = Number(process.env.BAND_SNAPSHOT_SWEEP_MS);
58211
58657
  return Number.isFinite(raw) && raw > 0 ? raw : SNAPSHOT_SWEEP_MS;
@@ -58294,9 +58740,9 @@ var init_runner_service = __esm({
58294
58740
  "SHELL"
58295
58741
  ];
58296
58742
  RunLog = class {
58297
- constructor(file2) {
58298
- this.file = file2;
58299
- mkdirSync7(dirname11(file2), { recursive: true, mode: 448 });
58743
+ constructor(file3) {
58744
+ this.file = file3;
58745
+ mkdirSync8(dirname12(file3), { recursive: true, mode: 448 });
58300
58746
  }
58301
58747
  lines = [];
58302
58748
  bytes = 0;
@@ -58721,7 +59167,7 @@ ${tail}` : head;
58721
59167
  () => false
58722
59168
  );
58723
59169
  if (exists3) return;
58724
- await host.git.exec(["clone", "--quiet", "--", url2, dest], dirname11(dest));
59170
+ await host.git.exec(["clone", "--quiet", "--", url2, dest], dirname12(dest));
58725
59171
  }
58726
59172
  /** Records the machine of an attempt. An older machine of the same worker id is gone by now, so it is retired. */
58727
59173
  recordMachine(runner, row, workerId) {
@@ -58796,7 +59242,7 @@ ${tail}` : head;
58796
59242
  } else if (!runner.destroy) {
58797
59243
  outcome = { state: "lost", error: `runner "${runner.id}" has no destroy hook` };
58798
59244
  } else {
58799
- let failure = null;
59245
+ let failure2 = null;
58800
59246
  try {
58801
59247
  const code = await this.runDestroy(
58802
59248
  runner,
@@ -58805,17 +59251,17 @@ ${tail}` : head;
58805
59251
  machine.handle,
58806
59252
  runLog
58807
59253
  );
58808
- if (code !== 0) failure = `destroy exited with code ${code}`;
59254
+ if (code !== 0) failure2 = `destroy exited with code ${code}`;
58809
59255
  } catch (err) {
58810
- failure = `destroy failed: ${err instanceof Error ? err.message : err}`;
59256
+ failure2 = `destroy failed: ${err instanceof Error ? err.message : err}`;
58811
59257
  }
58812
- if (failure === null) {
59258
+ if (failure2 === null) {
58813
59259
  outcome = { state: "destroyed", destroyedAt: Date.now(), error: reason };
58814
59260
  } else {
58815
59261
  const attempts = machine.destroyAttempts + 1;
58816
- outcome = attempts >= DESTROY_ATTEMPTS ? { state: "lost", destroyAttempts: attempts, error: failure } : {
59262
+ outcome = attempts >= DESTROY_ATTEMPTS ? { state: "lost", destroyAttempts: attempts, error: failure2 } : {
58817
59263
  destroyAttempts: attempts,
58818
- error: `${failure} (try ${attempts} of ${DESTROY_ATTEMPTS})`
59264
+ error: `${failure2} (try ${attempts} of ${DESTROY_ATTEMPTS})`
58819
59265
  };
58820
59266
  }
58821
59267
  }
@@ -58829,12 +59275,12 @@ ${tail}` : head;
58829
59275
  /** Runs `destroy` for a machine the hub has no record of, named only by the handle `status` printed. */
58830
59276
  async destroyHandle(runner, handle, reason) {
58831
59277
  if (!runner.destroy) return false;
58832
- const file2 = join39(bandHome(), "runners", "logs", "reaper.log");
59278
+ const file3 = join39(bandHome(), "runners", "logs", "reaper.log");
58833
59279
  try {
58834
- if (statSync11(file2).size > LOG_FILE_LIMIT) rmSync4(file2, { force: true });
59280
+ if (statSync13(file3).size > LOG_FILE_LIMIT) rmSync5(file3, { force: true });
58835
59281
  } catch {
58836
59282
  }
58837
- const runLog = new RunLog(file2);
59283
+ const runLog = new RunLog(file3);
58838
59284
  runLog.write("hub", `destroying machine ${handle} of runner ${runner.id}: ${reason}`);
58839
59285
  try {
58840
59286
  const code = await this.runDestroy(runner, null, null, handle, runLog);
@@ -59078,7 +59524,7 @@ ${tail}` : head;
59078
59524
  runHook(opts) {
59079
59525
  const { runner, name: name4, path: path2, env, timeoutMs, runLog, watch, onStdout, onStderr } = opts;
59080
59526
  const cwd = join39(bandHome(), "runners", runner.id);
59081
- mkdirSync7(cwd, { recursive: true, mode: 448 });
59527
+ mkdirSync8(cwd, { recursive: true, mode: 448 });
59082
59528
  return new Promise((resolve15, reject) => {
59083
59529
  let child;
59084
59530
  try {
@@ -59136,7 +59582,7 @@ ${tail}` : head;
59136
59582
  });
59137
59583
 
59138
59584
  // src/server/services/ephemeral-lifecycle-service.ts
59139
- import { rmSync as rmSync5 } from "node:fs";
59585
+ import { rmSync as rmSync6 } from "node:fs";
59140
59586
  import { mkdir as mkdir6, readFile as readFile6, writeFile as writeFile4 } from "node:fs/promises";
59141
59587
  import { join as join40, posix as posix5 } from "node:path";
59142
59588
  function idleTimeoutMs() {
@@ -59146,7 +59592,7 @@ function idleTimeoutMs() {
59146
59592
  function sleepDir(worktreeId4) {
59147
59593
  return join40(bandHome(), "sleep", worktreeId4);
59148
59594
  }
59149
- var log23, LOCAL_HOST_ID5, WAKE_POLL_MS, WAKE_GRACE_MS, EXIT_GRACE_MS, SleepError, errorText2, EphemeralLifecycleService, ephemeralLifecycleService;
59595
+ var log23, LOCAL_HOST_ID6, WAKE_POLL_MS, WAKE_GRACE_MS, EXIT_GRACE_MS, SleepError, errorText2, EphemeralLifecycleService, ephemeralLifecycleService;
59150
59596
  var init_ephemeral_lifecycle_service = __esm({
59151
59597
  "src/server/services/ephemeral-lifecycle-service.ts"() {
59152
59598
  "use strict";
@@ -59168,7 +59614,7 @@ var init_ephemeral_lifecycle_service = __esm({
59168
59614
  init_state();
59169
59615
  init_task_service();
59170
59616
  log23 = createLogger("ephemeral-lifecycle");
59171
- LOCAL_HOST_ID5 = "local";
59617
+ LOCAL_HOST_ID6 = "local";
59172
59618
  WAKE_POLL_MS = 500;
59173
59619
  WAKE_GRACE_MS = 5 * 6e4;
59174
59620
  EXIT_GRACE_MS = 6e4;
@@ -59499,7 +59945,7 @@ var init_ephemeral_lifecycle_service = __esm({
59499
59945
  }
59500
59946
  forget(row) {
59501
59947
  this.sleeps.delete(row.worktreeId);
59502
- rmSync5(sleepDir(row.worktreeId), { recursive: true, force: true });
59948
+ rmSync6(sleepDir(row.worktreeId), { recursive: true, force: true });
59503
59949
  }
59504
59950
  // ---- wake ---------------------------------------------------------------
59505
59951
  /**
@@ -59511,7 +59957,7 @@ var init_ephemeral_lifecycle_service = __esm({
59511
59957
  async ensureAwake(worktreeId4) {
59512
59958
  for (let i = 0; i < 3; i++) {
59513
59959
  const hostId2 = this.worktrees.findHostId(worktreeId4);
59514
- if (!hostId2 || hostId2 === LOCAL_HOST_ID5) return;
59960
+ if (!hostId2 || hostId2 === LOCAL_HOST_ID6) return;
59515
59961
  const draining = this.draining.get(hostId2);
59516
59962
  if (draining) {
59517
59963
  await draining;
@@ -59680,10 +60126,10 @@ var init_ephemeral_lifecycle_service = __esm({
59680
60126
  }
59681
60127
  const stage = posix5.join(wipDir, "sessions", row.worktreeId);
59682
60128
  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);
60129
+ for (const file3 of files) {
60130
+ const target = posix5.join(stage, file3.root, file3.rel);
59685
60131
  await host.fs.mkdir(posix5.dirname(target), { recursive: true });
59686
- await host.fs.writeFile(target, Buffer.from(file2.data, "base64"));
60132
+ await host.fs.writeFile(target, Buffer.from(file3.data, "base64"));
59687
60133
  }
59688
60134
  await rpc.call(METHOD_LIFECYCLE_IMPORT_SESSIONS, { dir: stage });
59689
60135
  }
@@ -60843,6 +61289,8 @@ var init_client_state_keys = __esm({
60843
61289
  exact("band.repos-list.collapsed-pinned", [jsonPart("all")]),
60844
61290
  exact("band.repos-list.label-filter", [rawPart("all")]),
60845
61291
  exact("band.repos-list.label-last-worktree", [jsonPart("all")]),
61292
+ // The hooks version whose install banner the user dismissed.
61293
+ exact("band:agent-hooks-banner-dismissed", [rawPart("all")]),
60846
61294
  perWorktree(CENTER_TABS_PREFIX, [jsonPart("all")]),
60847
61295
  perWorktreeLeaf("band:term-split:", [jsonPart("all")]),
60848
61296
  perWorktree("band:diff-compare-branch:", [rawPart("all")]),
@@ -61069,11 +61517,11 @@ var init_cronjobs = __esm({
61069
61517
  * incomplete state (some jobs missing) — the legacy `cronjob-store.ts`
61070
61518
  * had the same gap; the migration is the appropriate moment to fix it.
61071
61519
  */
61072
- saveFile(key2, file2) {
61520
+ saveFile(key2, file3) {
61073
61521
  const db = getDb();
61074
61522
  db.transaction((tx) => {
61075
61523
  tx.delete(cronjobs).where(eq(cronjobs.fileKey, key2)).run();
61076
- for (const job of file2.jobs) {
61524
+ for (const job of file3.jobs) {
61077
61525
  tx.insert(cronjobs).values({
61078
61526
  id: job.id,
61079
61527
  fileKey: key2,
@@ -61265,14 +61713,14 @@ var init_cronjob_service = __esm({
61265
61713
  list(filter) {
61266
61714
  if (filter?.repo) {
61267
61715
  const { repo } = filter;
61268
- const file2 = this.queries.loadFile(repo);
61269
- return { jobs: file2.jobs.map((j) => ({ ...j, fileKey: repo })) };
61716
+ const file3 = this.queries.loadFile(repo);
61717
+ return { jobs: file3.jobs.map((j) => ({ ...j, fileKey: repo })) };
61270
61718
  }
61271
61719
  if (filter?.worktreeId) {
61272
61720
  const { worktreeId: worktreeId4 } = filter;
61273
- const file2 = this.queries.loadFile(worktreeId4);
61721
+ const file3 = this.queries.loadFile(worktreeId4);
61274
61722
  return {
61275
- jobs: file2.jobs.map((j) => ({ ...j, fileKey: worktreeId4 }))
61723
+ jobs: file3.jobs.map((j) => ({ ...j, fileKey: worktreeId4 }))
61276
61724
  };
61277
61725
  }
61278
61726
  return { jobs: this.queries.listAll() };
@@ -61300,7 +61748,7 @@ var init_cronjob_service = __esm({
61300
61748
  if (input.scope === "worktree" && !input.worktreeId) {
61301
61749
  throw new CronjobWorktreeMissingError();
61302
61750
  }
61303
- const file2 = this.queries.loadFile(input.key);
61751
+ const file3 = this.queries.loadFile(input.key);
61304
61752
  const job = {
61305
61753
  id: generateCronjobId(),
61306
61754
  name: input.name,
@@ -61314,8 +61762,8 @@ var init_cronjob_service = __esm({
61314
61762
  enabled: input.enabled,
61315
61763
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
61316
61764
  };
61317
- file2.jobs.push(job);
61318
- this.queries.saveFile(input.key, file2);
61765
+ file3.jobs.push(job);
61766
+ this.queries.saveFile(input.key, file3);
61319
61767
  this.reloadSchedules();
61320
61768
  return { job };
61321
61769
  }
@@ -61330,14 +61778,14 @@ var init_cronjob_service = __esm({
61330
61778
  if (input.cronExpression !== void 0) {
61331
61779
  this.assertValidCron(input.cronExpression);
61332
61780
  }
61333
- const file2 = this.queries.loadFile(input.key);
61334
- const job = file2.jobs.find((j) => j.id === input.id);
61781
+ const file3 = this.queries.loadFile(input.key);
61782
+ const job = file3.jobs.find((j) => j.id === input.id);
61335
61783
  if (!job) throw new CronjobNotFoundError();
61336
61784
  if (input.name !== void 0) job.name = input.name;
61337
61785
  if (input.prompt !== void 0) job.prompt = input.prompt;
61338
61786
  if (input.cronExpression !== void 0) job.cronExpression = input.cronExpression;
61339
61787
  if (input.enabled !== void 0) job.enabled = input.enabled;
61340
- this.queries.saveFile(input.key, file2);
61788
+ this.queries.saveFile(input.key, file3);
61341
61789
  this.reloadSchedules();
61342
61790
  return { job };
61343
61791
  }
@@ -61347,11 +61795,11 @@ var init_cronjob_service = __esm({
61347
61795
  * Throws `CronjobNotFoundError` if the job is already gone.
61348
61796
  */
61349
61797
  delete(key2, id) {
61350
- const file2 = this.queries.loadFile(key2);
61351
- const index2 = file2.jobs.findIndex((j) => j.id === id);
61798
+ const file3 = this.queries.loadFile(key2);
61799
+ const index2 = file3.jobs.findIndex((j) => j.id === id);
61352
61800
  if (index2 === -1) throw new CronjobNotFoundError();
61353
- const [removed] = file2.jobs.splice(index2, 1);
61354
- this.queries.saveFile(key2, file2);
61801
+ const [removed] = file3.jobs.splice(index2, 1);
61802
+ this.queries.saveFile(key2, file3);
61355
61803
  if (removed?.lastTerminalId) {
61356
61804
  const terminalId = removed.lastTerminalId;
61357
61805
  void terminalService.kill(terminalId).catch((err) => {
@@ -61469,8 +61917,8 @@ var init_cronjob_service = __esm({
61469
61917
  */
61470
61918
  stopJobsForKey(key2) {
61471
61919
  const state2 = schedulerState();
61472
- const file2 = this.queries.loadFile(key2);
61473
- for (const job of file2.jobs) {
61920
+ const file3 = this.queries.loadFile(key2);
61921
+ for (const job of file3.jobs) {
61474
61922
  const cron = state2.jobs.get(job.id);
61475
61923
  if (cron) {
61476
61924
  cron.stop();
@@ -61621,8 +62069,8 @@ exit`;
61621
62069
  // Internals
61622
62070
  // -------------------------------------------------------------------------
61623
62071
  findJob(key2, id) {
61624
- const file2 = this.queries.loadFile(key2);
61625
- return file2.jobs.find((j) => j.id === id);
62072
+ const file3 = this.queries.loadFile(key2);
62073
+ return file3.jobs.find((j) => j.id === id);
61626
62074
  }
61627
62075
  assertValidCron(expression) {
61628
62076
  try {
@@ -61744,28 +62192,28 @@ exit`;
61744
62192
  // src/server/services/local-host-policy.ts
61745
62193
  function resolveWorktreeHostId(requested) {
61746
62194
  if (!isLocalHostEnabled()) {
61747
- if (requested === LOCAL_HOST_ID6) throw new LocalHostDisabledError();
62195
+ if (requested === LOCAL_HOST_ID7) throw new LocalHostDisabledError();
61748
62196
  if (requested) return requested;
61749
62197
  const configured = process.env.BAND_DEFAULT_HOST?.trim();
61750
62198
  if (configured) {
61751
- if (configured === LOCAL_HOST_ID6) throw new LocalHostDisabledError();
62199
+ if (configured === LOCAL_HOST_ID7) throw new LocalHostDisabledError();
61752
62200
  return configured;
61753
62201
  }
61754
- const online = tokenService.listHosts().filter((h) => h.id !== LOCAL_HOST_ID6 && h.status === "online");
62202
+ const online = tokenService.listHosts().filter((h) => h.id !== LOCAL_HOST_ID7 && h.status === "online");
61755
62203
  if (online.length === 1) return online[0].id;
61756
62204
  throw new NoDefaultHostError(
61757
62205
  online.length === 0 ? "No worker is online." : "More than one worker is online."
61758
62206
  );
61759
62207
  }
61760
- return requested ?? LOCAL_HOST_ID6;
62208
+ return requested ?? LOCAL_HOST_ID7;
61761
62209
  }
61762
- var LOCAL_HOST_ID6, LocalHostDisabledError, NoDefaultHostError;
62210
+ var LOCAL_HOST_ID7, LocalHostDisabledError, NoDefaultHostError;
61763
62211
  var init_local_host_policy = __esm({
61764
62212
  "src/server/services/local-host-policy.ts"() {
61765
62213
  "use strict";
61766
62214
  init_local_host_enabled();
61767
62215
  init_token_service();
61768
- LOCAL_HOST_ID6 = "local";
62216
+ LOCAL_HOST_ID7 = "local";
61769
62217
  LocalHostDisabledError = class extends Error {
61770
62218
  constructor() {
61771
62219
  super(
@@ -62225,6 +62673,7 @@ var init_worktree_script_service = __esm({
62225
62673
  // src/server/services/worktree-service.ts
62226
62674
  var worktree_service_exports = {};
62227
62675
  __export(worktree_service_exports, {
62676
+ InvalidOriginError: () => InvalidOriginError,
62228
62677
  PlainRepoError: () => PlainRepoError,
62229
62678
  RepoNotFoundError: () => RepoNotFoundError,
62230
62679
  WorktreeNotFoundError: () => WorktreeNotFoundError,
@@ -62236,11 +62685,12 @@ __export(worktree_service_exports, {
62236
62685
  worktreeRemoveInput: () => worktreeRemoveInput,
62237
62686
  worktreeRunScriptInput: () => worktreeRunScriptInput,
62238
62687
  worktreeService: () => worktreeService,
62688
+ worktreeSetOriginInput: () => worktreeSetOriginInput,
62239
62689
  worktreeSetPinnedInput: () => worktreeSetPinnedInput,
62240
62690
  worktreeVia: () => worktreeVia
62241
62691
  });
62242
62692
  import { randomUUID as randomUUID10 } from "node:crypto";
62243
- import { unlinkSync as unlinkSync4 } from "node:fs";
62693
+ import { unlinkSync as unlinkSync5 } from "node:fs";
62244
62694
  import { join as join41, posix as posix6 } from "node:path";
62245
62695
  function findWorktreeRow(worktrees2, name4, hostId2) {
62246
62696
  const named = worktrees2.filter((wt) => wt.name === name4);
@@ -62282,7 +62732,7 @@ function expandHome(given, home, hostId2) {
62282
62732
  }
62283
62733
  return path2;
62284
62734
  }
62285
- var TEARDOWN_TIMEOUT_MS, log33, worktreeVia, worktreeCreateInput, worktreeRemoveInput, worktreeSetPinnedInput, worktreeGitInput, worktreeRunScriptInput, RepoNotFoundError, PlainRepoError, WorktreeService, worktreeService;
62735
+ var TEARDOWN_TIMEOUT_MS, log33, worktreeVia, worktreeCreateInput, worktreeRemoveInput, worktreeSetPinnedInput, worktreeSetOriginInput, worktreeGitInput, worktreeRunScriptInput, RepoNotFoundError, InvalidOriginError, PlainRepoError, WorktreeService, worktreeService;
62286
62736
  var init_worktree_service = __esm({
62287
62737
  "src/server/services/worktree-service.ts"() {
62288
62738
  "use strict";
@@ -62383,6 +62833,14 @@ var init_worktree_service = __esm({
62383
62833
  hostId: external_exports2.string().optional(),
62384
62834
  pinned: external_exports2.boolean()
62385
62835
  });
62836
+ worktreeSetOriginInput = external_exports2.object({
62837
+ repo: external_exports2.string(),
62838
+ // Worktree identity (immutable `name`), not the live git branch.
62839
+ name: external_exports2.string(),
62840
+ hostId: external_exports2.string().optional(),
62841
+ // The parent worktree's id, or null to remove the parent.
62842
+ origin: external_exports2.string().min(1).nullable()
62843
+ });
62386
62844
  worktreeGitInput = external_exports2.object({
62387
62845
  repo: external_exports2.string(),
62388
62846
  // Worktree identity (immutable `name`), not the live git branch.
@@ -62399,6 +62857,12 @@ var init_worktree_service = __esm({
62399
62857
  this.name = "RepoNotFoundError";
62400
62858
  }
62401
62859
  };
62860
+ InvalidOriginError = class extends Error {
62861
+ constructor(message) {
62862
+ super(message);
62863
+ this.name = "InvalidOriginError";
62864
+ }
62865
+ };
62402
62866
  PlainRepoError = class extends Error {
62403
62867
  constructor(message) {
62404
62868
  super(message);
@@ -62913,7 +63377,7 @@ var init_worktree_service = __esm({
62913
63377
  saveState(state2);
62914
63378
  removal.commit();
62915
63379
  try {
62916
- unlinkSync4(join41(bandHome(), "workspace-prompts", `${worktreeId4}.json`));
63380
+ unlinkSync5(join41(bandHome(), "workspace-prompts", `${worktreeId4}.json`));
62917
63381
  } catch {
62918
63382
  }
62919
63383
  deleteWorktreeStatus(worktreeId4);
@@ -63075,6 +63539,52 @@ var init_worktree_service = __esm({
63075
63539
  saveState(state2);
63076
63540
  return { ok: true };
63077
63541
  }
63542
+ /**
63543
+ * Sets a worktree's parent (its origin) by hand, or removes it with `null`. A manual parent
63544
+ * carries no chat or terminal. Rejects the worktree itself and any descendant of it, since
63545
+ * either would make a cycle.
63546
+ */
63547
+ setOrigin(input) {
63548
+ const state2 = loadState();
63549
+ const repo = state2.repos.find((p2) => p2.name === input.repo);
63550
+ if (!repo) throw new RepoNotFoundError(input.repo);
63551
+ const target = findWorktreeRow(repo.worktrees, input.name, input.hostId);
63552
+ if (!target) throw new WorktreeNotFoundError(input.name);
63553
+ const targetHostId = target.hostId ?? "local";
63554
+ const worktreeId4 = toWorktreeId(repo.name, target.name, target.hostId);
63555
+ if (input.origin !== null) {
63556
+ const origins = /* @__PURE__ */ new Map();
63557
+ for (const r of state2.repos) {
63558
+ for (const wt of r.worktrees) {
63559
+ origins.set(toWorktreeId(r.name, wt.name, wt.hostId), wt.origin?.worktreeId);
63560
+ }
63561
+ }
63562
+ if (!origins.has(input.origin)) {
63563
+ throw new InvalidOriginError(`Unknown origin worktree: ${input.origin}`);
63564
+ }
63565
+ if (input.origin === worktreeId4) {
63566
+ throw new InvalidOriginError("A worktree cannot be its own parent.");
63567
+ }
63568
+ const seen = /* @__PURE__ */ new Set();
63569
+ for (let id = input.origin; id && !seen.has(id); ) {
63570
+ if (id === worktreeId4) {
63571
+ throw new InvalidOriginError(
63572
+ "That worktree is a descendant of this one, so it cannot be its parent."
63573
+ );
63574
+ }
63575
+ seen.add(id);
63576
+ id = origins.get(id);
63577
+ }
63578
+ }
63579
+ this.repoQueries.setOrigin(
63580
+ repo.name,
63581
+ target.name,
63582
+ input.origin === null ? null : { worktreeId: input.origin },
63583
+ targetHostId
63584
+ );
63585
+ emit({ kind: "worktree-origin-changed", worktreeId: worktreeId4 });
63586
+ return { ok: true };
63587
+ }
63078
63588
  pruning = /* @__PURE__ */ new Set();
63079
63589
  /**
63080
63590
  * Removes the records of worktrees whose folder is gone from their host (`missing` after a scan),
@@ -63375,8 +63885,8 @@ var init_worktree_service = __esm({
63375
63885
  throw new Error(`Script "${input.scriptType}" not found`);
63376
63886
  }
63377
63887
  try {
63378
- const { file: file2, args } = scriptInvocation(scriptPath);
63379
- await host.exec(file2, args, { cwd: input.path });
63888
+ const { file: file3, args } = scriptInvocation(scriptPath);
63889
+ await host.exec(file3, args, { cwd: input.path });
63380
63890
  } catch (err) {
63381
63891
  throw new Error(err instanceof Error ? err.message : String(err));
63382
63892
  }
@@ -63388,15 +63898,6 @@ var init_worktree_service = __esm({
63388
63898
  });
63389
63899
 
63390
63900
  // 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
63901
  function persistTask(task) {
63401
63902
  const worktree = worktreeService.resolve(task.worktreeId);
63402
63903
  try {
@@ -63429,19 +63930,19 @@ function fileUri(path2) {
63429
63930
  async function promptBlocks(task, firstTurn, sharedDir) {
63430
63931
  const caps = agentSessionService.promptCapabilities(task.chatId);
63431
63932
  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/")) {
63933
+ for (const file3 of task.attachments) {
63934
+ const name4 = file3.filename ?? file3.path.split("/").pop() ?? file3.path;
63935
+ if (caps.image && file3.mediaType.startsWith("image/")) {
63435
63936
  try {
63436
63937
  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) });
63938
+ const data = Buffer.from(await host.fs.readFile(file3.path)).toString("base64");
63939
+ blocks.push({ type: "image", mimeType: file3.mediaType, data, uri: fileUri(file3.path) });
63439
63940
  continue;
63440
63941
  } catch (err) {
63441
- log34.warn({ path: file2.path, err }, "could not read image attachment; linking it instead");
63942
+ log34.warn({ path: file3.path, err }, "could not read image attachment; linking it instead");
63442
63943
  }
63443
63944
  }
63444
- blocks.push({ type: "resource_link", uri: fileUri(file2.path), name: name4, mimeType: file2.mediaType });
63945
+ blocks.push({ type: "resource_link", uri: fileUri(file3.path), name: name4, mimeType: file3.mediaType });
63445
63946
  }
63446
63947
  if (firstTurn) {
63447
63948
  blocks.push({
@@ -63486,10 +63987,6 @@ function turnUsage(chatId, res, previousCost) {
63486
63987
  });
63487
63988
  return { ...usage, model, costUsd: (previousCost ?? 0) + cost };
63488
63989
  }
63489
- function setScopeStatus(scope3, chatId, status) {
63490
- const updated = setWorktreeSourceStatus(scope3, chatStatusSource(chatId), { status });
63491
- emit({ kind: "update", status: updated });
63492
- }
63493
63990
  function submitTask(options) {
63494
63991
  const { worktreeId: worktreeId4, chatId, prompt, sessionId, mode, model, codingAgentId } = options;
63495
63992
  if (worktreeService.resolve(worktreeId4) === null) {
@@ -63499,7 +63996,7 @@ function submitTask(options) {
63499
63996
  if (existing?.status === "running") {
63500
63997
  throw new TaskConflictError(chatId);
63501
63998
  }
63502
- observePending();
63999
+ observeAgentStatus(hasRunningTask);
63503
64000
  const task = {
63504
64001
  id: generateTaskId(),
63505
64002
  worktreeId: worktreeId4,
@@ -63543,7 +64040,7 @@ function submitOrQueueTask(options) {
63543
64040
  async function runTask(task) {
63544
64041
  const { chatId } = task;
63545
64042
  chatService.updateStatus(chatId, "running");
63546
- setScopeStatus(task.worktreeId, chatId, "working");
64043
+ beginBandTurn(task.worktreeId, chatId);
63547
64044
  const chat = chatService.get(chatId);
63548
64045
  if (task.codingAgentId && chat && task.codingAgentId !== chat.agent) {
63549
64046
  agentSessionService.stop(chatId);
@@ -63643,7 +64140,7 @@ async function runTask(task) {
63643
64140
  durationMs: Date.now() - task.startedAt,
63644
64141
  usage: turnUsage(chatId, res, previousCost)
63645
64142
  });
63646
- finishTask(task, res.stopReason === "cancelled" ? "cancelled" : "completed");
64143
+ finishTask(task, res.stopReason === "cancelled" ? "cancelled" : "completed", res.stopReason);
63647
64144
  } catch (err) {
63648
64145
  const message = err instanceof Error ? err.message : String(err);
63649
64146
  agentSessionService.record(chatId, {
@@ -63667,7 +64164,7 @@ async function applyTurnChoice(chatId, category, value) {
63667
64164
  log34.warn({ chatId, category, value, err }, "could not apply the task's setting");
63668
64165
  }
63669
64166
  }
63670
- function finishTask(task, outcome) {
64167
+ function finishTask(task, outcome, stopReason) {
63671
64168
  if (task.status !== "running") return;
63672
64169
  const status = outcome === "completed" ? "completed" : "failed";
63673
64170
  task.status = status;
@@ -63678,7 +64175,8 @@ function finishTask(task, outcome) {
63678
64175
  if (status === "completed" && drainQueue(task.worktreeId, task.chatId)) return;
63679
64176
  chatService.updateStatus(task.chatId, status === "completed" ? "idle" : "error");
63680
64177
  const stopped = outcome === "cancelled" || task.cancelRequested === true;
63681
- setScopeStatus(task.worktreeId, task.chatId, stopped ? "waiting" : "needs_attention");
64178
+ const failed = outcome === "failed" || stopReason !== void 0 && FAILED_STOP_REASONS.has(stopReason);
64179
+ endBandTurn(task.worktreeId, task.chatId, stopped ? "idle" : failed ? "failed" : "done");
63682
64180
  }
63683
64181
  function drainQueue(worktreeId4, chatId) {
63684
64182
  const queued = peekQueuedMessage(chatId);
@@ -63721,7 +64219,7 @@ function cancelTask(taskId) {
63721
64219
  const record3 = taskQueries.markFailed(taskId);
63722
64220
  if (record3) {
63723
64221
  if (record3.chatId) {
63724
- setScopeStatus(record3.worktreeId, record3.chatId, "waiting");
64222
+ setChatStatus(record3.worktreeId, record3.chatId, "idle");
63725
64223
  }
63726
64224
  log34.info({ taskId, worktreeId: record3.worktreeId }, "orphaned task cancelled");
63727
64225
  return { cancelled: true, worktreeId: record3.worktreeId };
@@ -63735,7 +64233,7 @@ function getTask(chatId) {
63735
64233
  const task = tasks2.get(chatId);
63736
64234
  return task ? toTaskInfo(task) : null;
63737
64235
  }
63738
- var log34, taskQueries, TASKS_KEY, g4, tasks2, observingPending, TaskConflictError, TaskService, taskService;
64236
+ var log34, taskQueries, TASKS_KEY, g4, tasks2, TaskConflictError, FAILED_STOP_REASONS, TaskService, taskService;
63739
64237
  var init_task_service = __esm({
63740
64238
  "src/server/services/task-service.ts"() {
63741
64239
  "use strict";
@@ -63750,8 +64248,8 @@ var init_task_service = __esm({
63750
64248
  init_turn_events();
63751
64249
  init_agent_session_service();
63752
64250
  init_chat_service();
64251
+ init_chat_status_service();
63753
64252
  init_state();
63754
- init_watcher_service();
63755
64253
  init_worktree_service();
63756
64254
  log34 = createLogger("task-service");
63757
64255
  taskQueries = new TaskQueries();
@@ -63759,13 +64257,13 @@ var init_task_service = __esm({
63759
64257
  g4 = globalThis;
63760
64258
  if (!g4[TASKS_KEY]) g4[TASKS_KEY] = /* @__PURE__ */ new Map();
63761
64259
  tasks2 = g4[TASKS_KEY];
63762
- observingPending = false;
63763
64260
  TaskConflictError = class extends Error {
63764
64261
  constructor(chatId) {
63765
64262
  super(`Task already running for chat ${chatId}`);
63766
64263
  this.name = "TaskConflictError";
63767
64264
  }
63768
64265
  };
64266
+ FAILED_STOP_REASONS = /* @__PURE__ */ new Set(["refusal", "max_tokens", "max_turn_requests"]);
63769
64267
  TaskService = class {
63770
64268
  constructor(queries3 = taskQueries) {
63771
64269
  this.queries = queries3;
@@ -63785,13 +64283,10 @@ var init_task_service = __esm({
63785
64283
  getTask(chatId) {
63786
64284
  return getTask(chatId);
63787
64285
  }
63788
- /** Clears the worktree's attention status, except for chats whose agent
63789
- * still waits on the user (a permission or elicitation request). */
64286
+ /** Sets the worktree's finished chats (`done`, `failed`) back to `idle`. A
64287
+ * chat whose agent still waits on the user keeps its `question`. */
63790
64288
  acknowledgeAttention(worktreeId4) {
63791
- return acknowledgeWorktreeAttention(
63792
- worktreeId4,
63793
- agentSessionService.chatsWithPendingRequest(worktreeId4)
63794
- );
64289
+ return acknowledgeWorktreeAttention(worktreeId4);
63795
64290
  }
63796
64291
  listTaskRecords(filters) {
63797
64292
  return this.queries.list(filters);
@@ -65365,9 +65860,88 @@ function parseReview(data, branch, defaultBranch) {
65365
65860
  updatedAt: pr.updatedAt,
65366
65861
  reviewDecision: reviewDecision(pr.reviewDecision),
65367
65862
  mergeState: mergeState(pr.mergeStateStatus),
65368
- checks: report(head?.oid ?? null, checks)
65863
+ checks: report(head?.oid ?? null, checks),
65864
+ reviewers: parseReviewers(pr),
65865
+ discussion: parseDiscussion(pr)
65866
+ };
65867
+ }
65868
+ function parseReviewers(pr) {
65869
+ const byLogin = /* @__PURE__ */ new Map();
65870
+ const order = [...pr.reviews?.nodes ?? []].sort(
65871
+ (a, b2) => (a.submittedAt ?? "").localeCompare(b2.submittedAt ?? "")
65872
+ );
65873
+ for (const review of order) {
65874
+ const state2 = REVIEWER_STATES[review.state];
65875
+ if (!state2 || !review.author) continue;
65876
+ const existing = byLogin.get(review.author.login);
65877
+ if (existing && state2 === "commented" && existing.state !== "commented") continue;
65878
+ byLogin.set(review.author.login, {
65879
+ ...actorOf(review.author),
65880
+ state: state2,
65881
+ submittedAt: review.submittedAt
65882
+ });
65883
+ }
65884
+ for (const request of pr.reviewRequests?.nodes ?? []) {
65885
+ const who = request.requestedReviewer;
65886
+ if (!who) continue;
65887
+ const actor = who.__typename === "Team" ? { login: who.slug, avatarUrl: null } : actorOf({ login: who.login, avatarUrl: who.avatarUrl });
65888
+ const key2 = who.__typename === "Team" ? `team:${actor.login}` : actor.login;
65889
+ const previous = byLogin.get(key2);
65890
+ byLogin.set(key2, {
65891
+ ...actor,
65892
+ state: "pending",
65893
+ submittedAt: previous?.submittedAt ?? null,
65894
+ ...request.asCodeOwner ? { required: true } : {}
65895
+ });
65896
+ }
65897
+ return [...byLogin.values()];
65898
+ }
65899
+ function fromCommentNode(node) {
65900
+ return {
65901
+ id: node.id,
65902
+ author: actorOf(node.author),
65903
+ body: node.body,
65904
+ createdAt: node.createdAt,
65905
+ url: webUrl(node.url)
65369
65906
  };
65370
65907
  }
65908
+ function parseDiscussion(pr) {
65909
+ const items = [];
65910
+ for (const node of pr.comments?.nodes ?? []) {
65911
+ items.push({ ...fromCommentNode(node), kind: "comment" });
65912
+ }
65913
+ for (const review of pr.reviews?.nodes ?? []) {
65914
+ const state2 = REVIEWER_STATES[review.state];
65915
+ if (!state2 || !review.body.trim() || !review.submittedAt) continue;
65916
+ items.push({
65917
+ id: review.id,
65918
+ kind: "review",
65919
+ reviewState: state2,
65920
+ author: actorOf(review.author),
65921
+ body: review.body,
65922
+ createdAt: review.submittedAt,
65923
+ url: webUrl(review.url)
65924
+ });
65925
+ }
65926
+ items.sort((a, b2) => a.createdAt.localeCompare(b2.createdAt));
65927
+ const threads = [];
65928
+ for (const node of pr.reviewThreads?.nodes ?? []) {
65929
+ const comments = node.comments.nodes.map(fromCommentNode);
65930
+ if (comments.length === 0) continue;
65931
+ threads.push({
65932
+ id: node.id,
65933
+ path: node.path,
65934
+ line: node.line ?? node.originalLine,
65935
+ isResolved: node.isResolved,
65936
+ isOutdated: node.isOutdated,
65937
+ comments,
65938
+ ...node.comments.totalCount > comments.length ? { truncated: true } : {}
65939
+ });
65940
+ }
65941
+ threads.sort((a, b2) => a.comments[0].createdAt.localeCompare(b2.comments[0].createdAt));
65942
+ 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);
65943
+ return { items, threads, ...truncated ? { truncated: true } : {} };
65944
+ }
65371
65945
  function parseBranchChecks(data) {
65372
65946
  const target = data.repository?.ref?.target;
65373
65947
  const latest = /* @__PURE__ */ new Map();
@@ -65484,10 +66058,16 @@ function reviewDecision(value) {
65484
66058
  function mergeState(value) {
65485
66059
  return value && MERGE_STATES[value] || "unknown";
65486
66060
  }
65487
- var CHECK_RUN_FIELDS, REVIEW_QUERY, LIST_ORDER, AGGREGATE_PRIORITY, MERGE_STATES;
66061
+ 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
66062
  var init_checks_query = __esm({
65489
66063
  "../../plugins/github/src/server/checks-query.ts"() {
65490
66064
  "use strict";
66065
+ MAX_COMMENTS = 50;
66066
+ MAX_REVIEWS = 50;
66067
+ MAX_THREADS = 50;
66068
+ MAX_THREAD_COMMENTS = 20;
66069
+ MAX_REVIEW_REQUESTS = 30;
66070
+ ACTOR = "login avatarUrl";
65491
66071
  CHECK_RUN_FIELDS = `
65492
66072
  databaseId
65493
66073
  name
@@ -65512,6 +66092,41 @@ var init_checks_query = __esm({
65512
66092
  updatedAt
65513
66093
  mergeStateStatus
65514
66094
  reviewDecision
66095
+ reviewRequests(first: ${MAX_REVIEW_REQUESTS}) {
66096
+ nodes {
66097
+ asCodeOwner
66098
+ requestedReviewer {
66099
+ __typename
66100
+ ... on User { ${ACTOR} }
66101
+ ... on Bot { ${ACTOR} }
66102
+ ... on Mannequin { ${ACTOR} }
66103
+ ... on Team { slug }
66104
+ }
66105
+ }
66106
+ }
66107
+ reviews(last: ${MAX_REVIEWS}) {
66108
+ totalCount
66109
+ nodes { id state body submittedAt url author { ${ACTOR} } }
66110
+ }
66111
+ comments(last: ${MAX_COMMENTS}) {
66112
+ totalCount
66113
+ nodes { id body createdAt url author { ${ACTOR} } }
66114
+ }
66115
+ reviewThreads(first: ${MAX_THREADS}) {
66116
+ totalCount
66117
+ nodes {
66118
+ id
66119
+ isResolved
66120
+ isOutdated
66121
+ path
66122
+ line
66123
+ originalLine
66124
+ comments(first: ${MAX_THREAD_COMMENTS}) {
66125
+ totalCount
66126
+ nodes { id body createdAt url author { ${ACTOR} } }
66127
+ }
66128
+ }
66129
+ }
65515
66130
  commits(last: 1) {
65516
66131
  nodes {
65517
66132
  commit {
@@ -65549,6 +66164,16 @@ var init_checks_query = __esm({
65549
66164
  }
65550
66165
  }
65551
66166
  }`;
66167
+ REVIEWER_STATES = {
66168
+ APPROVED: "approved",
66169
+ CHANGES_REQUESTED: "changes_requested",
66170
+ COMMENTED: "commented",
66171
+ DISMISSED: "dismissed"
66172
+ };
66173
+ actorOf = (author) => ({
66174
+ login: author?.login ?? "ghost",
66175
+ avatarUrl: webUrl(author?.avatarUrl ?? null)
66176
+ });
65552
66177
  LIST_ORDER = {
65553
66178
  failure: 0,
65554
66179
  running: 1,
@@ -68049,11 +68674,14 @@ function stopRuntime(rt) {
68049
68674
  }
68050
68675
  function idleTimeoutMs2() {
68051
68676
  const override = Number(process.env.BAND_AGENT_IDLE_TIMEOUT_MS);
68052
- return Number.isFinite(override) && override > 0 ? override : IDLE_TIMEOUT_MS;
68677
+ return Number.isFinite(override) && override > 0 ? override : null;
68053
68678
  }
68054
- function scheduleIdle(rt, delayMs = idleTimeoutMs2()) {
68679
+ function scheduleIdle(rt, delayMs) {
68055
68680
  if (rt.idleTimer) clearTimeout(rt.idleTimer);
68056
- const delay2 = Math.min(delayMs, MAX_TIMER_MS);
68681
+ rt.idleTimer = null;
68682
+ const timeout = idleTimeoutMs2();
68683
+ if (timeout === null) return;
68684
+ const delay2 = Math.min(delayMs ?? timeout, MAX_TIMER_MS2);
68057
68685
  rt.idleTimer = setTimeout(() => {
68058
68686
  rt.idleTimer = null;
68059
68687
  if (rt.inTurn || rt.pending.size > 0 || rt.attaching) return;
@@ -68068,7 +68696,7 @@ function scheduleIdle(rt, delayMs = idleTimeoutMs2()) {
68068
68696
  },
68069
68697
  "idle agent has pending work; not stopping it"
68070
68698
  );
68071
- scheduleIdle(rt, until - now + idleTimeoutMs2());
68699
+ scheduleIdle(rt, until - now + timeout);
68072
68700
  return;
68073
68701
  }
68074
68702
  log46.info({ chatId: rt.chatId }, "stopping idle agent");
@@ -68208,6 +68836,9 @@ function routeUpdate(rt, notification) {
68208
68836
  if (!rt.attaching) {
68209
68837
  rt.work.observeUpdate(update, rt.inTurn);
68210
68838
  agentActive(rt);
68839
+ if (!rt.inTurn && rt.routing === "log" && AGENT_WORK_UPDATES.has(update.sessionUpdate)) {
68840
+ activityObserver?.(rt.chatId, rt.worktreeId, "update");
68841
+ }
68211
68842
  }
68212
68843
  if (rt.routing === "drop") return;
68213
68844
  if (update.sessionUpdate === "user_message_chunk") {
@@ -68283,6 +68914,7 @@ function handlersFor(rt, generation) {
68283
68914
  if (n.sessionId !== rt.sessionId || rt.attaching) return;
68284
68915
  rt.work.observeAsyncTask(n.update);
68285
68916
  agentActive(rt);
68917
+ activityObserver?.(rt.chatId, rt.worktreeId, "work");
68286
68918
  },
68287
68919
  onPermission: (req, signal) => requestPermission(rt, req, signal),
68288
68920
  onElicitation: (req, signal) => requestElicitation(rt, req, signal),
@@ -68582,7 +69214,7 @@ function sessionCost(sessionId) {
68582
69214
  const row = events.latest(sessionId, revision, { kind: "turn-ended" });
68583
69215
  return row?.event.type === "turn-ended" ? row.event.usage?.costUsd ?? null : null;
68584
69216
  }
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;
69217
+ 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
69218
  var init_agent_session_service = __esm({
68587
69219
  "src/server/services/agent-session-service.ts"() {
68588
69220
  "use strict";
@@ -68600,9 +69232,8 @@ var init_agent_session_service = __esm({
68600
69232
  init_mcp_proxy_service();
68601
69233
  init_worktree_service();
68602
69234
  log46 = createLogger("agent-sessions");
68603
- IDLE_TIMEOUT_MS = 15 * 6e4;
68604
69235
  CANCEL_GRACE_MS = 15e3;
68605
- MAX_TIMER_MS = 2 ** 31 - 1;
69236
+ MAX_TIMER_MS2 = 2 ** 31 - 1;
68606
69237
  PROBE_SETTLE_MS = 1500;
68607
69238
  events = new ChatEventQueries();
68608
69239
  settings = new SettingsQueries();
@@ -68621,6 +69252,15 @@ var init_agent_session_service = __esm({
68621
69252
  defaultsChangedAt = g6[DEFAULTS_CHANGED_KEY];
68622
69253
  transientId = -1e9;
68623
69254
  pendingObserver = null;
69255
+ AGENT_WORK_UPDATES = /* @__PURE__ */ new Set([
69256
+ "user_message_chunk",
69257
+ "agent_message_chunk",
69258
+ "agent_thought_chunk",
69259
+ "tool_call",
69260
+ "tool_call_update",
69261
+ "plan"
69262
+ ]);
69263
+ activityObserver = null;
68624
69264
  ChatNotFoundError = class extends Error {
68625
69265
  constructor(chatId) {
68626
69266
  super(`Chat not found: ${chatId}`);
@@ -68647,6 +69287,16 @@ var init_agent_session_service = __esm({
68647
69287
  observePending(observer) {
68648
69288
  pendingObserver = observer;
68649
69289
  }
69290
+ /** Called when the agent acts outside a turn Band started: an update from
69291
+ * a turn of its own, or a background task starting or ending. */
69292
+ observeActivity(observer) {
69293
+ activityObserver = observer;
69294
+ }
69295
+ /** The soonest time one of the chat's pending-work holds lapses, or null with none. */
69296
+ nextWorkDeadline(chatId) {
69297
+ const holds = runtimes.get(chatId)?.work.outstanding() ?? [];
69298
+ return holds.length > 0 ? Math.min(...holds.map((h) => h.until)) : null;
69299
+ }
68650
69300
  /**
68651
69301
  * Makes sure the chat's agent process runs and is attached to the chat's
68652
69302
  * session, starting a new session when the chat has none. For `view`,
@@ -75914,10 +76564,10 @@ init_agent_spawn();
75914
76564
  import {
75915
76565
  appendFileSync as appendFileSync2,
75916
76566
  createReadStream as createReadStream5,
75917
- existsSync as existsSync16,
75918
- mkdirSync as mkdirSync9,
76567
+ existsSync as existsSync17,
76568
+ mkdirSync as mkdirSync10,
75919
76569
  readFileSync as readFileSync12,
75920
- statSync as statSync13
76570
+ statSync as statSync15
75921
76571
  } from "node:fs";
75922
76572
  import { createServer as createServer3 } from "node:http";
75923
76573
  import { basename as basename7, join as join55, resolve as resolve14, sep as sep10 } from "node:path";
@@ -75928,7 +76578,7 @@ import { parseArgs } from "node:util";
75928
76578
  init_src();
75929
76579
  init_daemon_backend();
75930
76580
  init_in_process_backend();
75931
- import { existsSync as existsSync4, statSync as statSync4 } from "node:fs";
76581
+ import { existsSync as existsSync5, statSync as statSync5 } from "node:fs";
75932
76582
  import { join as join7 } from "node:path";
75933
76583
  var log4 = createLogger("terminal-backend");
75934
76584
  function createTerminalBackend(serverRoot, bandHome2) {
@@ -75937,7 +76587,7 @@ function createTerminalBackend(serverRoot, bandHome2) {
75937
76587
  const entry = [
75938
76588
  join7(serverRoot, "terminal-daemon.mjs"),
75939
76589
  join7(serverRoot, "terminal-daemon.ts")
75940
- ].find((candidate) => existsSync4(candidate));
76590
+ ].find((candidate) => existsSync5(candidate));
75941
76591
  if (!entry) {
75942
76592
  log4.error(
75943
76593
  { serverRoot },
@@ -75945,7 +76595,7 @@ function createTerminalBackend(serverRoot, bandHome2) {
75945
76595
  );
75946
76596
  return new InProcessTerminalBackend();
75947
76597
  }
75948
- const { size, mtimeMs } = statSync4(entry);
76598
+ const { size, mtimeMs } = statSync5(entry);
75949
76599
  return new DaemonTerminalBackend({
75950
76600
  entry,
75951
76601
  runDir: join7(bandHome2, "run"),
@@ -78243,14 +78893,14 @@ import { join as join9, normalize, resolve as resolve2, sep as sep2 } from "node
78243
78893
 
78244
78894
  // ../../node_modules/.pnpm/totalist@3.0.1/node_modules/totalist/sync/index.mjs
78245
78895
  import { join as join8, resolve } from "path";
78246
- import { readdirSync as readdirSync5, statSync as statSync5 } from "fs";
78896
+ import { readdirSync as readdirSync5, statSync as statSync6 } from "fs";
78247
78897
  function totalist(dir, callback, pre = "") {
78248
78898
  dir = resolve(".", dir);
78249
78899
  let arr = readdirSync5(dir);
78250
78900
  let i = 0, abs, stats;
78251
78901
  for (; i < arr.length; i++) {
78252
78902
  abs = join8(dir, arr[i]);
78253
- stats = statSync5(abs);
78903
+ stats = statSync6(abs);
78254
78904
  stats.isDirectory() ? totalist(abs, callback, join8(pre, arr[i])) : callback(join8(pre, arr[i]), abs, stats);
78255
78905
  }
78256
78906
  }
@@ -78774,7 +79424,7 @@ function viaLocal(dir, isEtag, uri, extns) {
78774
79424
  function is404(req, res) {
78775
79425
  return res.statusCode = 404, res.end();
78776
79426
  }
78777
- function send(req, res, file2, stats, headers) {
79427
+ function send(req, res, file3, stats, headers) {
78778
79428
  let code = 200, tmp, opts = {};
78779
79429
  headers = { ...headers };
78780
79430
  for (let key2 in headers) {
@@ -78802,7 +79452,7 @@ function send(req, res, file2, stats, headers) {
78802
79452
  headers["Accept-Ranges"] = "bytes";
78803
79453
  }
78804
79454
  res.writeHead(code, headers);
78805
- fs.createReadStream(file2, opts).pipe(res);
79455
+ fs.createReadStream(file3, opts).pipe(res);
78806
79456
  }
78807
79457
  var ENCODING = {
78808
79458
  ".br": "br",
@@ -79524,7 +80174,7 @@ init_map_limited();
79524
80174
  init_remote_worktrees();
79525
80175
  init_repo_host_policy();
79526
80176
  init_ephemeral_lifecycle_service();
79527
- import { existsSync as existsSync14 } from "node:fs";
80177
+ import { existsSync as existsSync15 } from "node:fs";
79528
80178
  import { basename as basename2, join as join44, posix as posix7, resolve as resolve9 } from "node:path";
79529
80179
 
79530
80180
  // src/server/services/repo-avatar-service.ts
@@ -79916,7 +80566,7 @@ var RepoService = class {
79916
80566
  }
79917
80567
  }
79918
80568
  const resolvedPath = resolve9(path2);
79919
- const kind = existsSync14(join44(resolvedPath, ".git")) ? "git" : "plain";
80569
+ const kind = existsSync15(join44(resolvedPath, ".git")) ? "git" : "plain";
79920
80570
  let defaultBranch = "main";
79921
80571
  let worktrees2 = [];
79922
80572
  if (kind === "git") {
@@ -80253,15 +80903,15 @@ var RepoService = class {
80253
80903
  ...isLocalHostEnabled() ? ["local"] : [],
80254
80904
  ...tokenService.listHosts().filter((h) => h.id !== "local" && h.status === "online").map((h) => h.id)
80255
80905
  ];
80256
- let failure = "no host is available to ask";
80906
+ let failure2 = "no host is available to ask";
80257
80907
  for (const hostId2 of hostIds2) {
80258
80908
  try {
80259
80909
  const { stdout } = await hostRegistry.hostById(hostId2).exec("git", ["ls-remote", "--symref", "--", url2, "HEAD"], { timeoutMs: 6e4 });
80260
80910
  const match = stdout.match(/^ref:\s+refs\/heads\/(\S+)\s+HEAD$/m);
80261
80911
  if (match) return match[1];
80262
- failure = "the remote did not report a default branch";
80912
+ failure2 = "the remote did not report a default branch";
80263
80913
  } catch (err) {
80264
- failure = err instanceof Error ? err.message : String(err);
80914
+ failure2 = err instanceof Error ? err.message : String(err);
80265
80915
  }
80266
80916
  }
80267
80917
  if (asked?.host) {
@@ -80271,7 +80921,7 @@ var RepoService = class {
80271
80921
  if (viaApi) return viaApi;
80272
80922
  }
80273
80923
  throw new RepoInputError(
80274
- `Could not read the default branch of ${asked?.url ?? url2} (${failure}). Pass the branch.`
80924
+ `Could not read the default branch of ${asked?.url ?? url2} (${failure2}). Pass the branch.`
80275
80925
  );
80276
80926
  }
80277
80927
  /**
@@ -80294,7 +80944,7 @@ var RepoService = class {
80294
80944
  message: `Repo "${name4}" is already a git repo`
80295
80945
  });
80296
80946
  }
80297
- if (!existsSync14(repo.path)) {
80947
+ if (!existsSync15(repo.path)) {
80298
80948
  throw new TRPCError({
80299
80949
  code: "NOT_FOUND",
80300
80950
  message: `Repo path "${repo.path}" no longer exists. Remove the repo and re-add it.`
@@ -80511,7 +81161,7 @@ var DEFAULT_MAX_BYTES = 200 * 1024 * 1024;
80511
81161
  var DEFAULT_QUIET_MS = 3 * 6e4;
80512
81162
  var DEFAULT_POLL_MS2 = 6e4;
80513
81163
  var MAX_VIEW_BYTES = 1024 * 1024;
80514
- var LOCAL_HOST_ID7 = "local";
81164
+ var LOCAL_HOST_ID8 = "local";
80515
81165
  var errorText3 = (err) => err instanceof Error ? err.message : String(err);
80516
81166
  function envNumber2(name4, fallback) {
80517
81167
  const raw = Number(process.env[name4]);
@@ -80519,7 +81169,7 @@ function envNumber2(name4, fallback) {
80519
81169
  }
80520
81170
  var checkpointKeep = () => Math.floor(envNumber2("BAND_CHECKPOINT_KEEP", DEFAULT_KEEP));
80521
81171
  var checkpointMaxBytes = () => envNumber2("BAND_CHECKPOINT_MAX_BYTES", DEFAULT_MAX_BYTES);
80522
- var quietMs = () => envNumber2("BAND_CHECKPOINT_QUIET_MS", DEFAULT_QUIET_MS);
81172
+ var quietMs2 = () => envNumber2("BAND_CHECKPOINT_QUIET_MS", DEFAULT_QUIET_MS);
80523
81173
  var pollMs = () => envNumber2("BAND_CHECKPOINT_POLL_MS", DEFAULT_POLL_MS2);
80524
81174
  function toView2(row) {
80525
81175
  return {
@@ -80536,11 +81186,11 @@ function toView2(row) {
80536
81186
  sessions: row.sessionIds.length
80537
81187
  };
80538
81188
  }
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);
81189
+ function isSafeSessionFile(file3) {
81190
+ if (!/^[A-Za-z0-9_-]+$/.test(file3.root)) return false;
81191
+ if (typeof file3.rel !== "string" || file3.rel === "" || file3.rel.includes("\0")) return false;
81192
+ if (posix8.isAbsolute(file3.rel)) return false;
81193
+ const clean = posix8.normalize(file3.rel);
80544
81194
  return clean !== ".." && !clean.startsWith("../") && clean !== ".";
80545
81195
  }
80546
81196
  function sessionsHashOf(files) {
@@ -80629,8 +81279,8 @@ var CheckpointService = class {
80629
81279
  ineligible(worktreeId4) {
80630
81280
  const found = worktreeService.resolve(worktreeId4);
80631
81281
  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";
81282
+ const hostId2 = found.worktree.hostId ?? LOCAL_HOST_ID8;
81283
+ if (hostId2 === LOCAL_HOST_ID8) return "the worktree is on the hub's own machine";
80634
81284
  if (found.repo.kind !== "git") return "not a git repo";
80635
81285
  if (found.worktree.supersededBy) return "superseded";
80636
81286
  if (this.busy.has(worktreeId4)) return "busy";
@@ -80673,7 +81323,7 @@ var CheckpointService = class {
80673
81323
  const out = [];
80674
81324
  for (const repo of loadState().repos) {
80675
81325
  for (const wt of repo.worktrees) {
80676
- if ((wt.hostId ?? LOCAL_HOST_ID7) === hostId2) {
81326
+ if ((wt.hostId ?? LOCAL_HOST_ID8) === hostId2) {
80677
81327
  out.push({
80678
81328
  repo: repo.name,
80679
81329
  name: wt.name,
@@ -80687,8 +81337,8 @@ var CheckpointService = class {
80687
81337
  async doTake(worktreeId4, trigger, opts) {
80688
81338
  const found = worktreeService.resolve(worktreeId4);
80689
81339
  if (!found) throw new CheckpointError("invalid", `Unknown worktree "${worktreeId4}"`);
80690
- const hostId2 = found.worktree.hostId ?? LOCAL_HOST_ID7;
80691
- if (hostId2 === LOCAL_HOST_ID7) {
81340
+ const hostId2 = found.worktree.hostId ?? LOCAL_HOST_ID8;
81341
+ if (hostId2 === LOCAL_HOST_ID8) {
80692
81342
  throw new CheckpointError(
80693
81343
  "invalid",
80694
81344
  "Checkpoints are for worktrees on a worker, not on the hub's own machine."
@@ -80875,7 +81525,7 @@ var CheckpointService = class {
80875
81525
  if (this.ineligible(worktreeId4)) continue;
80876
81526
  live.add(worktreeId4);
80877
81527
  try {
80878
- await this.sweepOne(worktreeId4, wt.hostId ?? LOCAL_HOST_ID7, wt.path);
81528
+ await this.sweepOne(worktreeId4, wt.hostId ?? LOCAL_HOST_ID8, wt.path);
80879
81529
  } catch (err) {
80880
81530
  log49.debug({ worktreeId: worktreeId4, err: errorText3(err) }, "checkpoint sweep skipped a worktree");
80881
81531
  }
@@ -80899,7 +81549,7 @@ var CheckpointService = class {
80899
81549
  this.watches.set(worktreeId4, { signature, changedAt: now, checkpointed: watch.checkpointed });
80900
81550
  return;
80901
81551
  }
80902
- if (watch.checkpointed === signature || now - watch.changedAt < quietMs()) return;
81552
+ if (watch.checkpointed === signature || now - watch.changedAt < quietMs2()) return;
80903
81553
  await this.take(worktreeId4, "quiet");
80904
81554
  }
80905
81555
  /**
@@ -81074,11 +81724,11 @@ ${diff}`).digest("hex");
81074
81724
  const found = worktreeService.resolve(worktreeId4);
81075
81725
  if (!found) throw new CheckpointError("invalid", `Unknown worktree "${worktreeId4}"`);
81076
81726
  const { repo, worktree } = found;
81077
- const fromHostId = worktree.hostId ?? LOCAL_HOST_ID7;
81727
+ const fromHostId = worktree.hostId ?? LOCAL_HOST_ID8;
81078
81728
  if (fromHostId === toHostId) {
81079
81729
  throw new CheckpointError("invalid", `${worktreeId4} is already on ${toHostId}.`);
81080
81730
  }
81081
- if (toHostId === LOCAL_HOST_ID7) {
81731
+ if (toHostId === LOCAL_HOST_ID8) {
81082
81732
  throw new CheckpointError(
81083
81733
  "invalid",
81084
81734
  "A worktree can be restored on a worker, not on the hub's own machine."
@@ -81111,7 +81761,7 @@ ${diff}`).digest("hex");
81111
81761
  }
81112
81762
  }
81113
81763
  if (repo.worktrees.some(
81114
- (w2) => w2.name === worktree.name && (w2.hostId ?? LOCAL_HOST_ID7) === toHostId
81764
+ (w2) => w2.name === worktree.name && (w2.hostId ?? LOCAL_HOST_ID8) === toHostId
81115
81765
  )) {
81116
81766
  throw new CheckpointError(
81117
81767
  "conflict",
@@ -81250,11 +81900,11 @@ ${diff}`).digest("hex");
81250
81900
  }
81251
81901
  const stage = posix8.join(wipDir, "sessions", `${row.id}`);
81252
81902
  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);
81903
+ for (const file3 of files) {
81904
+ if (!isSafeSessionFile(file3)) continue;
81905
+ const target = posix8.join(stage, file3.root, file3.rel);
81256
81906
  await host.fs.mkdir(posix8.dirname(target), { recursive: true });
81257
- await host.fs.writeFile(target, Buffer.from(file2.data, "base64"));
81907
+ await host.fs.writeFile(target, Buffer.from(file3.data, "base64"));
81258
81908
  }
81259
81909
  await rpc.call(METHOD_LIFECYCLE_IMPORT_SESSIONS, { dir: stage });
81260
81910
  }
@@ -81266,8 +81916,8 @@ ${diff}`).digest("hex");
81266
81916
  const { worktreeId: worktreeId4 } = input;
81267
81917
  const found = worktreeService.resolve(worktreeId4);
81268
81918
  if (!found) throw new CheckpointError("invalid", `Unknown worktree "${worktreeId4}"`);
81269
- const fromHostId = found.worktree.hostId ?? LOCAL_HOST_ID7;
81270
- if (fromHostId === LOCAL_HOST_ID7) {
81919
+ const fromHostId = found.worktree.hostId ?? LOCAL_HOST_ID8;
81920
+ if (fromHostId === LOCAL_HOST_ID8) {
81271
81921
  throw new CheckpointError("invalid", "Only worktrees on a worker can be moved.");
81272
81922
  }
81273
81923
  if (!this.sessions.get(fromHostId)?.attached) {
@@ -81335,7 +81985,7 @@ ${diff}`).digest("hex");
81335
81985
  for (const repo of loadState().repos) {
81336
81986
  const clone3 = hostRegistry.repoPathOn(repo.name, hostId2, "");
81337
81987
  for (const wt of repo.worktrees) {
81338
- if ((wt.hostId ?? LOCAL_HOST_ID7) !== hostId2 || wt.supersededBy) continue;
81988
+ if ((wt.hostId ?? LOCAL_HOST_ID8) !== hostId2 || wt.supersededBy) continue;
81339
81989
  if (clone3 && wt.path === clone3) continue;
81340
81990
  out.push({
81341
81991
  repo: repo.name,
@@ -81682,7 +82332,7 @@ init_token_service();
81682
82332
  init_worktree_script_service();
81683
82333
  init_worktree_service();
81684
82334
  var log51 = createLogger("follow");
81685
- var LOCAL_HOST_ID8 = "local";
82335
+ var LOCAL_HOST_ID9 = "local";
81686
82336
  var errorText4 = (err) => err instanceof Error ? err.message : String(err);
81687
82337
  function autoContinueMaxAgeMs() {
81688
82338
  const raw = Number(process.env.BAND_AUTO_CONTINUE_MAX_AGE_MS);
@@ -81697,7 +82347,7 @@ var FollowService = class {
81697
82347
  locate(worktreeId4) {
81698
82348
  const found = worktreeService.resolve(worktreeId4);
81699
82349
  if (!found) return null;
81700
- return { ...found, hostId: found.worktree.hostId ?? LOCAL_HOST_ID8 };
82350
+ return { ...found, hostId: found.worktree.hostId ?? LOCAL_HOST_ID9 };
81701
82351
  }
81702
82352
  /**
81703
82353
  * Whether the worktree is on a worker that is offline and is not asleep. A sleeping worktree
@@ -81706,7 +82356,7 @@ var FollowService = class {
81706
82356
  isOffline(worktreeId4) {
81707
82357
  const at = this.locate(worktreeId4);
81708
82358
  if (!at) return false;
81709
- if (at.hostId === LOCAL_HOST_ID8) return false;
82359
+ if (at.hostId === LOCAL_HOST_ID9) return false;
81710
82360
  if (at.worktree.supersededBy) return false;
81711
82361
  if (checkpointService.isHostOnline(at.hostId)) return false;
81712
82362
  if (ephemeralLifecycleService.stateOf(worktreeId4)) return false;
@@ -81761,7 +82411,7 @@ var FollowService = class {
81761
82411
  blocked: { reason, message },
81762
82412
  question: message
81763
82413
  });
81764
- if (hostId2 === LOCAL_HOST_ID8) {
82414
+ if (hostId2 === LOCAL_HOST_ID9) {
81765
82415
  return blocked("not-offline", "This worktree is on the hub's own machine, which is up.");
81766
82416
  }
81767
82417
  if (worktree.supersededBy) {
@@ -81793,12 +82443,12 @@ var FollowService = class {
81793
82443
  const needed = this.neededAgents(worktreeId4, opts);
81794
82444
  const clones = new Set(
81795
82445
  [...this.repos.allHostPaths().get(repo.name)?.keys() ?? []].filter(
81796
- (id) => id !== LOCAL_HOST_ID8
82446
+ (id) => id !== LOCAL_HOST_ID9
81797
82447
  )
81798
82448
  );
81799
82449
  const load = await this.loadByHost();
81800
82450
  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)
82451
+ (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
82452
  );
81803
82453
  if (candidates.length === 0) {
81804
82454
  return {
@@ -81867,7 +82517,7 @@ var FollowService = class {
81867
82517
  for (const repo of this.repos.loadAll()) {
81868
82518
  for (const wt of repo.worktrees) {
81869
82519
  if (wt.supersededBy) continue;
81870
- const id = wt.hostId ?? LOCAL_HOST_ID8;
82520
+ const id = wt.hostId ?? LOCAL_HOST_ID9;
81871
82521
  out.set(id, (out.get(id) ?? 0) + 1);
81872
82522
  }
81873
82523
  }
@@ -81883,7 +82533,7 @@ var FollowService = class {
81883
82533
  async gate(worktreeId4, opts = {}) {
81884
82534
  const stale = this.locate(worktreeId4);
81885
82535
  const next = stale?.worktree.supersededBy ? this.locate(stale.worktree.supersededBy) : null;
81886
- if (stale && next && stale.hostId !== LOCAL_HOST_ID8 && !checkpointService.isHostOnline(stale.hostId)) {
82536
+ if (stale && next && stale.hostId !== LOCAL_HOST_ID9 && !checkpointService.isHostOnline(stale.hostId)) {
81887
82537
  return { worktreeId: idOf(next) };
81888
82538
  }
81889
82539
  if (!this.isOffline(worktreeId4)) return { worktreeId: worktreeId4 };
@@ -82017,7 +82667,7 @@ var FollowService = class {
82017
82667
  async onHostReturned(hostId2) {
82018
82668
  for (const repo of this.repos.loadAll()) {
82019
82669
  for (const wt of repo.worktrees) {
82020
- if ((wt.hostId ?? LOCAL_HOST_ID8) !== hostId2 || !wt.supersededBy) continue;
82670
+ if ((wt.hostId ?? LOCAL_HOST_ID9) !== hostId2 || !wt.supersededBy) continue;
82021
82671
  const oldId = toWorktreeId(repo.name, wt.name, wt.hostId);
82022
82672
  if (wt.divergence?.state === "resolved") continue;
82023
82673
  try {
@@ -90379,17 +91029,17 @@ import { execFile as execFile13 } from "node:child_process";
90379
91029
  import {
90380
91030
  accessSync as accessSync3,
90381
91031
  constants as constants3,
90382
- existsSync as existsSync15,
91032
+ existsSync as existsSync16,
90383
91033
  lstatSync as lstatSync4,
90384
- mkdirSync as mkdirSync8,
91034
+ mkdirSync as mkdirSync9,
90385
91035
  readFileSync as readFileSync11,
90386
- realpathSync as realpathSync4,
91036
+ realpathSync as realpathSync5,
90387
91037
  symlinkSync,
90388
- unlinkSync as unlinkSync5,
91038
+ unlinkSync as unlinkSync6,
90389
91039
  writeFileSync as writeFileSync7
90390
91040
  } from "node:fs";
90391
91041
  import { homedir as homedir15, platform as platform2 } from "node:os";
90392
- import { basename as basename3, dirname as dirname12, join as join46 } from "node:path";
91042
+ import { basename as basename3, dirname as dirname13, join as join46 } from "node:path";
90393
91043
  function windowsBinDir() {
90394
91044
  const localAppData = process.env.LOCALAPPDATA || join46(homedir15(), "AppData", "Local");
90395
91045
  return join46(localAppData, "band", "bin");
@@ -90408,7 +91058,7 @@ function checkCliWindows() {
90408
91058
  if (!match) return "ConflictingBinary";
90409
91059
  const target = match[1];
90410
91060
  if (basename3(target).toLowerCase() !== "band.exe") return "ConflictingBinary";
90411
- return existsSync15(target) ? "Installed" : "NotInstalled";
91061
+ return existsSync16(target) ? "Installed" : "NotInstalled";
90412
91062
  }
90413
91063
  async function checkCli() {
90414
91064
  if (platform2() === "win32") {
@@ -90421,7 +91071,7 @@ async function checkCli() {
90421
91071
  }
90422
91072
  let target;
90423
91073
  try {
90424
- target = realpathSync4(SYMLINK_PATH);
91074
+ target = realpathSync5(SYMLINK_PATH);
90425
91075
  } catch {
90426
91076
  return "NotInstalled";
90427
91077
  }
@@ -90500,7 +91150,7 @@ async function addBinDirToUserPath(binDir) {
90500
91150
  }
90501
91151
  async function installCliWindows(binaryPath) {
90502
91152
  const binDir = windowsBinDir();
90503
- mkdirSync8(binDir, { recursive: true });
91153
+ mkdirSync9(binDir, { recursive: true });
90504
91154
  const shim = `@echo off\r
90505
91155
  "${binaryPath}" %*\r
90506
91156
  `;
@@ -90516,11 +91166,11 @@ async function installCli(_opts = {}) {
90516
91166
  await installCliWindows(binaryPath);
90517
91167
  return;
90518
91168
  }
90519
- const dir = dirname12(SYMLINK_PATH);
91169
+ const dir = dirname13(SYMLINK_PATH);
90520
91170
  if (isDirWritable(dir)) {
90521
91171
  try {
90522
91172
  lstatSync4(SYMLINK_PATH);
90523
- unlinkSync5(SYMLINK_PATH);
91173
+ unlinkSync6(SYMLINK_PATH);
90524
91174
  } catch (err) {
90525
91175
  const code = err.code;
90526
91176
  if (code !== "ENOENT") throw err;
@@ -90774,7 +91424,7 @@ function subscribeToFileChanges(worktreeId4, listener) {
90774
91424
  // src/server/services/formatter.ts
90775
91425
  init_src();
90776
91426
  init_registry();
90777
- import { basename as basename4, dirname as dirname13, isAbsolute as isAbsolute4, join as join47, resolve as resolvePath3 } from "node:path";
91427
+ import { basename as basename4, dirname as dirname14, isAbsolute as isAbsolute4, join as join47, resolve as resolvePath3 } from "node:path";
90778
91428
  import prettier from "prettier";
90779
91429
  var log56 = createLogger("formatter");
90780
91430
  var FormatterError = class extends Error {
@@ -90853,7 +91503,7 @@ async function formatFile(worktreePath, filePath, content, options = {}) {
90853
91503
  var JSON_CONFIGS = [".prettierrc", ".prettierrc.json"];
90854
91504
  async function resolveConfigOnHost(fs3, absFile, worktreePath) {
90855
91505
  const root = resolvePath3(worktreePath);
90856
- let dir = dirname13(absFile);
91506
+ let dir = dirname14(absFile);
90857
91507
  for (; ; ) {
90858
91508
  for (const name4 of JSON_CONFIGS) {
90859
91509
  const parsed = await readJson2(fs3, join47(dir, name4));
@@ -90863,8 +91513,8 @@ async function resolveConfigOnHost(fs3, absFile, worktreePath) {
90863
91513
  if (pkg && typeof pkg.prettier === "object" && pkg.prettier !== null) {
90864
91514
  return withoutPlugins(pkg.prettier);
90865
91515
  }
90866
- if (dir === root || dirname13(dir) === dir) return null;
90867
- dir = dirname13(dir);
91516
+ if (dir === root || dirname14(dir) === dir) return null;
91517
+ dir = dirname14(dir);
90868
91518
  }
90869
91519
  }
90870
91520
  function withoutPlugins(config2) {
@@ -90897,7 +91547,7 @@ async function isInsideWorktree(fs3, absFile, worktreePath) {
90897
91547
  realFile = await fs3.realpath(absFile);
90898
91548
  } catch {
90899
91549
  try {
90900
- realFile = join47(await fs3.realpath(dirname13(absFile)), basename4(absFile));
91550
+ realFile = join47(await fs3.realpath(dirname14(absFile)), basename4(absFile));
90901
91551
  } catch {
90902
91552
  realFile = absFile;
90903
91553
  }
@@ -91160,7 +91810,7 @@ init_environment_build_service();
91160
91810
  init_src4();
91161
91811
  init_dist();
91162
91812
  init_registry();
91163
- import { basename as basename5, dirname as dirname14, isAbsolute as isAbsolute6 } from "node:path";
91813
+ import { basename as basename5, dirname as dirname15, isAbsolute as isAbsolute6 } from "node:path";
91164
91814
  init_token_service();
91165
91815
  var EnvironmentService = class {
91166
91816
  /** The environment of a repo's main checkout, and which hosts meet its `requires`. */
@@ -91178,8 +91828,8 @@ var EnvironmentService = class {
91178
91828
  if (!isAbsolute6(path2)) {
91179
91829
  throw new TRPCError({ code: "BAD_REQUEST", message: "path must be absolute" });
91180
91830
  }
91181
- const isFile2 = basename5(path2) === "environment.json" && basename5(dirname14(path2)) === ".band";
91182
- return this.read(isFile2 ? dirname14(dirname14(path2)) : path2);
91831
+ const isFile2 = basename5(path2) === "environment.json" && basename5(dirname15(path2)) === ".band";
91832
+ return this.read(isFile2 ? dirname15(dirname15(path2)) : path2);
91183
91833
  }
91184
91834
  read(dir) {
91185
91835
  return hostRegistry.local.scripts.environment({ repoPath: dir, worktreePath: dir });
@@ -91571,26 +92221,83 @@ var historyRouter = t.router({
91571
92221
  })
91572
92222
  });
91573
92223
 
92224
+ // src/server/api/hooks/router.ts
92225
+ init_zod();
92226
+
91574
92227
  // src/server/services/hooks-service.ts
91575
92228
  init_registry();
91576
- function checkHooks2() {
91577
- return hostRegistry.local.agentEnv.hooksStatus();
92229
+ init_token_service();
92230
+ var LOCAL_HOST_ID10 = "local";
92231
+ function hostOf2(hostId2) {
92232
+ return hostRegistry.hostById(hostId2);
91578
92233
  }
91579
- function installHooks2() {
91580
- return hostRegistry.local.agentEnv.installHooks();
92234
+ function checkHooks2(hostId2 = LOCAL_HOST_ID10) {
92235
+ return hostOf2(hostId2).agentEnv.hooksStatus();
92236
+ }
92237
+ function installHooks2(hostId2 = LOCAL_HOST_ID10) {
92238
+ return hostOf2(hostId2).agentEnv.installHooks();
92239
+ }
92240
+ function removeHooks2(hostId2 = LOCAL_HOST_ID10) {
92241
+ return hostOf2(hostId2).agentEnv.removeHooks();
92242
+ }
92243
+ async function claudeCodeOn(hostId2, agents) {
92244
+ if (hostId2 !== LOCAL_HOST_ID10) return agents.includes("claude-code");
92245
+ try {
92246
+ return typeof await hostOf2(hostId2).acp.resolveLaunch({ type: "claude-code" }) !== "string";
92247
+ } catch {
92248
+ return false;
92249
+ }
92250
+ }
92251
+ async function listHostHooks() {
92252
+ const hosts2 = tokenService.listHosts().filter((h) => h.id !== LOCAL_HOST_ID10 || h.usable);
92253
+ return Promise.all(
92254
+ hosts2.map(async (h) => {
92255
+ const online = h.id === LOCAL_HOST_ID10 || h.status === "online";
92256
+ const view4 = {
92257
+ hostId: h.id,
92258
+ name: h.name,
92259
+ online,
92260
+ hasClaudeCode: await claudeCodeOn(h.id, h.agents),
92261
+ status: null
92262
+ };
92263
+ if (!online) return view4;
92264
+ try {
92265
+ view4.status = await checkHooks2(h.id);
92266
+ } catch (err) {
92267
+ view4.error = err instanceof Error ? err.message : String(err);
92268
+ }
92269
+ return view4;
92270
+ })
92271
+ );
91581
92272
  }
91582
92273
 
91583
92274
  // src/server/api/hooks/router.ts
92275
+ var hostInput = external_exports2.object({ hostId: external_exports2.string().optional() }).default({});
92276
+ function failure(err) {
92277
+ return new Error(err instanceof Error ? err.message : String(err));
92278
+ }
91584
92279
  var hooksRouter = t.router({
91585
- check: publicProcedure.query(async () => {
91586
- return await checkHooks2();
92280
+ /** The hub's own host. */
92281
+ check: adminProcedure.input(hostInput).query(async ({ input }) => {
92282
+ return await checkHooks2(input.hostId);
91587
92283
  }),
91588
- install: publicProcedure.mutation(async () => {
92284
+ /** Every host with its hooks status. */
92285
+ hosts: adminProcedure.query(async () => ({ hosts: await listHostHooks() })),
92286
+ install: adminProcedure.input(hostInput).mutation(async ({ input }) => {
91589
92287
  try {
91590
- await installHooks2();
92288
+ await installHooks2(input.hostId);
91591
92289
  return { ok: true };
91592
92290
  } catch (err) {
91593
- throw new Error(err instanceof Error ? err.message : String(err));
92291
+ throw failure(err);
92292
+ }
92293
+ }),
92294
+ /** Removes Band's entries and leaves every other hook and setting. */
92295
+ remove: adminProcedure.input(hostInput).mutation(async ({ input }) => {
92296
+ try {
92297
+ await removeHooks2(input.hostId);
92298
+ return { ok: true };
92299
+ } catch (err) {
92300
+ throw failure(err);
91594
92301
  }
91595
92302
  })
91596
92303
  });
@@ -91724,6 +92431,17 @@ var RELAY_PROCEDURES = /* @__PURE__ */ new Set([
91724
92431
  "statuses.notify",
91725
92432
  "statuses.clearNeedsAttention",
91726
92433
  "repos.list",
92434
+ "repos.add",
92435
+ "repos.addFromWorker",
92436
+ "repos.addByUrl",
92437
+ "repos.inspectFolder",
92438
+ "repos.resolveRemote",
92439
+ "repos.gitInit",
92440
+ "repos.promoteToGit",
92441
+ "repos.update",
92442
+ "repos.updateLabel",
92443
+ "repos.reorder",
92444
+ "repos.remove",
91727
92445
  "hosts.list",
91728
92446
  "hosts.forAgents",
91729
92447
  "cronjobs.list",
@@ -91736,7 +92454,13 @@ var RELAY_PROCEDURES = /* @__PURE__ */ new Set([
91736
92454
  "worktrees.create",
91737
92455
  "worktrees.remove"
91738
92456
  ]);
91739
- var UNBATCHABLE = /* @__PURE__ */ new Set(["repos.list", "hosts.list", "worktrees.create", "worktrees.remove"]);
92457
+ var UNBATCHABLE = /* @__PURE__ */ new Set([
92458
+ "repos.list",
92459
+ "hosts.list",
92460
+ "worktrees.create",
92461
+ "worktrees.remove",
92462
+ "statuses.notify"
92463
+ ]);
91740
92464
  function pinHost(input, workerId, keep) {
91741
92465
  const base = input !== null && typeof input === "object" ? input : {};
91742
92466
  return keep && typeof base.hostId === "string" ? base : { ...base, hostId: workerId };
@@ -91749,7 +92473,7 @@ function pinWorktreeHost(request, workerId) {
91749
92473
  } catch {
91750
92474
  return request;
91751
92475
  }
91752
- const isTrpc = call === "/trpc/worktrees.create" || call === "/trpc/worktrees.remove";
92476
+ const isTrpc = call === "/trpc/worktrees.create" || call === "/trpc/worktrees.remove" || call === "/trpc/statuses.notify";
91753
92477
  const isMcp = call === "/mcp";
91754
92478
  if (!isTrpc && !isMcp) return request;
91755
92479
  const parsed = parseJson(bodyText(request.body));
@@ -91903,7 +92627,7 @@ function checkNamed(input, workerId, lookups) {
91903
92627
  if (worktree === null || lookups.hostOfWorktree(worktree) !== workerId) return outside;
91904
92628
  }
91905
92629
  for (const cwd of named.cwds) {
91906
- const worktree = lookups.worktreeOfCwd(path.resolve(cwd));
92630
+ const worktree = lookups.worktreeOfCwd(path.resolve(cwd), workerId);
91907
92631
  if (worktree === null || lookups.hostOfWorktree(worktree) !== workerId) return outside;
91908
92632
  }
91909
92633
  for (const id of named.terminals) {
@@ -91929,8 +92653,40 @@ function checkChosenId(procedure, input, lookups) {
91929
92653
  }
91930
92654
  return { ok: true };
91931
92655
  }
92656
+ var REPO_BY_NAME_PROCEDURES = /* @__PURE__ */ new Set([
92657
+ "repos.update",
92658
+ "repos.updateLabel",
92659
+ "repos.remove",
92660
+ "repos.promoteToGit"
92661
+ ]);
92662
+ function checkRepoCall(procedure, input, workerId, lookups) {
92663
+ const record3 = input ?? {};
92664
+ if (REPO_BY_NAME_PROCEDURES.has(procedure)) {
92665
+ if (typeof record3.name !== "string") return deny(400, `${procedure} needs a repo name`);
92666
+ const refusal = lookups.repoHostRefusal(record3.name, workerId);
92667
+ return refusal === null ? { ok: true } : deny(403, `${procedure}: ${refusal}`);
92668
+ }
92669
+ if (procedure === "repos.addFromWorker") {
92670
+ const { hostId: hostId2, name: name4, allowedHostIds } = record3;
92671
+ if (typeof hostId2 !== "string") return deny(400, `${procedure} needs a hostId`);
92672
+ if (Array.isArray(allowedHostIds) && !allowedHostIds.includes(hostId2)) {
92673
+ return deny(
92674
+ 403,
92675
+ `${procedure}: The repo would be limited to hosts that exclude ${hostId2}, where the folder is`
92676
+ );
92677
+ }
92678
+ if (typeof name4 === "string") {
92679
+ const refusal = lookups.repoHostRefusal(name4, workerId);
92680
+ if (refusal !== null) return deny(403, `${procedure}: ${refusal}`);
92681
+ }
92682
+ }
92683
+ return { ok: true };
92684
+ }
91932
92685
  function checkCall(procedure, input, workerId, lookups) {
91933
92686
  if (!RELAY_PROCEDURES.has(procedure)) return deny(403, `${procedure} is not available to agents`);
92687
+ if (procedure.startsWith("repos.") && procedure !== "repos.list") {
92688
+ return checkRepoCall(procedure, input, workerId, lookups);
92689
+ }
91934
92690
  let named = input;
91935
92691
  if (CRONJOB_KEY_PROCEDURES.has(procedure)) {
91936
92692
  const { key: key2, ...rest } = input ?? {};
@@ -94768,7 +95524,7 @@ init_src();
94768
95524
  init_zod();
94769
95525
  init_registry();
94770
95526
  init_queued_message_store();
94771
- import { realpathSync as realpathSync5 } from "node:fs";
95527
+ import { realpathSync as realpathSync6 } from "node:fs";
94772
95528
  import { join as join49, resolve as resolve11, sep as sep7 } from "node:path";
94773
95529
  init_chat_service();
94774
95530
  init_state();
@@ -94786,8 +95542,8 @@ function isPathWithinUploadDir(p2) {
94786
95542
  return false;
94787
95543
  }
94788
95544
  try {
94789
- const canonicalUploadDir = realpathSync5(uploadDir);
94790
- const canonicalPath2 = realpathSync5(p2);
95545
+ const canonicalUploadDir = realpathSync6(uploadDir);
95546
+ const canonicalPath2 = realpathSync6(p2);
94791
95547
  return canonicalPath2 === canonicalUploadDir || canonicalPath2.startsWith(canonicalUploadDir + sep7);
94792
95548
  } catch {
94793
95549
  return true;
@@ -94800,38 +95556,38 @@ async function resolveQueuedFiles(chatId, worktreeId4, files) {
94800
95556
  const needsSave = [];
94801
95557
  const needsSaveIdx = [];
94802
95558
  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;
95559
+ const file3 = files[i];
95560
+ const derivedPath = file3.path ?? await uploadPathFromUrl(worktreeId4, file3.url) ?? void 0;
94805
95561
  if (derivedPath) {
94806
95562
  const inside = remote ? await isWithinUploads(worktreeId4, derivedPath) : isPathWithinUploadDir(derivedPath);
94807
95563
  if (!inside) {
94808
95564
  log57.warn(
94809
- { chatId, path: derivedPath, filename: file2.filename },
95565
+ { chatId, path: derivedPath, filename: file3.filename },
94810
95566
  "queue: dropping file with path outside uploads directory"
94811
95567
  );
94812
95568
  continue;
94813
95569
  }
94814
95570
  resolved.push({
94815
- mediaType: file2.mediaType,
94816
- url: file2.url,
95571
+ mediaType: file3.mediaType,
95572
+ url: file3.url,
94817
95573
  path: derivedPath,
94818
- ...file2.filename !== void 0 && { filename: file2.filename }
95574
+ ...file3.filename !== void 0 && { filename: file3.filename }
94819
95575
  });
94820
95576
  continue;
94821
95577
  }
94822
- if (file2.url.startsWith("data:")) {
94823
- needsSave.push(file2);
95578
+ if (file3.url.startsWith("data:")) {
95579
+ needsSave.push(file3);
94824
95580
  needsSaveIdx.push(resolved.length);
94825
95581
  resolved.push({
94826
- mediaType: file2.mediaType,
94827
- url: file2.url,
95582
+ mediaType: file3.mediaType,
95583
+ url: file3.url,
94828
95584
  path: "",
94829
- filename: file2.filename
95585
+ filename: file3.filename
94830
95586
  });
94831
95587
  continue;
94832
95588
  }
94833
95589
  log57.warn(
94834
- { chatId, url: file2.url, filename: file2.filename },
95590
+ { chatId, url: file3.url, filename: file3.filename },
94835
95591
  "queue: dropping file with no path and non-data URL \u2014 cannot recover disk path"
94836
95592
  );
94837
95593
  }
@@ -96220,7 +96976,6 @@ var settingsRouter = t.router({
96220
96976
  init_zod();
96221
96977
  init_branch_status_poller();
96222
96978
  init_state();
96223
- init_task_service();
96224
96979
  init_watcher_service();
96225
96980
  var statusesRouter = t.router({
96226
96981
  get: publicProcedure.input(external_exports2.object({ worktreeId: external_exports2.string() })).query(({ input }) => {
@@ -96249,12 +97004,12 @@ var statusesRouter = t.router({
96249
97004
  refreshed: await branchStatusPoller.refreshWorktree(input.worktreeId)
96250
97005
  })),
96251
97006
  clearNeedsAttention: publicProcedure.input(external_exports2.object({ worktreeId: external_exports2.string() })).mutation(({ input }) => {
96252
- const status = taskService.acknowledgeAttention(input.worktreeId);
97007
+ const status = acknowledgeWorktreeAttention(input.worktreeId);
96253
97008
  if (status) emit({ kind: "update", status });
96254
97009
  return { ok: true };
96255
97010
  }),
96256
- resolve: publicProcedure.input(external_exports2.object({ cwd: external_exports2.string() })).query(({ input }) => {
96257
- return { worktreeId: resolveWorktreeIdByCwd(input.cwd) };
97011
+ resolve: publicProcedure.input(external_exports2.object({ cwd: external_exports2.string(), hostId: external_exports2.string().optional() })).query(({ input }) => {
97012
+ return { worktreeId: resolveWorktreeIdByCwd(input.cwd, input.hostId) };
96258
97013
  }),
96259
97014
  /**
96260
97015
  * Agent-agnostic entry point for coding-agent lifecycle notifications
@@ -96275,7 +97030,8 @@ var statusesRouter = t.router({
96275
97030
  payload: external_exports2.record(external_exports2.string(), external_exports2.unknown()),
96276
97031
  agent: external_exports2.string().max(64).optional(),
96277
97032
  dispatch: external_exports2.string().max(64).optional(),
96278
- terminalId: external_exports2.string().max(256).optional()
97033
+ terminalId: external_exports2.string().max(256).optional(),
97034
+ hostId: external_exports2.string().max(256).optional()
96279
97035
  })
96280
97036
  ).mutation(async ({ input }) => {
96281
97037
  const status = await applyHookNotification(input);
@@ -96472,6 +97228,91 @@ var subscriptionsRouter = t.router({
96472
97228
  init_src();
96473
97229
  init_dist();
96474
97230
  init_zod();
97231
+
97232
+ // package.json
97233
+ var package_default = {
97234
+ name: "@band-app/server",
97235
+ version: "0.48.0",
97236
+ type: "module",
97237
+ types: "src/index.ts",
97238
+ exports: {
97239
+ ".": "./src/index.ts",
97240
+ "./package.json": "./package.json"
97241
+ },
97242
+ bin: {
97243
+ "band-server": "./bin/band-server.mjs"
97244
+ },
97245
+ files: [
97246
+ "dist/client",
97247
+ "dist/start-server.mjs",
97248
+ "dist/terminal-daemon.mjs",
97249
+ "dist/openapi.json",
97250
+ "dist/migrations",
97251
+ "bin"
97252
+ ],
97253
+ engines: {
97254
+ node: ">=22.5.0"
97255
+ },
97256
+ os: [
97257
+ "darwin",
97258
+ "linux",
97259
+ "win32"
97260
+ ],
97261
+ scripts: {
97262
+ dev: "NODE_ENV=development tsx watch --exclude '../web/**' start-server.ts",
97263
+ build: "./scripts/build-server.sh",
97264
+ start: "node dist/start-server.mjs",
97265
+ prepack: "node scripts/copy-ui.mjs",
97266
+ postpack: "node scripts/copy-ui.mjs --clean",
97267
+ pretest: "pnpm --filter @band-app/web build && pnpm build",
97268
+ test: "NODE_OPTIONS='--no-warnings=ExperimentalWarning' vitest run",
97269
+ 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'})"`
97270
+ },
97271
+ dependencies: {
97272
+ "@agentclientprotocol/claude-agent-acp": "0.81.2",
97273
+ "@agentclientprotocol/codex-acp": "1.13.1",
97274
+ "@agentclientprotocol/sdk": "1.5.0",
97275
+ "@vscode/ripgrep": "^1.18.0",
97276
+ fzf: "0.5.2",
97277
+ "node-pty": "^1.1.0",
97278
+ prettier: "^3.8.3",
97279
+ "typescript-language-server": "^5.1.3"
97280
+ },
97281
+ devDependencies: {
97282
+ "@band-app/coding-agent": "workspace:^",
97283
+ "@band-app/environment": "workspace:^",
97284
+ "@band-app/host-api": "workspace:^",
97285
+ "@band-app/host-local": "workspace:^",
97286
+ "@band-app/host-remote": "workspace:^",
97287
+ "@band-app/link": "workspace:^",
97288
+ "@band-app/logger": "workspace:^",
97289
+ "@band-app/plugin-api": "workspace:^",
97290
+ "@band-app/plugin-github": "workspace:^",
97291
+ "@band-app/shared": "workspace:^",
97292
+ "@modelcontextprotocol/sdk": "^1.29.0",
97293
+ "@trpc/client": "^11.12.0",
97294
+ "@trpc/openapi": "11.16.0-alpha",
97295
+ "@trpc/server": "^11.12.0",
97296
+ "@types/express": "^5.0.6",
97297
+ "@types/node": "^22.19.13",
97298
+ "@types/ws": "^8.18.0",
97299
+ "@xterm/headless": "6.1.0-beta.302",
97300
+ croner: "^10.0.1",
97301
+ "drizzle-kit": "1.0.0-rc.1",
97302
+ "drizzle-orm": "1.0.0-rc.1",
97303
+ esbuild: "^0.25.0",
97304
+ express: "^5.2.1",
97305
+ sirv: "^3.0.0",
97306
+ tsx: "^4.21.0",
97307
+ typescript: "^5.7.0",
97308
+ vite: "^7.0.0",
97309
+ vitest: "^4.0.18",
97310
+ ws: "^8.18.0",
97311
+ zod: "^3.25.67"
97312
+ }
97313
+ };
97314
+
97315
+ // src/server/api/system/router.ts
96475
97316
  init_branch_status_poller();
96476
97317
  init_state();
96477
97318
  init_tunnel_service();
@@ -96488,6 +97329,7 @@ var systemRouter = t.router({
96488
97329
  log62.debug({ tunnel }, "services.health: tunnel status");
96489
97330
  const result = {
96490
97331
  webserver: true,
97332
+ version: package_default.version,
96491
97333
  tunnel: tunnel.running,
96492
97334
  tunnel_url: tunnel.url
96493
97335
  };
@@ -97043,6 +97885,45 @@ var vaultRouter = t.router({
97043
97885
  init_dist();
97044
97886
  init_zod();
97045
97887
 
97888
+ // src/server/services/_utils/worktree-preview-link.ts
97889
+ init_state();
97890
+ import { createHmac as createHmac2, timingSafeEqual as timingSafeEqual8 } from "node:crypto";
97891
+ import { posix as posix10 } from "node:path";
97892
+ var WORKTREE_PREVIEW_PREFIX = "/api/worktree-preview/";
97893
+ var PREVIEW_TTL_MS = 60 * 60 * 1e3;
97894
+ function signingKey() {
97895
+ const token = resolveAdminToken(process.env.BAND_ADMIN_TOKEN).token;
97896
+ return createHmac2("sha256", token).update("worktree-preview-v1").digest();
97897
+ }
97898
+ function sign(worktreeId4, dir, expiresAt) {
97899
+ return createHmac2("sha256", signingKey()).update(`${worktreeId4}
97900
+ ${dir}
97901
+ ${expiresAt}`).digest("hex");
97902
+ }
97903
+ function worktreePreviewUrl(worktreeId4, path2, now = Date.now()) {
97904
+ const expiresAt = now + PREVIEW_TTL_MS;
97905
+ const dir = posix10.dirname(path2.replace(/^\/+/, ""));
97906
+ const encodedDir = Buffer.from(dir, "utf8").toString("base64url");
97907
+ const encodedPath = path2.split("/").map(encodeURIComponent).join("/");
97908
+ const grant = `${expiresAt}.${encodedDir}.${sign(worktreeId4, dir, expiresAt)}`;
97909
+ return `${WORKTREE_PREVIEW_PREFIX}${encodeURIComponent(worktreeId4)}/${grant}/${encodedPath}`;
97910
+ }
97911
+ function verifyWorktreePreviewGrant(worktreeId4, grant, filePath, now = Date.now()) {
97912
+ if (filePath.includes("\0")) return false;
97913
+ const [expiry, encodedDir, signature, ...extra] = grant.split(".");
97914
+ const expiresAt = Number(expiry);
97915
+ if (extra.length > 0 || encodedDir === void 0 || !signature || !Number.isSafeInteger(expiresAt) || expiresAt < now) {
97916
+ return false;
97917
+ }
97918
+ const dir = Buffer.from(encodedDir, "base64url").toString("utf8");
97919
+ const expected = Buffer.from(sign(worktreeId4, dir, expiresAt), "hex");
97920
+ const given = Buffer.from(signature, "hex");
97921
+ if (given.length !== expected.length || !timingSafeEqual8(given, expected)) return false;
97922
+ const target = posix10.normalize(filePath.replace(/^\/+/, ""));
97923
+ if (target === ".." || target.startsWith("../")) return false;
97924
+ return dir === "." || target === dir || target.startsWith(`${dir}/`);
97925
+ }
97926
+
97046
97927
  // src/server/services/diff-service.ts
97047
97928
  init_src2();
97048
97929
  init_src();
@@ -97158,9 +98039,9 @@ function assertWorktreeRelative(cwd, filePath) {
97158
98039
  }
97159
98040
  return target;
97160
98041
  }
97161
- async function readUntrackedFileLines(host, cwd, file2) {
98042
+ async function readUntrackedFileLines(host, cwd, file3) {
97162
98043
  try {
97163
- const content = new TextDecoder().decode(await host.fs.readFile(join50(cwd, file2)));
98044
+ const content = new TextDecoder().decode(await host.fs.readFile(join50(cwd, file3)));
97164
98045
  const lines = content.split("\n");
97165
98046
  if (lines.length > 0 && lines[lines.length - 1] === "") {
97166
98047
  lines.pop();
@@ -97450,17 +98331,17 @@ var DiffService = class {
97450
98331
  const fileStatuses = parseFileStatuses(nameStatusOutput);
97451
98332
  const untrackedFiles = untrackedOutput.trim().split("\n").filter(Boolean);
97452
98333
  const untrackedLines = await Promise.all(
97453
- untrackedFiles.map((file2) => readUntrackedFileLines(worktree.host, cwd, file2))
98334
+ untrackedFiles.map((file3) => readUntrackedFileLines(worktree.host, cwd, file3))
97454
98335
  );
97455
98336
  for (let i = 0; i < untrackedFiles.length; i++) {
97456
- const file2 = untrackedFiles[i];
98337
+ const file3 = untrackedFiles[i];
97457
98338
  const lines = untrackedLines[i];
97458
98339
  if (lines === null) continue;
97459
- diff += `diff --git a/${file2} b/${file2}
98340
+ diff += `diff --git a/${file3} b/${file3}
97460
98341
  `;
97461
98342
  diff += "new file mode 100644\n";
97462
98343
  diff += "--- /dev/null\n";
97463
- diff += `+++ b/${file2}
98344
+ diff += `+++ b/${file3}
97464
98345
  `;
97465
98346
  diff += `@@ -0,0 +1,${lines.length} @@
97466
98347
  `;
@@ -97468,7 +98349,7 @@ var DiffService = class {
97468
98349
  diff += "\n";
97469
98350
  stats.filesChanged++;
97470
98351
  stats.insertions += lines.length;
97471
- fileStatuses[file2] = "U";
98352
+ fileStatuses[file3] = "U";
97472
98353
  }
97473
98354
  return {
97474
98355
  diff,
@@ -97696,7 +98577,7 @@ var diffService = new DiffService();
97696
98577
  init_errors6();
97697
98578
  init_ephemeral_lifecycle_service();
97698
98579
  init_worktree_service();
97699
- import { dirname as dirname15, extname as extname3, join as join51, resolve as resolve13, sep as sep9 } from "node:path";
98580
+ import { dirname as dirname16, extname as extname3, join as join51, resolve as resolve13, sep as sep9 } from "node:path";
97700
98581
  var MAX_FILE_SIZE2 = 1024 * 1024;
97701
98582
  var LANG_MAP2 = {
97702
98583
  ".js": "javascript",
@@ -97837,7 +98718,7 @@ var FilesService = class {
97837
98718
  if (await exists2(fs3, target)) {
97838
98719
  throw new Error("A file or directory already exists at this path");
97839
98720
  }
97840
- await assertParentDirectory(fs3, dirname15(target), "Parent");
98721
+ await assertParentDirectory(fs3, dirname16(target), "Parent");
97841
98722
  await fs3.writeFile(target, content, { exclusive: true });
97842
98723
  return { ok: true };
97843
98724
  }
@@ -97847,7 +98728,7 @@ var FilesService = class {
97847
98728
  if (await exists2(fs3, target)) {
97848
98729
  throw new Error("A file or directory already exists at this path");
97849
98730
  }
97850
- await assertParentDirectory(fs3, dirname15(target), "Parent");
98731
+ await assertParentDirectory(fs3, dirname16(target), "Parent");
97851
98732
  await fs3.mkdir(target);
97852
98733
  return { ok: true };
97853
98734
  }
@@ -97889,7 +98770,7 @@ var FilesService = class {
97889
98770
  if (await exists2(fs3, toTarget)) {
97890
98771
  throw new Error("A file or directory already exists at the destination");
97891
98772
  }
97892
- await assertParentDirectory(fs3, dirname15(toTarget), "Destination parent");
98773
+ await assertParentDirectory(fs3, dirname16(toTarget), "Destination parent");
97893
98774
  await fs3.rename(fromTarget, toTarget);
97894
98775
  return {
97895
98776
  ok: true,
@@ -97922,7 +98803,7 @@ var FilesService = class {
97922
98803
  if (await exists2(fs3, toTarget)) {
97923
98804
  throw new Error("A file or directory already exists at the destination");
97924
98805
  }
97925
- await assertParentDirectory(fs3, dirname15(toTarget), "Destination parent");
98806
+ await assertParentDirectory(fs3, dirname16(toTarget), "Destination parent");
97926
98807
  await fs3.copy(fromTarget, toTarget, { recursive: true, exclusive: true });
97927
98808
  return {
97928
98809
  ok: true,
@@ -99408,6 +100289,13 @@ var worktreeRouter = t.router({
99408
100289
  toPath: external_exports2.string().min(1)
99409
100290
  })
99410
100291
  ).mutation(({ input }) => filesService.copyPath(input.worktreeId, input.fromPath, input.toPath)),
100292
+ /** A signed, expiring URL for one worktree file, for a browser tab that cannot send the auth header. */
100293
+ previewUrl: publicProcedure.input(external_exports2.object({ worktreeId: external_exports2.string(), path: external_exports2.string().min(1) })).query(({ input }) => {
100294
+ if (!worktreeService.resolve(input.worktreeId)) {
100295
+ throw new TRPCError({ code: "NOT_FOUND", message: "Worktree not found" });
100296
+ }
100297
+ return { url: worktreePreviewUrl(input.worktreeId, input.path) };
100298
+ }),
99411
100299
  searchFiles: publicProcedure.input(
99412
100300
  external_exports2.object({
99413
100301
  worktreeId: external_exports2.string(),
@@ -99491,6 +100379,13 @@ var worktreesRouter = t.router({
99491
100379
  throwAsTrpcError3(err);
99492
100380
  }
99493
100381
  }),
100382
+ setOrigin: publicProcedure.input(worktreeSetOriginInput).mutation(({ input }) => {
100383
+ try {
100384
+ return worktreeService.setOrigin(input);
100385
+ } catch (err) {
100386
+ throwAsTrpcError3(err);
100387
+ }
100388
+ }),
99494
100389
  gitPull: publicProcedure.input(worktreeGitInput).mutation(async ({ input }) => {
99495
100390
  try {
99496
100391
  return await worktreeService.gitPull(input);
@@ -99514,7 +100409,7 @@ var worktreesRouter = t.router({
99514
100409
  })
99515
100410
  });
99516
100411
  function throwAsTrpcError3(err) {
99517
- if (err instanceof PlainRepoError) {
100412
+ if (err instanceof PlainRepoError || err instanceof InvalidOriginError) {
99518
100413
  throw new TRPCError({ code: "BAD_REQUEST", message: err.message, cause: err });
99519
100414
  }
99520
100415
  if (err instanceof RepoHostNotAllowedError) {
@@ -99585,12 +100480,15 @@ function discoverProcedures() {
99585
100480
  for (const [path2, procedure] of Object.entries(procRecord)) {
99586
100481
  const type = procedure._def.type;
99587
100482
  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."))
100483
+ 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.
100484
+ path2.startsWith("hooks."))
99589
100485
  continue;
100486
+ if (path2 === "worktree.previewUrl") continue;
99590
100487
  if (path2 === "environment.validate") continue;
99591
100488
  if (path2 === "repos.addFromWorker") continue;
99592
100489
  if (path2 === "repos.addByUrl") continue;
99593
100490
  if (path2 === "repos.inspectFolder" || path2 === "repos.resolveRemote") continue;
100491
+ if (path2 === "worktrees.setOrigin") continue;
99594
100492
  if (path2 === "environment.build") continue;
99595
100493
  const toolName2 = `band_${path2.replace(/\./g, "_")}`;
99596
100494
  const inputs = procedure._def.inputs;
@@ -99764,6 +100662,11 @@ function crossHostRefusal(repo, targetHostId, callerHostId) {
99764
100662
  }
99765
100663
  return null;
99766
100664
  }
100665
+ function repoHostRefusal(repo, callerHostId) {
100666
+ const allowed2 = allowedHostsOf(repo);
100667
+ if (isHostAllowed(allowed2, callerHostId)) return null;
100668
+ return `${repo} is limited to other hosts (${hostNamesOf(allowed2 ?? []).join(", ")})`;
100669
+ }
99767
100670
 
99768
100671
  // src/server/api/mcp-proxy/band-tools.ts
99769
100672
  init_worktree_service();
@@ -100905,7 +101808,7 @@ async function handleOAuthCallback(req, res) {
100905
101808
  }
100906
101809
 
100907
101810
  // src/server/api/web-app-manifest.ts
100908
- import { createReadStream as createReadStream4, statSync as statSync12 } from "node:fs";
101811
+ import { createReadStream as createReadStream4, statSync as statSync14 } from "node:fs";
100909
101812
  import { join as join52 } from "node:path";
100910
101813
  var WEB_APP_MANIFEST_PATH = "/manifest.webmanifest";
100911
101814
  var ICON_FILES = ["band-192.png", "band-512.png", "apple-touch-icon.png"];
@@ -100929,7 +101832,7 @@ function handleWebAppManifest(req, res, publicDir) {
100929
101832
  }
100930
101833
  function sendFile(req, res, filePath, headers) {
100931
101834
  try {
100932
- const { size } = statSync12(filePath);
101835
+ const { size } = statSync14(filePath);
100933
101836
  res.writeHead(200, { ...headers, "Content-Length": size.toString() });
100934
101837
  if (req.method === "HEAD") res.end();
100935
101838
  else
@@ -101709,7 +102612,6 @@ async function runFirstTimeSetup() {
101709
102612
  const results = await Promise.allSettled([
101710
102613
  repoSync,
101711
102614
  ensureSettingsDefaults(),
101712
- ensureClaudeHooks(),
101713
102615
  ensureSkillsInstalled()
101714
102616
  ]);
101715
102617
  for (const r of results) {
@@ -101847,21 +102749,6 @@ function migrateAgentDefaultMode() {
101847
102749
  saveSettings({ ...settings3, agents: { ...settings3.agents, defaultMode: mode } });
101848
102750
  log73.info("Migrated cli.defaultVia to agents.defaultMode = %s", mode);
101849
102751
  }
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
- }
101864
- }
101865
102752
  async function ensureSkillsInstalled() {
101866
102753
  try {
101867
102754
  const result = await installSkills2({ log: log73 });
@@ -102013,11 +102900,12 @@ var defaultLookups = {
102013
102900
  const chat = chatService.get(id);
102014
102901
  return chat?.worktreeId ?? null;
102015
102902
  },
102016
- worktreeOfCwd: (cwd) => resolveWorktreeIdByCwd(cwd),
102903
+ worktreeOfCwd: (cwd, hostId2) => resolveWorktreeIdByCwd(cwd, hostId2),
102017
102904
  hostOfTerminal: (id) => terminalService.hostIdOf(id),
102018
102905
  worktreeOfBrowser: (id) => browserService.get(id)?.worktreeId ?? null,
102019
102906
  worktreeOfSubscription: (id) => subscriptionService.list().find((s2) => s2.id === id)?.worktreeId ?? null,
102020
- crossHostRefusal
102907
+ crossHostRefusal,
102908
+ repoHostRefusal
102021
102909
  };
102022
102910
  var WorkerRelayService = class {
102023
102911
  constructor(lookups = defaultLookups) {
@@ -102152,7 +103040,7 @@ var workerRelayService = new WorkerRelayService();
102152
103040
  init_worktree_service();
102153
103041
  var log76 = createLogger("worker-link");
102154
103042
  var repoQueries5 = new RepoQueries();
102155
- var LOCAL_HOST_ID9 = "local";
103043
+ var LOCAL_HOST_ID11 = "local";
102156
103044
  var CAPABILITY_REFRESH_MS = 3 * 6e4;
102157
103045
  var WORKER_CONNECT_PATH = "/api/workers/connect";
102158
103046
  var WORKER_EXCHANGE_PATH = "/api/workers/exchange";
@@ -102195,10 +103083,10 @@ var WorkerLinkService = class {
102195
103083
  /** Makes every known worker resolvable, so a worktree on an offline host still finds its host. */
102196
103084
  start() {
102197
103085
  for (const row of this.queries.listHosts(1e3)) {
102198
- if (row.id !== LOCAL_HOST_ID9) this.remoteHost(row.id);
103086
+ if (row.id !== LOCAL_HOST_ID11) this.remoteHost(row.id);
102199
103087
  }
102200
103088
  for (const row of this.queries.listHosts(1e3)) {
102201
- if (row.id !== LOCAL_HOST_ID9 && row.status === "online") {
103089
+ if (row.id !== LOCAL_HOST_ID11 && row.status === "online") {
102202
103090
  this.queries.setHostStatus(row.id, "offline");
102203
103091
  }
102204
103092
  }
@@ -102224,7 +103112,7 @@ var WorkerLinkService = class {
102224
103112
  this.refreshing = true;
102225
103113
  try {
102226
103114
  await Promise.allSettled(
102227
- this.queries.listHosts(1e3).filter((row) => row.id !== LOCAL_HOST_ID9 && row.status === "online").map((row) => this.refreshReport(row))
103115
+ this.queries.listHosts(1e3).filter((row) => row.id !== LOCAL_HOST_ID11 && row.status === "online").map((row) => this.refreshReport(row))
102228
103116
  );
102229
103117
  } finally {
102230
103118
  this.refreshing = false;
@@ -102383,7 +103271,7 @@ function reportKey(report2) {
102383
103271
  init_worktree_service();
102384
103272
  function logCrash(message) {
102385
103273
  try {
102386
- mkdirSync9(bandHome(), { recursive: true });
103274
+ mkdirSync10(bandHome(), { recursive: true });
102387
103275
  appendFileSync2(join55(bandHome(), "server.log"), message, "utf-8");
102388
103276
  } catch {
102389
103277
  }
@@ -102426,7 +103314,7 @@ function resolveUiDir() {
102426
103314
  resolve14(SERVER_ROOT, "..", "..", "web", "dist", "client"),
102427
103315
  join55(SERVER_ROOT, "client")
102428
103316
  ];
102429
- return candidates.find((dir) => existsSync16(join55(dir, "_shell.html"))) ?? candidates[1];
103317
+ return candidates.find((dir) => existsSync17(join55(dir, "_shell.html"))) ?? candidates[1];
102430
103318
  }
102431
103319
  var clientDir = resolveUiDir();
102432
103320
  var uiRoot = process.env.BAND_UI_ROOT ? resolve14(process.env.BAND_UI_ROOT) : resolve14(SERVER_ROOT, "..", "web");
@@ -102511,7 +103399,7 @@ function serveStaticFile(res, root, subdir, rawFilename) {
102511
103399
  }
102512
103400
  const filePath = join55(root, subdir, filename);
102513
103401
  try {
102514
- const fileStat = statSync13(filePath);
103402
+ const fileStat = statSync15(filePath);
102515
103403
  const contentType = mimeTypeFromFilename(filename);
102516
103404
  res.writeHead(200, {
102517
103405
  "Content-Type": contentType,
@@ -102526,26 +103414,26 @@ function serveStaticFile(res, root, subdir, rawFilename) {
102526
103414
  }
102527
103415
  async function serveHostedFile(res, kind, worktreeId4, rawName) {
102528
103416
  if (hostRegistry.hostFor(worktreeId4).id === hostRegistry.local.id) return false;
102529
- const file2 = await openHostedFile(kind, worktreeId4, rawName);
102530
- if (!file2) {
103417
+ const file3 = await openHostedFile(kind, worktreeId4, rawName);
103418
+ if (!file3) {
102531
103419
  res.writeHead(404);
102532
103420
  res.end("Not found");
102533
103421
  return true;
102534
103422
  }
102535
103423
  res.writeHead(200, {
102536
103424
  "Content-Type": mimeTypeFromFilename(basename7(decodeURIComponent(rawName))),
102537
- "Content-Length": file2.size.toString(),
103425
+ "Content-Length": file3.size.toString(),
102538
103426
  "Cache-Control": "private, max-age=86400"
102539
103427
  });
102540
103428
  pipeline(
102541
- Readable2.from(file2.stream, { objectMode: false, highWaterMark: 64 * 1024 }),
103429
+ Readable2.from(file3.stream, { objectMode: false, highWaterMark: 64 * 1024 }),
102542
103430
  res,
102543
103431
  () => {
102544
103432
  }
102545
103433
  );
102546
103434
  return true;
102547
103435
  }
102548
- async function serveWorktreeFile(res, worktreeId4, rawPath) {
103436
+ async function serveWorktreeFile(res, worktreeId4, rawPath, extraHeaders = {}) {
102549
103437
  const worktree = worktreeService.resolve(worktreeId4);
102550
103438
  if (!worktree) {
102551
103439
  res.writeHead(404);
@@ -102561,12 +103449,25 @@ async function serveWorktreeFile(res, worktreeId4, rawPath) {
102561
103449
  }
102562
103450
  try {
102563
103451
  const { fs: fs3 } = worktree.host;
103452
+ const [realRoot, realTarget] = await Promise.all([fs3.realpath(root), fs3.realpath(target)]);
103453
+ if (realTarget !== realRoot && !realTarget.startsWith(realRoot + sep10)) {
103454
+ res.writeHead(400);
103455
+ res.end("Bad request");
103456
+ return;
103457
+ }
102564
103458
  const fileStat = await fs3.stat(target, { followSymlinks: true });
102565
103459
  const contentType = mimeTypeFromFilename(basename7(target));
103460
+ const isHtml = contentType.startsWith("text/html");
102566
103461
  res.writeHead(200, {
102567
103462
  "Content-Type": contentType,
102568
103463
  "Content-Length": fileStat.size.toString(),
102569
- "Cache-Control": "private, no-cache"
103464
+ "Cache-Control": "private, no-cache",
103465
+ // HTML from a repo must not run with the hub's origin.
103466
+ ...isHtml ? {
103467
+ "Content-Security-Policy": "sandbox allow-scripts",
103468
+ "X-Content-Type-Options": "nosniff"
103469
+ } : {},
103470
+ ...extraHeaders
102570
103471
  });
102571
103472
  pipeline(
102572
103473
  Readable2.from(fs3.readStream(target), { objectMode: false, highWaterMark: 64 * 1024 }),
@@ -102579,6 +103480,41 @@ async function serveWorktreeFile(res, worktreeId4, rawPath) {
102579
103480
  res.end("Not found");
102580
103481
  }
102581
103482
  }
103483
+ async function handleWorktreePreview(req, res) {
103484
+ const rest = (req.url ?? "").slice(WORKTREE_PREVIEW_PREFIX.length).split("?")[0];
103485
+ const [rawId, grant, ...pathParts] = rest.split("/");
103486
+ let wId;
103487
+ let filePath;
103488
+ try {
103489
+ wId = decodeURIComponent(rawId ?? "");
103490
+ filePath = decodeURIComponent(pathParts.join("/"));
103491
+ } catch {
103492
+ res.writeHead(400);
103493
+ res.end("Bad request");
103494
+ return;
103495
+ }
103496
+ if (!wId || !grant || !filePath) {
103497
+ res.writeHead(400);
103498
+ res.end("Bad request");
103499
+ return;
103500
+ }
103501
+ if (!verifyWorktreePreviewGrant(wId, grant, filePath)) {
103502
+ res.writeHead(403);
103503
+ res.end("Forbidden");
103504
+ return;
103505
+ }
103506
+ await serveWorktreeFile(res, wId, filePath, {
103507
+ // The page runs repo-controlled scripts on the hub's origin. `sandbox`
103508
+ // without `allow-same-origin` gives it an opaque origin, so it cannot use
103509
+ // the hub's cookies, storage or tRPC. Its own assets are plain GETs under
103510
+ // the grant, and the wildcard CORS header lets scripts fetch them.
103511
+ "Content-Security-Policy": "sandbox allow-scripts allow-forms allow-popups allow-modals",
103512
+ "X-Content-Type-Options": "nosniff",
103513
+ "Referrer-Policy": "no-referrer",
103514
+ "Access-Control-Allow-Origin": "*",
103515
+ "Cache-Control": "private, no-store"
103516
+ });
103517
+ }
102582
103518
  var MAX_WEBHOOK_BODY_BYTES = 256 * 1024;
102583
103519
  var MAX_GITHUB_HOOK_BODY_BYTES = 2 * 1024 * 1024;
102584
103520
  async function handleGithubWebhook(req, res) {
@@ -102815,6 +103751,10 @@ async function main() {
102815
103751
  await handleSubscriptionWebhook(req, res, subscriptionId);
102816
103752
  return;
102817
103753
  }
103754
+ if (req.method === "GET" && req.url?.startsWith(WORKTREE_PREVIEW_PREFIX)) {
103755
+ await handleWorktreePreview(req, res);
103756
+ return;
103757
+ }
102818
103758
  if (handleAuth(req, res)) return;
102819
103759
  if (req.url?.startsWith("/api/uploads/")) {
102820
103760
  const rest = req.url.slice("/api/uploads/".length);
@@ -102882,21 +103822,33 @@ async function main() {
102882
103822
  return;
102883
103823
  }
102884
103824
  if (req.url?.startsWith("/api/worktree-file/")) {
102885
- const rest = req.url.slice("/api/worktree-file/".length);
103825
+ const queryIdx = req.url.indexOf("?");
103826
+ const rest = req.url.slice(
103827
+ "/api/worktree-file/".length,
103828
+ queryIdx === -1 ? void 0 : queryIdx
103829
+ );
102886
103830
  const slashIdx = rest.indexOf("/");
102887
103831
  if (slashIdx === -1) {
102888
103832
  res.writeHead(400);
102889
103833
  res.end("Bad request");
102890
103834
  return;
102891
103835
  }
102892
- const wId = decodeURIComponent(rest.slice(0, slashIdx));
102893
- const filePath = rest.slice(slashIdx + 1);
103836
+ let wId;
103837
+ let filePath;
103838
+ try {
103839
+ wId = decodeURIComponent(rest.slice(0, slashIdx));
103840
+ filePath = decodeURIComponent(rest.slice(slashIdx + 1));
103841
+ } catch {
103842
+ res.writeHead(400);
103843
+ res.end("Bad request");
103844
+ return;
103845
+ }
102894
103846
  if (!wId || !filePath) {
102895
103847
  res.writeHead(400);
102896
103848
  res.end("Bad request");
102897
103849
  return;
102898
103850
  }
102899
- await serveWorktreeFile(res, wId, decodeURIComponent(filePath));
103851
+ await serveWorktreeFile(res, wId, filePath);
102900
103852
  return;
102901
103853
  }
102902
103854
  if (req.url?.startsWith("/api/cdp/tabs")) {
@@ -103087,7 +104039,7 @@ async function main() {
103087
104039
  }
103088
104040
  });
103089
104041
  const shellPath2 = join55(clientDir, "_shell.html");
103090
- if (!existsSync16(shellPath2)) {
104042
+ if (!existsSync17(shellPath2)) {
103091
104043
  throw new Error(
103092
104044
  `UI build not found at ${clientDir} (no _shell.html). Run \`pnpm build\` or pass --ui-dir <path>.`
103093
104045
  );