bazilion 0.9.0 → 0.11.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 (189) hide show
  1. package/dist/cli.js +1257 -150
  2. package/dist/cli.js.map +1 -1
  3. package/dist/daemon.js +3049 -471
  4. package/dist/daemon.js.map +1 -1
  5. package/dist/migrations/0001_init.sql +20 -0
  6. package/dist/web/client/assets/AgentTabs-B3hq5a3e.js +1 -0
  7. package/dist/web/client/assets/ConfigPage-BCwfgyjD.js +1 -0
  8. package/dist/web/client/assets/FieldRow-avAFLgYW.js +1 -0
  9. package/dist/web/client/assets/Page-CwNuDhWz.js +1 -0
  10. package/dist/web/client/assets/ProductionTeamPolicyEditor-B6dbpuYy.js +7 -0
  11. package/dist/web/client/assets/{ProfileCommunicationEditor-D1OZ8WE5.js → ProfileCommunicationEditor-Bf8UvmZT.js} +1 -1
  12. package/dist/web/client/assets/SectionTabs-DZzy01az.js +1 -0
  13. package/dist/web/client/assets/TeamTabs-sQBHChtd.js +1 -0
  14. package/dist/web/client/assets/TemplatesTabs-DPsuwsWB.js +1 -0
  15. package/dist/web/client/assets/_id-C9rkCclz.js +1 -0
  16. package/dist/web/client/assets/_id-D65IzC8H.js +1 -0
  17. package/dist/web/client/assets/_id-DE4IY5bL.js +1 -0
  18. package/dist/web/client/assets/_id-DYASsGQ8.js +1 -0
  19. package/dist/web/client/assets/activity-C7o6-xHg.js +1 -0
  20. package/dist/web/client/assets/agents-UroO4d25.js +1 -0
  21. package/dist/web/client/assets/agents-h8yWXQ2H.js +1 -0
  22. package/dist/web/client/assets/approvals-DKhF0YLC.js +1 -0
  23. package/dist/web/client/assets/chevron-down-BK6NPMMP.js +1 -0
  24. package/dist/web/client/assets/config-q8JDresK.js +3 -0
  25. package/dist/web/client/assets/context-CBucAoi6.js +1 -0
  26. package/dist/web/client/assets/inbox-BwoRikj1.js +1 -0
  27. package/dist/web/client/assets/index-DUXAevpZ.js +12 -0
  28. package/dist/web/client/assets/login-CNAV9Wer.js +1 -0
  29. package/dist/web/client/assets/mcp-BKdEFwHO.js +1 -0
  30. package/dist/web/client/assets/members-BDqZQh26.js +1 -0
  31. package/dist/web/client/assets/memory-DVdEVr6L.js +1 -0
  32. package/dist/web/client/assets/plus-b4yFXP0O.js +1 -0
  33. package/dist/web/client/assets/policy-DdDctqvg.js +1 -0
  34. package/dist/web/client/assets/preload-helper-mNuBjNXU.js +1 -0
  35. package/dist/web/client/assets/react-3W8R1pOO.js +1 -0
  36. package/dist/web/client/assets/routes-BEfALSZv.js +1 -0
  37. package/dist/web/client/assets/services-DZP2ybzb.js +1 -0
  38. package/dist/web/client/assets/skills-DoeIjITs.js +1 -0
  39. package/dist/web/client/assets/styles-BYM0UGdF.css +2 -0
  40. package/dist/web/client/assets/team-policy-D0jpnn2b.js +1 -0
  41. package/dist/web/client/assets/teams-DlHRnrQw.js +1 -0
  42. package/dist/web/client/assets/teams-W3j-UoJ9.js +1 -0
  43. package/dist/web/client/assets/telegram-2OdeGv4E.js +1 -0
  44. package/dist/web/client/assets/tokens-MuBfmgue.js +1 -0
  45. package/dist/web/client/assets/triggers-CX7RMQFQ.js +1 -0
  46. package/dist/web/client/assets/welcome-CvNr5IMi.js +1 -0
  47. package/dist/web/client/assets/wire-constants-BhVKOCQa.js +74 -0
  48. package/dist/web/server/assets/{AgentTabs-BOjTN7QF.js → AgentTabs-DoMDJBXH.js} +10 -7
  49. package/dist/web/server/assets/{ChatPane-Dh3qxzdb.js → ChatPane-Ba5wTsVk.js} +340 -44
  50. package/dist/web/server/assets/ConfigPage-BKlh5vOU.js +68 -0
  51. package/dist/web/server/assets/{FieldRow-DwyqycWI.js → FieldRow-B4eTegH9.js} +5 -5
  52. package/dist/web/server/assets/Page-LENulSXY.js +111 -0
  53. package/dist/web/server/assets/{ProductionTeamPolicyEditor-Bke5doyl.js → ProductionTeamPolicyEditor-Y4mzFFvR.js} +4 -4
  54. package/dist/web/server/assets/SectionTabs-BJTPr5nU.js +8 -0
  55. package/dist/web/server/assets/{Sidebar-CGmtCnYU.js → Sidebar-DsRVCszF.js} +229 -118
  56. package/dist/web/server/assets/{TeamTabs-BPxDBfqy.js → TeamTabs-zZzbP1t_.js} +9 -8
  57. package/dist/web/server/assets/{TemplatesTabs-zf4YhStd.js → TemplatesTabs-CcRxZhJe.js} +5 -4
  58. package/dist/web/server/assets/{_id-fpFQ9_Ya.js → _id-BhOB8Umi.js} +1 -1
  59. package/dist/web/server/assets/{_id-CCHA0gdR.js → _id-C3HY4lWg.js} +7 -10
  60. package/dist/web/server/assets/{_id-B6g9wmPR.js → _id-Cg7x6q3g.js} +22 -28
  61. package/dist/web/server/assets/{_id-B04VaQlh.js → _id-CwyGjJeP.js} +2 -2
  62. package/dist/web/server/assets/{_id-_nFwi9gD.js → _id-D27T85mU.js} +1 -1
  63. package/dist/web/server/assets/{_id-5lLzLwcv.js → _id-D3Ap7zZA.js} +142 -144
  64. package/dist/web/server/assets/{_id-ButXHnRH.js → _id-DinTp_7X.js} +2 -2
  65. package/dist/web/server/assets/{_id-DdC9wpNN.js → _id-Dj1yZ3zJ.js} +2 -2
  66. package/dist/web/server/assets/{_id-DkFgCIb-.js → _id-DqON-59C.js} +2 -2
  67. package/dist/web/server/assets/{_id-CUWgTqMS.js → _id-IzadFZV9.js} +1 -1
  68. package/dist/web/server/assets/{_id-C4AOhstq.js → _id-LrvXU4Nr.js} +6 -5
  69. package/dist/web/server/assets/{_id-DJkzwHjM.js → _id-tB47Vsfp.js} +1 -1
  70. package/dist/web/server/assets/{_tanstack-start-manifest_v-Div-TukY.js → _tanstack-start-manifest_v-BXauUXg6.js} +96 -51
  71. package/dist/web/server/assets/{activity-C2ZKg1Q9.js → activity-BCI4mfNS.js} +28 -30
  72. package/dist/web/server/assets/{activity-DedraoFj.js → activity-D9SMqBhy.js} +2 -2
  73. package/dist/web/server/assets/{activity-GPx8NCeB.js → activity-DohitgwW.js} +1 -1
  74. package/dist/web/server/assets/{agents-BR2qXlXF.js → agents-BPKOshR5.js} +1 -1
  75. package/dist/web/server/assets/{agents-LHowEI6S.js → agents-BY74v7fM.js} +1 -1
  76. package/dist/web/server/assets/{agents-B8kinIPW.js → agents-BvttypqB.js} +2 -2
  77. package/dist/web/server/assets/{agents-Bp1nsVs2.js → agents-C9xlWK0Z.js} +2 -2
  78. package/dist/web/server/assets/agents-CgZMHj80.js +265 -0
  79. package/dist/web/server/assets/{agents-CUQmWhTp.js → agents-DZuFEj0D.js} +59 -77
  80. package/dist/web/server/assets/{approvals-VPc0YpLN.js → approvals-BGU-XGIO.js} +1 -1
  81. package/dist/web/server/assets/{approvals-Dw2LJoSA.js → approvals-CWcFHlor.js} +2 -2
  82. package/dist/web/server/assets/{approvals-DHgARzd8.js → approvals-DCaVw1Mz.js} +116 -78
  83. package/dist/web/server/assets/{auth-l-Vf8wlu.js → auth-byfSx6Ja.js} +1 -1
  84. package/dist/web/server/assets/{config-CHaCv89D.js → config-BDCJdtj2.js} +1 -1
  85. package/dist/web/server/assets/{config-BvKctDOX.js → config-D2LAsud2.js} +28 -49
  86. package/dist/web/server/assets/{config-QvHaxGlP.js → config-TeIoyYYc.js} +2 -2
  87. package/dist/web/server/assets/{context-5lrf2ame.js → context-B-XDf6Ym.js} +1 -1
  88. package/dist/web/server/assets/{context-D3Egm-vY.js → context-BXGo5Vfi.js} +2 -2
  89. package/dist/web/server/assets/context-DrkrIWun.js +47 -0
  90. package/dist/web/server/assets/{createServerRpc-CeKECg65.js → createServerRpc-pU-IQV2Y.js} +1 -1
  91. package/dist/web/server/assets/{createSsrRpc-ZBGxhl8t.js → createSsrRpc-DFsIAfFd.js} +1 -1
  92. package/dist/web/server/assets/{empty-plugin-adapters-DKUlnwwl.js → empty-plugin-adapters-Cq4--YW-.js} +1 -1
  93. package/dist/web/server/assets/{inbox-CkiSvD14.js → inbox-B3-TRsNI.js} +1 -1
  94. package/dist/web/server/assets/{inbox-Bxb3W04M.js → inbox-BardsGyy.js} +2 -2
  95. package/dist/web/server/assets/inbox-CUIyf9MT.js +131 -0
  96. package/dist/web/server/assets/login-Bx5LEyGP.js +118 -0
  97. package/dist/web/server/assets/login-CmWvxfTK.js +136 -0
  98. package/dist/web/server/assets/{mcp-BsFKEoXt.js → mcp-2pwT9HU8.js} +1 -1
  99. package/dist/web/server/assets/{mcp-BU_kKENi.js → mcp-BJjLIdP0.js} +105 -112
  100. package/dist/web/server/assets/{mcp-CKqwG4mt.js → mcp-DIWTogHM.js} +2 -2
  101. package/dist/web/server/assets/members-CCv17cjQ.js +46 -0
  102. package/dist/web/server/assets/{members-DlGBKZNl.js → members-CW5aBe81.js} +1 -1
  103. package/dist/web/server/assets/{members-ksELRsyk.js → members-VdPwD6CE.js} +2 -2
  104. package/dist/web/server/assets/{memory-CO0jKGzJ.js → memory-Dngc13Nw.js} +1 -1
  105. package/dist/web/server/assets/memory-a8Kz57Kh.js +287 -0
  106. package/dist/web/server/assets/{memory-B5moRnnp.js → memory-pZ4pZT7p.js} +2 -2
  107. package/dist/web/server/assets/{policy-Dxs78_GO.js → policy-Bb-vEgqg.js} +1 -1
  108. package/dist/web/server/assets/{policy-Dr55y-Mh.js → policy-BcGELcc9.js} +16 -22
  109. package/dist/web/server/assets/{policy-DNcGZLOB.js → policy-C30AeFIb.js} +2 -2
  110. package/dist/web/server/assets/{router-_izWQUeG.js → router-C5_NjTsP.js} +100 -111
  111. package/dist/web/server/assets/{routes-BSSKp3Rv.js → routes-B6cMLIPk.js} +16 -13
  112. package/dist/web/server/assets/{routes-BrTOoeth.js → routes-BrA-U_Hm.js} +2 -2
  113. package/dist/web/server/assets/{routes-DOj6ljcm.js → routes-C19CMLZt.js} +2 -2
  114. package/dist/web/server/assets/services-B9yBWDX0.js +79 -0
  115. package/dist/web/server/assets/{services-Pv_V88VA.js → services-C4szdhPx.js} +2 -2
  116. package/dist/web/server/assets/{services-DhRiNeq0.js → services-wG8I06oS.js} +1 -1
  117. package/dist/web/server/assets/skills-BZdqiDig.js +358 -0
  118. package/dist/web/server/assets/{skills-l2cyQ3zS.js → skills-C-UgkuCW.js} +2 -2
  119. package/dist/web/server/assets/{skills-CFIWVg32.js → skills-a9VSWHWq.js} +1 -1
  120. package/dist/web/server/assets/{start-DlHq0RaQ.js → start-WH8WzTL_.js} +1 -1
  121. package/dist/web/server/assets/{team-policy-Csvc5EDd.js → team-policy-vPG01onj.js} +3 -3
  122. package/dist/web/server/assets/{teams-B9O-WDH5.js → teams-CpQ0YgWf.js} +38 -34
  123. package/dist/web/server/assets/{teams-35Oprf6e.js → teams-DEpQR2QA.js} +2 -2
  124. package/dist/web/server/assets/{teams-DFkN7ic7.js → teams-DqfrEoH6.js} +1 -1
  125. package/dist/web/server/assets/{teams-B5IF65p2.js → teams-MjqgsmC3.js} +1 -1
  126. package/dist/web/server/assets/{teams-DH1CvWmT.js → teams-OIkHuxy1.js} +2 -2
  127. package/dist/web/server/assets/teams-jBtvnk59.js +193 -0
  128. package/dist/web/server/assets/{telegram-C9sQFwWj.js → telegram-CBF8SJzB.js} +36 -71
  129. package/dist/web/server/assets/{telegram-BdaUIjvs.js → telegram-DNgCWebD.js} +1 -1
  130. package/dist/web/server/assets/{telegram-mTnJCOmn.js → telegram-XwyKSEtj.js} +2 -2
  131. package/dist/web/server/assets/{tokens-DdpnfV_Y.js → tokens-BnwFbV7T.js} +2 -2
  132. package/dist/web/server/assets/{tokens-BCz3PSoT.js → tokens-DPDMSN5O.js} +1 -1
  133. package/dist/web/server/assets/tokens-yyTSfBSN.js +186 -0
  134. package/dist/web/server/assets/{triggers-BcTTwMfy.js → triggers-CBl4yMyw.js} +2 -2
  135. package/dist/web/server/assets/{triggers-BcH_6PW1.js → triggers-D79NoSXk.js} +4 -3
  136. package/dist/web/server/assets/{triggers-BzJG1nN5.js → triggers-_3rdhwSN.js} +31 -6
  137. package/dist/web/server/assets/{welcome-CDGE2Sdk.js → welcome-IYHGurWx.js} +21 -13
  138. package/dist/web/server/server.js +82 -73
  139. package/dist/worker.js +1614 -153
  140. package/dist/worker.js.map +1 -1
  141. package/package.json +5 -3
  142. package/dist/web/client/assets/AgentTabs-DioGqUVR.js +0 -1
  143. package/dist/web/client/assets/ConfigTabs-pY-V0Rie.js +0 -1
  144. package/dist/web/client/assets/FieldRow-DmwAFfbF.js +0 -1
  145. package/dist/web/client/assets/ProductionTeamPolicyEditor-1a5jQqUB.js +0 -7
  146. package/dist/web/client/assets/TeamTabs-BViZ4sti.js +0 -1
  147. package/dist/web/client/assets/TemplatesTabs-DVcY9pMD.js +0 -1
  148. package/dist/web/client/assets/_id-8P7brhGy.js +0 -1
  149. package/dist/web/client/assets/_id-C3o3MHq8.js +0 -1
  150. package/dist/web/client/assets/_id-DGRrSF1e.js +0 -1
  151. package/dist/web/client/assets/_id-KnJe-aW9.js +0 -1
  152. package/dist/web/client/assets/activity-WWvJSo4t.js +0 -1
  153. package/dist/web/client/assets/agents-C32T1voV.js +0 -1
  154. package/dist/web/client/assets/agents-nH4m4gZ1.js +0 -1
  155. package/dist/web/client/assets/approvals-l8jIZjqz.js +0 -1
  156. package/dist/web/client/assets/config-C7wvi1nR.js +0 -3
  157. package/dist/web/client/assets/context-BEGWE3RC.js +0 -1
  158. package/dist/web/client/assets/inbox--4aBaAgG.js +0 -1
  159. package/dist/web/client/assets/index-BCXoh1c8.js +0 -12
  160. package/dist/web/client/assets/login-Bk2wwIHQ.js +0 -1
  161. package/dist/web/client/assets/mcp-vM7xgh9e.js +0 -1
  162. package/dist/web/client/assets/members-cPKfnrSL.js +0 -1
  163. package/dist/web/client/assets/memory-BOcsaemD.js +0 -1
  164. package/dist/web/client/assets/policy-BGLCJYvS.js +0 -1
  165. package/dist/web/client/assets/preload-helper-eOOCz1Fu.js +0 -1
  166. package/dist/web/client/assets/routes-DnrP3dwv.js +0 -1
  167. package/dist/web/client/assets/services-C2_O3ifw.js +0 -1
  168. package/dist/web/client/assets/skills-B__fLZjx.js +0 -1
  169. package/dist/web/client/assets/styles-Dn24-ZvP.css +0 -2
  170. package/dist/web/client/assets/team-policy-BdX340m4.js +0 -1
  171. package/dist/web/client/assets/teams-DZMB4_iw.js +0 -1
  172. package/dist/web/client/assets/teams-rpaYKNIb.js +0 -1
  173. package/dist/web/client/assets/telegram-B1O4Ko0V.js +0 -1
  174. package/dist/web/client/assets/tokens-D7FH9VgA.js +0 -1
  175. package/dist/web/client/assets/triggers-3PLy2_Fo.js +0 -1
  176. package/dist/web/client/assets/welcome-B-QQobzq.js +0 -1
  177. package/dist/web/client/assets/wire-constants-BY9Q9Z7d.js +0 -74
  178. package/dist/web/server/assets/ConfigTabs-CyuxzesF.js +0 -41
  179. package/dist/web/server/assets/agents-DU9Hqs58.js +0 -249
  180. package/dist/web/server/assets/context-CEr4-xiA.js +0 -49
  181. package/dist/web/server/assets/inbox-DU27iHv_.js +0 -133
  182. package/dist/web/server/assets/login-CNznncgA.js +0 -58
  183. package/dist/web/server/assets/login-CQybKk7Y.js +0 -74
  184. package/dist/web/server/assets/members-40DxRdVj.js +0 -48
  185. package/dist/web/server/assets/memory-CiTl4cN9.js +0 -289
  186. package/dist/web/server/assets/services-CHe4ihJk.js +0 -94
  187. package/dist/web/server/assets/skills-D89ND2kR.js +0 -321
  188. package/dist/web/server/assets/teams-zcm9XaQU.js +0 -174
  189. package/dist/web/server/assets/tokens-CkKqrtk-.js +0 -193
package/dist/cli.js CHANGED
@@ -6,7 +6,7 @@ import { defineCommand as defineCommand25, runCommand, showUsage } from "citty";
6
6
  // package.json
7
7
  var package_default = {
8
8
  name: "bazilion",
9
- version: "0.9.0",
9
+ version: "0.11.0",
10
10
  description: "Multi-agent runtime CLI \u2014 spawn LLM agents, manage profiles/teams/skills, and run the local daemon.",
11
11
  license: "MIT",
12
12
  repository: {
@@ -54,6 +54,7 @@ var package_default = {
54
54
  "@earendil-works/pi-coding-agent": "^0.80.6",
55
55
  "@hono/node-server": "^2.0.6",
56
56
  "@mozilla/readability": "^0.6.0",
57
+ "@playwright/browser-chromium": "^1.61.0",
57
58
  "@tanstack/react-router": "1.170.16",
58
59
  "@tobilu/qmd": "^2.5.3",
59
60
  "adm-zip": "^0.5.17",
@@ -67,6 +68,7 @@ var package_default = {
67
68
  "qrcode-terminal": "^0.12.0",
68
69
  react: "^19.2.7",
69
70
  "react-dom": "^19.2.7",
71
+ tar: "^7.5.16",
70
72
  typebox: "^1.3.0",
71
73
  undici: "^8.5.0",
72
74
  yaml: "^2.9.0"
@@ -304,7 +306,7 @@ var spawnCmd = defineCommand({
304
306
  reasoningLevel: args.reasoning,
305
307
  teamId,
306
308
  teamExpectedRevision: policy.teamPolicy.revision,
307
- placement: "open"
309
+ placement: "profile_defaults"
308
310
  };
309
311
  const response = await client.post("/api/agents", body);
310
312
  const agent = response.agent;
@@ -371,12 +373,12 @@ var listCmd = defineCommand({
371
373
  async run({ args }) {
372
374
  const client = createClient2();
373
375
  const q = args.all ? "?includeArchived=true" : "";
374
- const list3 = await client.get(`/api/agents${q}`);
375
- if (list3.length === 0) {
376
+ const list4 = await client.get(`/api/agents${q}`);
377
+ if (list4.length === 0) {
376
378
  console.log("(no agents)");
377
379
  return;
378
380
  }
379
- const rows = list3.map(
381
+ const rows = list4.map(
380
382
  (a) => args.long ? [a.id, a.status, a.name, a.profileId] : (
381
383
  // Short UUID prefix — all CLI commands accept 4+ char prefixes now.
382
384
  [a.id.slice(0, 8), a.status, a.name]
@@ -447,6 +449,74 @@ var deleteCmd = defineCommand({
447
449
  console.log(`deleted agent ${args.id}`);
448
450
  }
449
451
  });
452
+ function bashApprovalModeForTty(inputIsTty, outputIsTty) {
453
+ return inputIsTty && outputIsTty ? "interactive" : "auto_deny";
454
+ }
455
+ function buildChatRequest(message, attachments, bashApprovalMode) {
456
+ return {
457
+ message,
458
+ bashApprovalMode,
459
+ ...attachments && attachments.length > 0 ? { attachments } : {}
460
+ };
461
+ }
462
+ function terminalSafe(value) {
463
+ let safe = "";
464
+ for (const character of value) {
465
+ const code = character.charCodeAt(0);
466
+ const isControl = code <= 9 || code >= 11 && code <= 31 || code >= 127 && code <= 159;
467
+ safe += isControl ? `\\x${code.toString(16).padStart(2, "0")}` : character;
468
+ }
469
+ return safe;
470
+ }
471
+ async function promptForCommandApproval(approval, prompt) {
472
+ prompt.write(" [shell command approval required]");
473
+ prompt.write(" command:");
474
+ for (const line of approval.command.split("\n")) prompt.write(` ${terminalSafe(line)}`);
475
+ prompt.write(" risks:");
476
+ for (const risk of approval.risks) {
477
+ prompt.write(
478
+ ` - ${terminalSafe(risk.code)} (${terminalSafe(risk.severity)}): ${terminalSafe(risk.message)}`
479
+ );
480
+ }
481
+ try {
482
+ const answer = (await prompt.question(" Allow this command once? [y/N] ")).trim().toLowerCase();
483
+ return answer === "y" || answer === "yes" ? "allow" : "deny";
484
+ } catch {
485
+ prompt.write(" [approval input unavailable; denying]");
486
+ return "deny";
487
+ }
488
+ }
489
+ async function respondToCommandApproval(client, approval, prompt) {
490
+ const decision2 = prompt ? await promptForCommandApproval(approval, prompt) : "deny";
491
+ await client.post(`/api/shell-approvals/${encodeURIComponent(approval.id)}`, { decision: decision2 });
492
+ return decision2;
493
+ }
494
+ function closeDeltaLine(state) {
495
+ if (!state.inDeltaStream) return;
496
+ process.stdout.write("\n");
497
+ state.inDeltaStream = false;
498
+ }
499
+ function printCommandApprovalStatus(approval) {
500
+ switch (approval.status) {
501
+ case "allowed":
502
+ console.log(" [shell command allowed]");
503
+ break;
504
+ case "denied":
505
+ console.log(" [shell command denied]");
506
+ break;
507
+ case "auto_denied":
508
+ console.log(" [shell command auto-denied: non-interactive turn]");
509
+ break;
510
+ case "expired":
511
+ console.log(" [shell command approval expired]");
512
+ break;
513
+ case "cancelled":
514
+ console.log(" [shell command approval cancelled]");
515
+ break;
516
+ case "pending":
517
+ break;
518
+ }
519
+ }
450
520
  function printEvent(e, state) {
451
521
  switch (e.type) {
452
522
  case "assistant_delta":
@@ -493,16 +563,32 @@ function printEvent(e, state) {
493
563
  break;
494
564
  }
495
565
  }
496
- async function streamTurn(client, agentId, message, attachments) {
566
+ async function streamTurn(client, agentId, message, attachments, options = { bashApprovalMode: "auto_deny" }) {
497
567
  const state = { inDeltaStream: false };
498
- for await (const frame of client.stream("POST", `/api/agents/${agentId}/chat`, {
499
- message,
500
- ...attachments && attachments.length > 0 ? { attachments } : {}
501
- })) {
568
+ const respondedApprovalIds = /* @__PURE__ */ new Set();
569
+ for await (const frame of client.stream(
570
+ "POST",
571
+ `/api/agents/${agentId}/chat`,
572
+ buildChatRequest(message, attachments, options.bashApprovalMode)
573
+ )) {
502
574
  if (frame.kind === "event") {
503
- if (frame.event.type !== "user_message") printEvent(frame.event, state);
575
+ const event = frame.event;
576
+ if (event.type === "command_approval") {
577
+ closeDeltaLine(state);
578
+ const approval = event.approval;
579
+ if (approval.status === "pending") {
580
+ if (!respondedApprovalIds.has(approval.id)) {
581
+ respondedApprovalIds.add(approval.id);
582
+ await respondToCommandApproval(client, approval, options.approvalPrompt);
583
+ }
584
+ } else {
585
+ printCommandApprovalStatus(approval);
586
+ }
587
+ } else if (event.type !== "user_message") {
588
+ printEvent(event, state);
589
+ }
504
590
  } else if (frame.kind === "fatal") {
505
- if (state.inDeltaStream) process.stdout.write("\n");
591
+ closeDeltaLine(state);
506
592
  throw new Error(frame.error);
507
593
  }
508
594
  }
@@ -527,34 +613,78 @@ var chatCmd = defineCommand({
527
613
  async run({ args }) {
528
614
  const client = createClient2();
529
615
  const resolved = await client.get(`/api/agents/${args.id}`);
616
+ const bashApprovalMode = bashApprovalModeForTty(stdin.isTTY, stdout.isTTY);
530
617
  const attachments = [
531
618
  ...loadImages(asPaths(args.image)),
532
619
  ...loadFiles(asPaths(args.file))
533
620
  ];
534
621
  if (args.message || attachments.length > 0) {
535
- await streamTurn(client, resolved.agent.id, args.message ?? "", attachments);
622
+ const rl2 = bashApprovalMode === "interactive" ? createInterface({ input: stdin, output: stdout }) : null;
623
+ try {
624
+ await streamTurn(client, resolved.agent.id, args.message ?? "", attachments, {
625
+ bashApprovalMode,
626
+ ...rl2 ? {
627
+ approvalPrompt: {
628
+ question: (question) => rl2.question(question),
629
+ write: (line) => console.log(line)
630
+ }
631
+ } : {}
632
+ });
633
+ } finally {
634
+ rl2?.close();
635
+ }
536
636
  return;
537
637
  }
538
638
  console.log(`chatting with ${resolved.agent.name} (${resolved.model})`);
539
639
  console.log("(type /exit to quit)");
540
640
  const rl = createInterface({ input: stdin, output: stdout });
541
- rl.setPrompt("> ");
542
- rl.prompt();
543
- for await (const line of rl) {
544
- const trimmed = line.trim();
545
- if (trimmed === "/exit" || trimmed === "/quit") break;
546
- if (!trimmed) {
641
+ try {
642
+ if (bashApprovalMode === "interactive") {
643
+ while (true) {
644
+ let line;
645
+ try {
646
+ line = await rl.question("> ");
647
+ } catch {
648
+ break;
649
+ }
650
+ const trimmed = line.trim();
651
+ if (trimmed === "/exit" || trimmed === "/quit") break;
652
+ if (!trimmed) continue;
653
+ try {
654
+ await streamTurn(client, resolved.agent.id, trimmed, void 0, {
655
+ bashApprovalMode,
656
+ approvalPrompt: {
657
+ question: (question) => rl.question(question),
658
+ write: (output) => console.log(output)
659
+ }
660
+ });
661
+ } catch (err) {
662
+ console.error(`error: ${err.message}`);
663
+ }
664
+ }
665
+ } else {
666
+ rl.setPrompt("> ");
547
667
  rl.prompt();
548
- continue;
549
- }
550
- try {
551
- await streamTurn(client, resolved.agent.id, trimmed);
552
- } catch (err) {
553
- console.error(`error: ${err.message}`);
668
+ for await (const line of rl) {
669
+ const trimmed = line.trim();
670
+ if (trimmed === "/exit" || trimmed === "/quit") break;
671
+ if (!trimmed) {
672
+ rl.prompt();
673
+ continue;
674
+ }
675
+ try {
676
+ await streamTurn(client, resolved.agent.id, trimmed, void 0, {
677
+ bashApprovalMode
678
+ });
679
+ } catch (err) {
680
+ console.error(`error: ${err.message}`);
681
+ }
682
+ rl.prompt();
683
+ }
554
684
  }
555
- rl.prompt();
685
+ } finally {
686
+ rl.close();
556
687
  }
557
- rl.close();
558
688
  }
559
689
  });
560
690
  var chatResetCmd = defineCommand({
@@ -566,8 +696,8 @@ var chatResetCmd = defineCommand({
566
696
  async run({ args }) {
567
697
  if (!args.force) {
568
698
  process.stdout.write(`reset all chat history for ${args.id}? [y/N] `);
569
- const answer = await new Promise((resolve3) => {
570
- process.stdin.once("data", (d) => resolve3(String(d).trim().toLowerCase()));
699
+ const answer = await new Promise((resolve4) => {
700
+ process.stdin.once("data", (d) => resolve4(String(d).trim().toLowerCase()));
571
701
  });
572
702
  if (answer !== "y" && answer !== "yes") {
573
703
  console.log("aborted");
@@ -715,8 +845,8 @@ var chatCompactCmd = defineCommand({
715
845
  process.stdout.write(
716
846
  `compact chat history for ${args.id}? head turns become a summary; tail is preserved. [y/N] `
717
847
  );
718
- const answer = await new Promise((resolve3) => {
719
- process.stdin.once("data", (d) => resolve3(String(d).trim().toLowerCase()));
848
+ const answer = await new Promise((resolve4) => {
849
+ process.stdin.once("data", (d) => resolve4(String(d).trim().toLowerCase()));
720
850
  });
721
851
  if (answer !== "y" && answer !== "yes") {
722
852
  console.log("aborted");
@@ -819,7 +949,7 @@ var moveCmd = defineCommand({
819
949
  teamId: args.team,
820
950
  sourceExpectedRevision: source.teamPolicy.revision,
821
951
  destinationExpectedRevision: destination.teamPolicy.revision,
822
- placement: "open"
952
+ placement: "profile_defaults"
823
953
  };
824
954
  await client.patch(`/api/agents/${args.agent}/team`, body);
825
955
  console.log(`moved ${args.agent} to team ${args.team}`);
@@ -989,8 +1119,8 @@ var openaiLoginCmd = defineCommand3({
989
1119
  },
990
1120
  onPrompt: async (prompt) => {
991
1121
  process.stdout.write(`${prompt.message}: `);
992
- return new Promise((resolve3) => {
993
- process.stdin.once("data", (d) => resolve3(String(d).trim()));
1122
+ return new Promise((resolve4) => {
1123
+ process.stdin.once("data", (d) => resolve4(String(d).trim()));
994
1124
  });
995
1125
  },
996
1126
  onProgress: (msg) => {
@@ -1064,12 +1194,435 @@ var authCommand = defineCommand3({
1064
1194
  });
1065
1195
 
1066
1196
  // src/commands/backup.ts
1067
- import { spawn } from "child_process";
1068
- import { createWriteStream, existsSync as existsSync2, mkdirSync, readdirSync, rmSync } from "fs";
1069
- import { resolve } from "path";
1197
+ import { createHash as createHash3, randomUUID as randomUUID2 } from "crypto";
1198
+ import {
1199
+ chmodSync as chmodSync2,
1200
+ closeSync as closeSync2,
1201
+ copyFileSync,
1202
+ createWriteStream,
1203
+ existsSync as existsSync2,
1204
+ fsyncSync as fsyncSync2,
1205
+ lstatSync as lstatSync2,
1206
+ mkdirSync as mkdirSync2,
1207
+ mkdtempSync,
1208
+ openSync as openSync2,
1209
+ readdirSync,
1210
+ readFileSync as readFileSync4,
1211
+ renameSync as renameSync2,
1212
+ rmSync as rmSync2
1213
+ } from "fs";
1214
+ import { tmpdir } from "os";
1215
+ import { basename as basename3, dirname as dirname2, isAbsolute, parse, posix, relative, resolve as resolve2 } from "path";
1216
+ import { DatabaseSync } from "node:sqlite";
1070
1217
  import { Readable } from "stream";
1071
1218
  import { pipeline } from "stream/promises";
1072
1219
  import { defineCommand as defineCommand4 } from "citty";
1220
+ import { extract, list as list2 } from "tar";
1221
+
1222
+ // src/backup-schema.ts
1223
+ import { createHash } from "crypto";
1224
+ var CANONICAL_MIGRATION = "0001_init";
1225
+ var CANONICAL_SCHEMA_HASH = "f2da105d21bdea7bedff3ba601243c22d0a7b2d023b954c714940642152b227f";
1226
+ var CANONICAL_OBJECTS = [
1227
+ ["index", "agent_triggers_agent"],
1228
+ ["index", "agent_triggers_enabled"],
1229
+ ["index", "communication_approval_events_attempt"],
1230
+ ["index", "communication_approvals_queue"],
1231
+ ["index", "communication_approvals_teams"],
1232
+ ["index", "idx_agents_telegram_topic_id"],
1233
+ ["index", "idx_provider_models_provider"],
1234
+ ["index", "messages_policy_delivery_queue"],
1235
+ ["index", "messages_to_unread"],
1236
+ ["index", "team_policy_baseline_owner"],
1237
+ ["index", "team_policy_blocks_team_time"],
1238
+ ["index", "team_template_active_position"],
1239
+ ["index", "trigger_dispatches_claimable"],
1240
+ ["index", "trigger_dispatches_trigger_time"],
1241
+ ["index", "web_tokens_active"],
1242
+ ["table", "agent_skills"],
1243
+ ["table", "agent_triggers"],
1244
+ ["table", "agents"],
1245
+ ["table", "communication_approval_events"],
1246
+ ["table", "communication_approval_message_grants"],
1247
+ ["table", "communication_approvals"],
1248
+ ["table", "config"],
1249
+ ["table", "mcp_servers"],
1250
+ ["table", "messages"],
1251
+ ["table", "profile_communication_defaults"],
1252
+ ["table", "profile_default_skills"],
1253
+ ["table", "profiles"],
1254
+ ["table", "provider_models"],
1255
+ ["table", "provider_state"],
1256
+ ["table", "schema_migrations"],
1257
+ ["table", "secrets"],
1258
+ ["table", "skill_meta"],
1259
+ ["table", "source_slot_bindings"],
1260
+ ["table", "team_agent_state"],
1261
+ ["table", "team_policies"],
1262
+ ["table", "team_policy_block_events"],
1263
+ ["table", "team_policy_edges"],
1264
+ ["table", "team_template_edges"],
1265
+ ["table", "team_template_revision_edges"],
1266
+ ["table", "team_template_revision_slots"],
1267
+ ["table", "team_template_revisions"],
1268
+ ["table", "team_template_slots"],
1269
+ ["table", "team_templates"],
1270
+ ["table", "teams"],
1271
+ ["table", "telegram_allowed_users"],
1272
+ ["table", "template_instantiations"],
1273
+ ["table", "trigger_dispatches"],
1274
+ ["table", "web_tokens"],
1275
+ ["trigger", "create_team_policy"],
1276
+ ["trigger", "prevent_detached_team_policy_delete"],
1277
+ ["trigger", "validate_source_binding_insert"],
1278
+ ["trigger", "validate_team_agent_state_insert"],
1279
+ ["trigger", "validate_team_policy_baseline_update"],
1280
+ ["trigger", "validate_team_policy_edge_insert"],
1281
+ ["trigger", "validate_team_template_edge_insert"]
1282
+ ];
1283
+ function normalizeSql(sql) {
1284
+ return sql.replace(/\s+/g, " ").trim();
1285
+ }
1286
+ function assertCanonicalBackupSchema(db) {
1287
+ const migrations = db.prepare("SELECT version FROM schema_migrations ORDER BY version").all();
1288
+ if (migrations.length !== 1 || migrations[0]?.version !== CANONICAL_MIGRATION) {
1289
+ throw new Error(
1290
+ `schema_migrations must contain exactly ${CANONICAL_MIGRATION}; found ` + (migrations.map((row) => row.version).join(", ") || "none")
1291
+ );
1292
+ }
1293
+ const rows = db.prepare(
1294
+ `SELECT type, name, tbl_name, sql
1295
+ FROM sqlite_schema
1296
+ WHERE sql IS NOT NULL AND name NOT LIKE 'sqlite_%'`
1297
+ ).all();
1298
+ const byKey = new Map(rows.map((row) => [`${row.type}\0${row.name}`, row]));
1299
+ const canonicalKeys = new Set(CANONICAL_OBJECTS.map(([type, name]) => `${type}\0${name}`));
1300
+ for (const row of rows) {
1301
+ if (!canonicalKeys.has(`${row.type}\0${row.name}`)) {
1302
+ throw new Error(`unexpected schema ${row.type} is not canonical: ${row.name}`);
1303
+ }
1304
+ }
1305
+ const canonicalRows = [];
1306
+ for (const [type, name] of CANONICAL_OBJECTS) {
1307
+ const row = byKey.get(`${type}\0${name}`);
1308
+ if (!row) throw new Error(`required canonical schema ${type} is missing: ${name}`);
1309
+ canonicalRows.push(row);
1310
+ }
1311
+ const payload = canonicalRows.map((row) => [row.type, row.name, row.tbl_name, normalizeSql(row.sql)].join("\0")).join("\n");
1312
+ const actualHash = createHash("sha256").update(payload).digest("hex");
1313
+ if (actualHash !== CANONICAL_SCHEMA_HASH) {
1314
+ throw new Error(
1315
+ "canonical schema fingerprint does not match this Bazilion release; restore a backup created from the current clean-install schema"
1316
+ );
1317
+ }
1318
+ }
1319
+
1320
+ // src/daemon-liveness.ts
1321
+ import { createHash as createHash2, randomUUID } from "crypto";
1322
+ import {
1323
+ chmodSync,
1324
+ closeSync,
1325
+ fstatSync,
1326
+ fsyncSync,
1327
+ ftruncateSync,
1328
+ futimesSync,
1329
+ lstatSync,
1330
+ mkdirSync,
1331
+ openSync,
1332
+ readFileSync as readFileSync3,
1333
+ realpathSync,
1334
+ renameSync,
1335
+ rmSync,
1336
+ writeSync
1337
+ } from "fs";
1338
+ import { basename as basename2, dirname, join as join2, resolve } from "path";
1339
+ var DAEMON_LIVENESS_FILENAME = "daemon-runtime.json";
1340
+ var STALE_AFTER_MS = 6e4;
1341
+ var PROBE_TIMEOUT_MS = 750;
1342
+ function canonicalHomeIdentity(home) {
1343
+ const absolute = resolve(home);
1344
+ try {
1345
+ return realpathSync.native(absolute);
1346
+ } catch (error) {
1347
+ if (errorCode(error) !== "ENOENT") throw error;
1348
+ const parent = dirname(absolute);
1349
+ if (parent === absolute) throw error;
1350
+ return join2(canonicalHomeIdentity(parent), basename2(absolute));
1351
+ }
1352
+ }
1353
+ function daemonLivenessPath(home) {
1354
+ const canonicalHome = canonicalHomeIdentity(home);
1355
+ const identity = createHash2("sha256").update(canonicalHome).digest("hex").slice(0, 24);
1356
+ return join2(dirname(canonicalHome), `.bazilion-runtime-${identity}.json`);
1357
+ }
1358
+ function errorCode(error) {
1359
+ return error instanceof Error && "code" in error && typeof error.code === "string" ? error.code : void 0;
1360
+ }
1361
+ function parseRecord(raw) {
1362
+ try {
1363
+ const value = JSON.parse(raw);
1364
+ if (value.version !== 1 || typeof value.instanceId !== "string" || !value.instanceId || !Number.isSafeInteger(value.pid) || (value.pid ?? 0) <= 0 || !["daemon", "restore"].includes(value.owner ?? "") || ![
1365
+ "starting",
1366
+ "listening",
1367
+ "restoring",
1368
+ "swapping",
1369
+ "installed",
1370
+ "recovery-required"
1371
+ ].includes(value.state ?? "") || !Number.isFinite(value.startedAt)) {
1372
+ return null;
1373
+ }
1374
+ if (value.owner === "daemon" && !["starting", "listening"].includes(value.state ?? "") || value.owner === "restore" && !["restoring", "swapping", "installed", "recovery-required"].includes(value.state ?? "")) {
1375
+ return null;
1376
+ }
1377
+ if (value.owner === "restore" && ["swapping", "recovery-required"].includes(value.state ?? "") && (typeof value.recoveryPath !== "string" || !value.recoveryPath || typeof value.hadPrevious !== "boolean")) {
1378
+ return null;
1379
+ }
1380
+ if (value.owner === "daemon" && value.state === "listening" && (typeof value.host !== "string" || !value.host || !Number.isSafeInteger(value.port) || (value.port ?? 0) < 1 || (value.port ?? 0) > 65535)) {
1381
+ return null;
1382
+ }
1383
+ return value;
1384
+ } catch {
1385
+ return null;
1386
+ }
1387
+ }
1388
+ function readRecord(path) {
1389
+ try {
1390
+ const stat = lstatSync(path);
1391
+ const raw = stat.isFile() ? readFileSync3(path, "utf8") : "";
1392
+ return { raw, stat, record: stat.isFile() ? parseRecord(raw) : null };
1393
+ } catch (error) {
1394
+ if (errorCode(error) === "ENOENT") return null;
1395
+ throw error;
1396
+ }
1397
+ }
1398
+ function pidIsAlive(pid) {
1399
+ try {
1400
+ process.kill(pid, 0);
1401
+ return true;
1402
+ } catch (error) {
1403
+ return errorCode(error) !== "ESRCH";
1404
+ }
1405
+ }
1406
+ function endpointFor(record) {
1407
+ if (record.state !== "listening" || !record.host || !record.port) return null;
1408
+ const normalized = record.host.toLowerCase();
1409
+ if (normalized === "0.0.0.0") return `http://127.0.0.1:${record.port}`;
1410
+ if (normalized === "::" || normalized === "0:0:0:0:0:0:0:0") {
1411
+ return `http://[::1]:${record.port}`;
1412
+ }
1413
+ if (normalized === "localhost" || normalized === "127.0.0.1") {
1414
+ return `http://${normalized}:${record.port}`;
1415
+ }
1416
+ if (normalized === "::1" || normalized === "[::1]") {
1417
+ return `http://[::1]:${record.port}`;
1418
+ }
1419
+ return null;
1420
+ }
1421
+ async function endpointHasOwner(endpoint3, instanceId) {
1422
+ const controller = new AbortController();
1423
+ const timer = setTimeout(() => controller.abort(), PROBE_TIMEOUT_MS);
1424
+ try {
1425
+ const response = await fetch(new URL("/api/health", endpoint3), {
1426
+ signal: controller.signal
1427
+ });
1428
+ return response.headers.get("x-bazilion-daemon-instance") === instanceId;
1429
+ } catch {
1430
+ return false;
1431
+ } finally {
1432
+ clearTimeout(timer);
1433
+ }
1434
+ }
1435
+ function sameFile(left, right) {
1436
+ return left.dev === right.dev && left.ino === right.ino;
1437
+ }
1438
+ function writeRecord(fd, record) {
1439
+ const body = `${JSON.stringify(record, null, 2)}
1440
+ `;
1441
+ ftruncateSync(fd, 0);
1442
+ writeSync(fd, body, 0, "utf8");
1443
+ fsyncSync(fd);
1444
+ }
1445
+ function fsyncParentDirectory(path) {
1446
+ let directoryFd = null;
1447
+ try {
1448
+ directoryFd = openSync(dirname(path), "r");
1449
+ fsyncSync(directoryFd);
1450
+ } catch (error) {
1451
+ if (process.platform !== "win32") throw error;
1452
+ } finally {
1453
+ if (directoryFd !== null) closeSync(directoryFd);
1454
+ }
1455
+ }
1456
+ function reclaimRecord(path, existing) {
1457
+ const identity = createHash2("sha256").update(existing.record?.instanceId ?? existing.raw).update(`\0${existing.stat.dev}\0${existing.stat.ino}`).digest("hex").slice(0, 24);
1458
+ const claim = `${path}.reclaim-${identity}`;
1459
+ try {
1460
+ mkdirSync(claim, { mode: 448 });
1461
+ } catch (error) {
1462
+ if (errorCode(error) !== "EEXIST") throw error;
1463
+ return null;
1464
+ }
1465
+ const current = readRecord(path);
1466
+ if (!current || !sameFile(current.stat, existing.stat) || current.raw !== existing.raw) {
1467
+ return null;
1468
+ }
1469
+ try {
1470
+ renameSync(path, join2(claim, "stale-record.json"));
1471
+ return claim;
1472
+ } catch (error) {
1473
+ if (errorCode(error) === "ENOENT") return null;
1474
+ throw error;
1475
+ }
1476
+ }
1477
+ async function acquireHomeRestoreLock(home) {
1478
+ const targetHome = resolve(home);
1479
+ mkdirSync(dirname(targetHome), { recursive: true });
1480
+ const path = daemonLivenessPath(targetHome);
1481
+ mkdirSync(dirname(path), { recursive: true });
1482
+ const instanceId = randomUUID();
1483
+ let fd = null;
1484
+ let reclaimClaim = null;
1485
+ for (let attempt = 0; attempt < 8 && fd === null; attempt++) {
1486
+ try {
1487
+ fd = openSync(path, "wx", 384);
1488
+ break;
1489
+ } catch (error) {
1490
+ if (errorCode(error) !== "EEXIST") throw error;
1491
+ }
1492
+ const existing = readRecord(path);
1493
+ if (!existing) continue;
1494
+ const endpoint3 = existing.record ? endpointFor(existing.record) : null;
1495
+ if (endpoint3 && existing.record && await endpointHasOwner(endpoint3, existing.record.instanceId)) {
1496
+ throw new Error(
1497
+ `bazilion daemon is running at ${endpoint3} for ${targetHome} \u2014 stop it before restoring`
1498
+ );
1499
+ }
1500
+ if (!existing.record) {
1501
+ throw new Error(
1502
+ `the daemon ownership record for ${targetHome} is invalid; verify no daemon is running before removing it manually`
1503
+ );
1504
+ }
1505
+ const livePid = pidIsAlive(existing.record.pid);
1506
+ if (existing.record.owner === "restore" && ["swapping", "recovery-required"].includes(existing.record.state)) {
1507
+ if (livePid) {
1508
+ throw new Error(
1509
+ `bazilion restore process ${existing.record.pid} is ${existing.record.state} ${targetHome} \u2014 wait for it to finish`
1510
+ );
1511
+ }
1512
+ const phase = existing.record.state.replace("-", " ");
1513
+ throw new Error(
1514
+ `Bazilion restore ${phase} for ${targetHome}; recovery data is at ${existing.record.recoveryPath}. Recover the previous home, then remove ${path} manually.`
1515
+ );
1516
+ }
1517
+ if (livePid) {
1518
+ const ageMs = Math.max(0, Date.now() - existing.stat.mtimeMs);
1519
+ const staleDetail = ageMs > STALE_AFTER_MS ? " (its heartbeat is stale)" : "";
1520
+ const owner = existing.record.owner === "daemon" ? "daemon" : "restore";
1521
+ throw new Error(
1522
+ `bazilion ${owner} process ${existing.record.pid} owns ${targetHome}${staleDetail} \u2014 stop it before restoring`
1523
+ );
1524
+ }
1525
+ const claim = reclaimRecord(path, existing);
1526
+ if (claim) {
1527
+ reclaimClaim = claim;
1528
+ console.warn(`removed stale Bazilion daemon ownership record for ${targetHome}`);
1529
+ }
1530
+ }
1531
+ if (fd === null) {
1532
+ throw new Error(
1533
+ `could not safely acquire exclusive restore ownership for ${targetHome}; try again`
1534
+ );
1535
+ }
1536
+ const ownedStat = fstatSync(fd);
1537
+ const record = {
1538
+ version: 1,
1539
+ instanceId,
1540
+ pid: process.pid,
1541
+ owner: "restore",
1542
+ state: "restoring",
1543
+ startedAt: Date.now()
1544
+ };
1545
+ try {
1546
+ writeRecord(fd, record);
1547
+ fsyncParentDirectory(path);
1548
+ if (reclaimClaim) rmSync(reclaimClaim, { recursive: true, force: true });
1549
+ try {
1550
+ chmodSync(path, 384);
1551
+ } catch {
1552
+ }
1553
+ } catch (error) {
1554
+ closeSync(fd);
1555
+ fd = null;
1556
+ try {
1557
+ if (sameFile(lstatSync(path), ownedStat)) rmSync(path, { force: true });
1558
+ } catch {
1559
+ }
1560
+ throw error;
1561
+ }
1562
+ let released = false;
1563
+ const stillOwnsPath = () => {
1564
+ try {
1565
+ return sameFile(lstatSync(path), ownedStat);
1566
+ } catch {
1567
+ return false;
1568
+ }
1569
+ };
1570
+ const heartbeat = setInterval(() => {
1571
+ if (released) return;
1572
+ if (!stillOwnsPath()) {
1573
+ console.error("restore ownership was lost; exiting to protect the Bazilion database");
1574
+ process.exit(1);
1575
+ }
1576
+ try {
1577
+ const now = /* @__PURE__ */ new Date();
1578
+ futimesSync(fd, now, now);
1579
+ } catch (error) {
1580
+ console.error(
1581
+ "restore ownership heartbeat failed:",
1582
+ error instanceof Error ? error.message : error
1583
+ );
1584
+ process.exit(1);
1585
+ }
1586
+ }, 2e3);
1587
+ heartbeat.unref();
1588
+ return {
1589
+ path,
1590
+ markSwapping(recoveryPath, hadPrevious) {
1591
+ if (released || !stillOwnsPath()) throw new Error("restore ownership was lost");
1592
+ record.state = "swapping";
1593
+ record.recoveryPath = resolve(recoveryPath);
1594
+ record.hadPrevious = hadPrevious;
1595
+ writeRecord(fd, record);
1596
+ fsyncParentDirectory(path);
1597
+ },
1598
+ markInstalled() {
1599
+ if (released || !stillOwnsPath()) throw new Error("restore ownership was lost");
1600
+ record.state = "installed";
1601
+ delete record.recoveryPath;
1602
+ delete record.hadPrevious;
1603
+ writeRecord(fd, record);
1604
+ },
1605
+ markRecoveryRequired(recoveryPath, hadPrevious) {
1606
+ if (released || !stillOwnsPath()) throw new Error("restore ownership was lost");
1607
+ record.state = "recovery-required";
1608
+ record.recoveryPath = resolve(recoveryPath);
1609
+ record.hadPrevious = hadPrevious;
1610
+ writeRecord(fd, record);
1611
+ },
1612
+ release() {
1613
+ if (released) return;
1614
+ released = true;
1615
+ clearInterval(heartbeat);
1616
+ const ownsPath = stillOwnsPath();
1617
+ const retainForRecovery = ["swapping", "recovery-required"].includes(record.state);
1618
+ closeSync(fd);
1619
+ fd = null;
1620
+ if (ownsPath && !retainForRecovery) rmSync(path, { force: true });
1621
+ }
1622
+ };
1623
+ }
1624
+
1625
+ // src/commands/backup.ts
1073
1626
  var createCmd = defineCommand4({
1074
1627
  meta: { name: "create", description: "Download a tar.gz backup of ~/.bazilion from the server" },
1075
1628
  args: {
@@ -1082,7 +1635,14 @@ var createCmd = defineCommand4({
1082
1635
  async run({ args }) {
1083
1636
  const cfg = loadClientConfig();
1084
1637
  const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
1085
- const outAbs = resolve(args.output ?? `bazilion-backup-${date}.tar.gz`);
1638
+ const outAbs = resolve2(args.output ?? `bazilion-backup-${date}.tar.gz`);
1639
+ const configuredHome = resolve2(resolveCliPaths().home);
1640
+ const outputFromHome = relative(configuredHome, outAbs);
1641
+ if (!outputFromHome || !outputFromHome.startsWith("..") && !isAbsolute(outputFromHome)) {
1642
+ throw new Error(
1643
+ `backup output must be outside BAZILION_HOME (${configuredHome}) to avoid nesting backups`
1644
+ );
1645
+ }
1086
1646
  console.log(`downloading backup \u2192 ${outAbs}`);
1087
1647
  const res = await fetch(`${cfg.serverUrl}/api/backup`, {
1088
1648
  headers: { authorization: `Bearer ${cfg.token}`, origin: cfg.serverUrl }
@@ -1096,21 +1656,462 @@ var createCmd = defineCommand4({
1096
1656
  }
1097
1657
  throw new Error(`backup failed: ${err}`);
1098
1658
  }
1099
- await pipeline(Readable.fromWeb(res.body), createWriteStream(outAbs));
1659
+ const downloadDir = mkdtempSync(resolve2(tmpdir(), "bazilion-download-"));
1660
+ const partial = resolve2(downloadDir, "backup.tar.gz");
1661
+ try {
1662
+ await pipeline(
1663
+ Readable.fromWeb(res.body),
1664
+ createWriteStream(partial, { flags: "wx", mode: 384 })
1665
+ );
1666
+ await validateArchive(partial);
1667
+ try {
1668
+ renameSync2(partial, outAbs);
1669
+ } catch (error) {
1670
+ if (!(error instanceof Error) || !("code" in error) || error.code !== "EXDEV") throw error;
1671
+ const installPartial = resolve2(
1672
+ dirname2(outAbs),
1673
+ `.${basename3(outAbs)}.${randomUUID2()}.installing`
1674
+ );
1675
+ try {
1676
+ copyFileSync(partial, installPartial);
1677
+ chmodSync2(installPartial, 384);
1678
+ renameSync2(installPartial, outAbs);
1679
+ } finally {
1680
+ rmSync2(installPartial, { force: true });
1681
+ }
1682
+ }
1683
+ } finally {
1684
+ rmSync2(downloadDir, { recursive: true, force: true });
1685
+ }
1100
1686
  console.log("done");
1101
1687
  }
1102
1688
  });
1103
- async function probeServerRunning(port) {
1689
+ async function probeServerRunning(serverUrl) {
1104
1690
  try {
1105
1691
  const ctrl = new AbortController();
1106
1692
  const timer = setTimeout(() => ctrl.abort(), 500);
1107
- const res = await fetch(`http://127.0.0.1:${port}/login`, { signal: ctrl.signal });
1108
- clearTimeout(timer);
1109
- return res.status >= 200 && res.status < 600;
1693
+ try {
1694
+ const res = await fetch(new URL("/api/health", serverUrl), { signal: ctrl.signal });
1695
+ return res.status >= 200 && res.status < 600;
1696
+ } finally {
1697
+ clearTimeout(timer);
1698
+ }
1110
1699
  } catch {
1111
1700
  return false;
1112
1701
  }
1113
1702
  }
1703
+ function loopbackServerUrl(raw) {
1704
+ if (!raw) return null;
1705
+ try {
1706
+ const url = new URL(raw);
1707
+ if (!["http:", "https:"].includes(url.protocol)) return null;
1708
+ const hostname = url.hostname.toLowerCase();
1709
+ if (!["127.0.0.1", "localhost", "[::1]", "::1"].includes(hostname)) return null;
1710
+ return url.origin;
1711
+ } catch {
1712
+ return null;
1713
+ }
1714
+ }
1715
+ function configuredLoopbackUrls() {
1716
+ const urls = /* @__PURE__ */ new Set(["http://127.0.0.1:4321"]);
1717
+ const envUrl = loopbackServerUrl(process.env.BAZILION_SERVER);
1718
+ if (envUrl) urls.add(envUrl);
1719
+ try {
1720
+ const clientUrl = loopbackServerUrl(loadClientConfig().serverUrl);
1721
+ if (clientUrl) urls.add(clientUrl);
1722
+ } catch {
1723
+ }
1724
+ return [...urls];
1725
+ }
1726
+ var EXTRACTABLE_ENTRY_TYPES = /* @__PURE__ */ new Set([
1727
+ "File",
1728
+ "OldFile",
1729
+ "Directory",
1730
+ "SymbolicLink"
1731
+ ]);
1732
+ function normalizeArchivePath(rawPath) {
1733
+ if (!rawPath || rawPath.includes("\0")) throw new Error("backup contains an invalid empty path");
1734
+ if (rawPath.includes("\\")) {
1735
+ throw new Error(`backup contains a non-portable path: ${JSON.stringify(rawPath)}`);
1736
+ }
1737
+ if (rawPath.startsWith("/") || /^[A-Za-z]:/.test(rawPath)) {
1738
+ throw new Error(`backup contains an absolute path: ${JSON.stringify(rawPath)}`);
1739
+ }
1740
+ const segments = [];
1741
+ for (const segment of rawPath.split("/")) {
1742
+ if (!segment || segment === ".") continue;
1743
+ if (segment === "..") {
1744
+ throw new Error(`backup path escapes the restore directory: ${JSON.stringify(rawPath)}`);
1745
+ }
1746
+ segments.push(segment);
1747
+ }
1748
+ return segments.join("/");
1749
+ }
1750
+ function validateArchiveSymbolicLink(path, rawTarget) {
1751
+ if (!rawTarget || rawTarget.includes("\0")) {
1752
+ throw new Error(`backup contains an invalid symbolic link target: ${path}`);
1753
+ }
1754
+ const segments = path.split("/");
1755
+ const isTeamSlot = segments.length === 2 && segments[0] === "teams" && /^[a-z0-9][a-z0-9-]*$/.test(segments[1] ?? "");
1756
+ const portableTarget = rawTarget.replaceAll("\\", "/");
1757
+ const absoluteOnAnyPlatform = isAbsolute(rawTarget) || posix.isAbsolute(portableTarget) || /^[A-Za-z]:\//.test(portableTarget);
1758
+ if (absoluteOnAnyPlatform) {
1759
+ if (!isTeamSlot) {
1760
+ throw new Error(`backup absolute symbolic links are only allowed for Team slots: ${path}`);
1761
+ }
1762
+ if (!isAbsolute(rawTarget)) {
1763
+ throw new Error(`backup contains an invalid Team link target: ${path}`);
1764
+ }
1765
+ return;
1766
+ }
1767
+ if (isTeamSlot) {
1768
+ throw new Error(`backup contains an invalid Team link target: ${path}`);
1769
+ }
1770
+ if (/^[A-Za-z]:/.test(portableTarget)) {
1771
+ throw new Error(`backup contains a non-portable symbolic link target: ${path}`);
1772
+ }
1773
+ const target = posix.normalize(posix.join(posix.dirname(path), portableTarget));
1774
+ if (target === ".." || target.startsWith("../") || posix.isAbsolute(target)) {
1775
+ throw new Error(`backup symbolic link target escapes the restore directory: ${path}`);
1776
+ }
1777
+ }
1778
+ async function validateArchive(file) {
1779
+ const entries = [];
1780
+ await list2({
1781
+ file,
1782
+ strict: true,
1783
+ onentry(entry2) {
1784
+ if (entry2.meta) return;
1785
+ const path = normalizeArchivePath(entry2.path);
1786
+ if (!path) return;
1787
+ if (!EXTRACTABLE_ENTRY_TYPES.has(entry2.type)) {
1788
+ throw new Error(`backup contains unsupported ${entry2.type} entry: ${path}`);
1789
+ }
1790
+ if (entry2.type === "SymbolicLink") {
1791
+ validateArchiveSymbolicLink(path, entry2.linkpath);
1792
+ }
1793
+ entries.push({ path, type: entry2.type, linkpath: entry2.linkpath });
1794
+ }
1795
+ });
1796
+ const byPath = /* @__PURE__ */ new Map();
1797
+ for (const entry2 of entries) {
1798
+ if (byPath.has(entry2.path)) throw new Error(`backup contains a duplicate path: ${entry2.path}`);
1799
+ byPath.set(entry2.path, entry2);
1800
+ }
1801
+ const symlinks = new Set(
1802
+ entries.filter((entry2) => entry2.type === "SymbolicLink").map((entry2) => entry2.path)
1803
+ );
1804
+ for (const entry2 of entries) {
1805
+ const segments = entry2.path.split("/");
1806
+ for (let i = 1; i < segments.length; i++) {
1807
+ const ancestor = segments.slice(0, i).join("/");
1808
+ if (symlinks.has(ancestor)) {
1809
+ throw new Error(`backup entry traverses symbolic link ${ancestor}: ${entry2.path}`);
1810
+ }
1811
+ const ancestorEntry = byPath.get(ancestor);
1812
+ if (ancestorEntry && ancestorEntry.type !== "Directory") {
1813
+ throw new Error(`backup entry traverses non-directory ${ancestor}: ${entry2.path}`);
1814
+ }
1815
+ }
1816
+ }
1817
+ for (const required of ["bazilion.db", "auth.json"]) {
1818
+ const entry2 = byPath.get(required);
1819
+ if (!entry2 || entry2.type !== "File" && entry2.type !== "OldFile") {
1820
+ throw new Error(`backup is missing required regular file: ${required}`);
1821
+ }
1822
+ }
1823
+ for (const transient of [
1824
+ "bazilion.db-wal",
1825
+ "bazilion.db-shm",
1826
+ "bazilion.db-journal",
1827
+ DAEMON_LIVENESS_FILENAME
1828
+ ]) {
1829
+ if (byPath.has(transient)) {
1830
+ throw new Error(`backup contains unsafe transient SQLite file: ${transient}`);
1831
+ }
1832
+ }
1833
+ return entries;
1834
+ }
1835
+ function validateAuthFile(path) {
1836
+ let parsed;
1837
+ try {
1838
+ parsed = JSON.parse(readFileSync4(path, "utf8"));
1839
+ } catch (error) {
1840
+ const detail = error instanceof Error ? error.message : String(error);
1841
+ throw new Error(`backup auth.json is invalid: ${detail}`);
1842
+ }
1843
+ if (typeof parsed !== "object" || parsed === null || !("token" in parsed) || typeof parsed.token !== "string" || !parsed.token.trim()) {
1844
+ throw new Error("backup auth.json is invalid: token is missing");
1845
+ }
1846
+ return parsed.token;
1847
+ }
1848
+ function validateDatabase(path, authToken) {
1849
+ let db = null;
1850
+ try {
1851
+ db = new DatabaseSync(path, { readOnly: true });
1852
+ const integrity = db.prepare("PRAGMA integrity_check").all();
1853
+ if (integrity.length !== 1 || integrity[0]?.integrity_check !== "ok") {
1854
+ const detail = integrity.map((row) => row.integrity_check).join("; ") || "no result";
1855
+ throw new Error(`integrity_check failed: ${detail}`);
1856
+ }
1857
+ const foreignKeyErrors = db.prepare("PRAGMA foreign_key_check").all();
1858
+ if (foreignKeyErrors.length > 0) {
1859
+ throw new Error(`foreign_key_check failed with ${foreignKeyErrors.length} violation(s)`);
1860
+ }
1861
+ assertCanonicalBackupSchema(db);
1862
+ const tokenHash = createHash3("sha256").update(authToken).digest("hex");
1863
+ const activeBootstrap = db.prepare(
1864
+ `SELECT 1 FROM web_tokens
1865
+ WHERE token_hash = ? AND label = 'bootstrap' AND revoked_at IS NULL
1866
+ LIMIT 1`
1867
+ ).get(tokenHash);
1868
+ if (!activeBootstrap) {
1869
+ throw new Error("auth.json token does not match an active web_tokens row");
1870
+ }
1871
+ } catch (error) {
1872
+ if (error instanceof Error && error.message.startsWith("backup database is invalid:")) {
1873
+ throw error;
1874
+ }
1875
+ const detail = error instanceof Error ? error.message : String(error);
1876
+ throw new Error(`backup database is invalid: ${detail}`);
1877
+ } finally {
1878
+ db?.close();
1879
+ }
1880
+ }
1881
+ var SLUG_ID = /^[a-z0-9][a-z0-9-]*$/;
1882
+ var AGENT_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
1883
+ function validateEntityId(id, pattern, entity) {
1884
+ if (typeof id !== "string" || !pattern.test(id)) {
1885
+ throw new Error(`${entity} has an invalid canonical id: ${JSON.stringify(id)}`);
1886
+ }
1887
+ return id;
1888
+ }
1889
+ function containedChildDirectory(root, id, rowLabel) {
1890
+ const child = resolve2(root, id);
1891
+ const fromRoot = relative(root, child);
1892
+ if (!fromRoot || fromRoot.startsWith("..") || isAbsolute(fromRoot)) {
1893
+ throw new Error(`${rowLabel} directory escapes its canonical root: ${JSON.stringify(id)}`);
1894
+ }
1895
+ return child;
1896
+ }
1897
+ function requireExtractedDirectory(path, rowLabel) {
1898
+ try {
1899
+ if (lstatSync2(path).isDirectory()) return;
1900
+ } catch {
1901
+ }
1902
+ throw new Error(`${rowLabel} is missing its canonical directory in the backup`);
1903
+ }
1904
+ function requireExtractedTeamSlot(path, rowLabel) {
1905
+ try {
1906
+ const entry2 = lstatSync2(path);
1907
+ if (entry2.isDirectory() || entry2.isSymbolicLink()) return;
1908
+ } catch {
1909
+ }
1910
+ throw new Error(`${rowLabel} is missing its canonical Team slot in the backup`);
1911
+ }
1912
+ function rebaseRestoredHomeDirectories(database, payload, targetHome) {
1913
+ let db = null;
1914
+ try {
1915
+ db = new DatabaseSync(database);
1916
+ db.exec("PRAGMA foreign_keys = ON");
1917
+ const profiles = db.prepare("SELECT id FROM profiles ORDER BY id").all();
1918
+ const agents = db.prepare("SELECT id FROM agents ORDER BY id").all();
1919
+ const teams = db.prepare("SELECT id FROM teams ORDER BY id").all();
1920
+ const rewrites = [];
1921
+ for (const [table, rows, idPattern] of [
1922
+ ["profiles", profiles, SLUG_ID],
1923
+ ["agents", agents, AGENT_ID]
1924
+ ]) {
1925
+ const collection = table;
1926
+ const stagedRoot = resolve2(payload, collection);
1927
+ const targetRoot = resolve2(targetHome, collection);
1928
+ for (const row of rows) {
1929
+ const id = validateEntityId(row.id, idPattern, `${table} row`);
1930
+ const rowLabel = `${table} row ${JSON.stringify(id)}`;
1931
+ const stagedDir = containedChildDirectory(stagedRoot, id, rowLabel);
1932
+ requireExtractedDirectory(stagedDir, rowLabel);
1933
+ rewrites.push({
1934
+ table,
1935
+ id,
1936
+ dir: containedChildDirectory(targetRoot, id, rowLabel)
1937
+ });
1938
+ }
1939
+ }
1940
+ const stagedTeamsRoot = resolve2(payload, "teams");
1941
+ for (const row of teams) {
1942
+ const id = validateEntityId(row.id, SLUG_ID, "teams row");
1943
+ const rowLabel = `teams row ${JSON.stringify(id)}`;
1944
+ requireExtractedTeamSlot(containedChildDirectory(stagedTeamsRoot, id, rowLabel), rowLabel);
1945
+ }
1946
+ db.exec("PRAGMA journal_mode = DELETE");
1947
+ db.exec("PRAGMA synchronous = FULL");
1948
+ let transactionOpen = false;
1949
+ try {
1950
+ db.exec("BEGIN IMMEDIATE");
1951
+ transactionOpen = true;
1952
+ const updateProfile = db.prepare("UPDATE profiles SET dir = ? WHERE id = ?");
1953
+ const updateAgent = db.prepare("UPDATE agents SET dir = ? WHERE id = ?");
1954
+ for (const rewrite of rewrites) {
1955
+ const result = rewrite.table === "profiles" ? updateProfile.run(rewrite.dir, rewrite.id) : updateAgent.run(rewrite.dir, rewrite.id);
1956
+ if (result.changes !== 1) {
1957
+ throw new Error(`could not rebase ${rewrite.table} row ${JSON.stringify(rewrite.id)}`);
1958
+ }
1959
+ }
1960
+ const readProfile = db.prepare("SELECT dir FROM profiles WHERE id = ?");
1961
+ const readAgent = db.prepare("SELECT dir FROM agents WHERE id = ?");
1962
+ for (const rewrite of rewrites) {
1963
+ const row = rewrite.table === "profiles" ? readProfile.get(rewrite.id) : readAgent.get(rewrite.id);
1964
+ if (row?.dir !== rewrite.dir) {
1965
+ throw new Error(
1966
+ `could not verify rebased ${rewrite.table} row ${JSON.stringify(rewrite.id)}`
1967
+ );
1968
+ }
1969
+ }
1970
+ db.exec("COMMIT");
1971
+ transactionOpen = false;
1972
+ } catch (error) {
1973
+ if (transactionOpen) db.exec("ROLLBACK");
1974
+ throw error;
1975
+ }
1976
+ } catch (error) {
1977
+ const detail = error instanceof Error ? error.message : String(error);
1978
+ throw new Error(`backup database is invalid: ${detail}`);
1979
+ } finally {
1980
+ db?.close();
1981
+ }
1982
+ for (const suffix of ["-wal", "-shm", "-journal"]) {
1983
+ if (existsSync2(`${database}${suffix}`)) {
1984
+ throw new Error(`backup database is invalid: path rebasing left transient ${suffix} state`);
1985
+ }
1986
+ }
1987
+ }
1988
+ function removeRebuildableQmdIndexes(payload) {
1989
+ const teamsDir = resolve2(payload, "teams");
1990
+ if (!existsSync2(teamsDir) || !lstatSync2(teamsDir).isDirectory()) return;
1991
+ for (const team of readdirSync(teamsDir, { withFileTypes: true })) {
1992
+ if (!team.isDirectory() || team.isSymbolicLink()) continue;
1993
+ const memoryDir = resolve2(teamsDir, team.name, "memory");
1994
+ if (!existsSync2(memoryDir) || !lstatSync2(memoryDir).isDirectory()) continue;
1995
+ for (const entry2 of readdirSync(memoryDir, { withFileTypes: true })) {
1996
+ if (entry2.isFile() && entry2.name.startsWith(".qmd-index.sqlite")) {
1997
+ rmSync2(resolve2(memoryDir, entry2.name), { force: true });
1998
+ }
1999
+ }
2000
+ }
2001
+ }
2002
+ function installValidatedPayload(payload, targetHome, staging, lifecycle, operations = { rename: renameSync2 }) {
2003
+ const previous = resolve2(staging, "previous-home");
2004
+ const hadPrevious = existsSync2(targetHome);
2005
+ const targetParent = dirname2(targetHome);
2006
+ const syncDirectory = operations.fsyncDirectory ?? fsyncDirectory;
2007
+ const recoveryError = (message, recoveryPath, cause) => {
2008
+ let markerError;
2009
+ try {
2010
+ lifecycle?.markRecoveryRequired(recoveryPath, hadPrevious);
2011
+ } catch (error) {
2012
+ markerError = error;
2013
+ }
2014
+ const causeDetail = cause instanceof Error ? cause.message : String(cause);
2015
+ const markerDetail = markerError ? `; recovery marker update failed: ${markerError instanceof Error ? markerError.message : markerError}` : "";
2016
+ return new RestoreRecoveryRequiredError(
2017
+ `${message} (${causeDetail}); recovery data is at ${recoveryPath}${markerDetail}`,
2018
+ recoveryPath
2019
+ );
2020
+ };
2021
+ lifecycle?.markSwapping(previous, hadPrevious);
2022
+ if (hadPrevious) {
2023
+ try {
2024
+ operations.rename(targetHome, previous);
2025
+ } catch (moveError) {
2026
+ lifecycle?.markInstalled();
2027
+ throw moveError;
2028
+ }
2029
+ try {
2030
+ syncDirectory(targetParent);
2031
+ operations.afterPreviousMoved?.();
2032
+ } catch (postMoveError) {
2033
+ throw recoveryError(
2034
+ "the previous home moved but the swap could not continue safely",
2035
+ previous,
2036
+ postMoveError
2037
+ );
2038
+ }
2039
+ }
2040
+ try {
2041
+ operations.rename(payload, targetHome);
2042
+ } catch (installError) {
2043
+ if (hadPrevious) {
2044
+ try {
2045
+ operations.rename(previous, targetHome);
2046
+ } catch (rollbackError) {
2047
+ const installDetail = installError instanceof Error ? installError.message : String(installError);
2048
+ throw recoveryError(
2049
+ `restore install failed (${installDetail}) and rollback failed`,
2050
+ previous,
2051
+ rollbackError
2052
+ );
2053
+ }
2054
+ try {
2055
+ syncDirectory(targetParent);
2056
+ } catch (rollbackSyncError) {
2057
+ throw recoveryError(
2058
+ "the previous home was restored but its directory entry could not be made durable",
2059
+ targetHome,
2060
+ rollbackSyncError
2061
+ );
2062
+ }
2063
+ try {
2064
+ lifecycle?.markInstalled();
2065
+ } catch (phaseError) {
2066
+ throw recoveryError(
2067
+ "the previous home was restored but could not be marked safe",
2068
+ targetHome,
2069
+ phaseError
2070
+ );
2071
+ }
2072
+ } else {
2073
+ lifecycle?.markInstalled();
2074
+ }
2075
+ throw installError;
2076
+ }
2077
+ try {
2078
+ syncDirectory(targetParent);
2079
+ } catch (installSyncError) {
2080
+ throw recoveryError(
2081
+ "the restored home was installed but its directory entry could not be made durable",
2082
+ hadPrevious ? previous : targetHome,
2083
+ installSyncError
2084
+ );
2085
+ }
2086
+ try {
2087
+ lifecycle?.markInstalled();
2088
+ } catch (phaseError) {
2089
+ throw recoveryError(
2090
+ "the restored home was installed but could not be marked safe",
2091
+ hadPrevious ? previous : targetHome,
2092
+ phaseError
2093
+ );
2094
+ }
2095
+ }
2096
+ var RestoreRecoveryRequiredError = class extends Error {
2097
+ recoveryPath;
2098
+ constructor(message, recoveryPath) {
2099
+ super(message);
2100
+ this.name = "RestoreRecoveryRequiredError";
2101
+ this.recoveryPath = recoveryPath;
2102
+ }
2103
+ };
2104
+ function fsyncDirectory(path) {
2105
+ let fd = null;
2106
+ try {
2107
+ fd = openSync2(path, "r");
2108
+ fsyncSync2(fd);
2109
+ } catch (error) {
2110
+ if (process.platform !== "win32") throw error;
2111
+ } finally {
2112
+ if (fd !== null) closeSync2(fd);
2113
+ }
2114
+ }
1114
2115
  var restoreCmd = defineCommand4({
1115
2116
  meta: {
1116
2117
  name: "restore",
@@ -1129,37 +2130,91 @@ var restoreCmd = defineCommand4({
1129
2130
  }
1130
2131
  },
1131
2132
  async run({ args }) {
1132
- const file = resolve(args.file);
2133
+ const file = resolve2(args.file);
1133
2134
  if (!existsSync2(file)) throw new Error(`backup file not found: ${file}`);
1134
2135
  const paths = resolveCliPaths(args.home);
1135
- const targetHome = paths.home;
1136
- const defaultHome = !args.home && !process.env.BAZILION_HOME;
1137
- if (defaultHome && !args.force && await probeServerRunning(4321)) {
1138
- throw new Error(
1139
- "bazilion server appears to be running on :4321 \u2014 stop it first (DB corruption risk), or pass --force to override."
1140
- );
2136
+ const targetHome = resolve2(paths.home);
2137
+ if (targetHome === parse(targetHome).root) {
2138
+ throw new Error(`refusing to restore over filesystem root: ${targetHome}`);
1141
2139
  }
1142
- if (existsSync2(targetHome)) {
1143
- const entries = readdirSync(targetHome);
1144
- if (entries.length > 0) {
1145
- if (!args.force) {
2140
+ const targetParent = dirname2(targetHome);
2141
+ mkdirSync2(targetParent, { recursive: true });
2142
+ const restoreLock = await acquireHomeRestoreLock(targetHome);
2143
+ try {
2144
+ const targetsConfiguredHome = targetHome === resolve2(resolveCliPaths().home);
2145
+ if (targetsConfiguredHome) {
2146
+ for (const serverUrl of configuredLoopbackUrls()) {
2147
+ if (await probeServerRunning(serverUrl)) {
2148
+ throw new Error(
2149
+ `bazilion server appears to be running at ${serverUrl} \u2014 stop it before restoring.`
2150
+ );
2151
+ }
2152
+ }
2153
+ }
2154
+ if (existsSync2(targetHome)) {
2155
+ if (!lstatSync2(targetHome).isDirectory()) {
2156
+ throw new Error(`${targetHome} exists and is not a directory`);
2157
+ }
2158
+ const entries = readdirSync(targetHome);
2159
+ if (entries.length > 0 && !args.force) {
1146
2160
  throw new Error(
1147
2161
  `${targetHome} is not empty. Pass --force to overwrite (destroys existing data).`
1148
2162
  );
1149
2163
  }
1150
- rmSync(targetHome, { recursive: true, force: true });
1151
2164
  }
2165
+ const staging = mkdtempSync(resolve2(targetParent, ".bazilion-restore-"));
2166
+ const stagedArchive = resolve2(staging, "backup.tar.gz");
2167
+ const payload = resolve2(staging, "payload");
2168
+ let preserveStaging = false;
2169
+ try {
2170
+ copyFileSync(file, stagedArchive);
2171
+ await validateArchive(stagedArchive);
2172
+ mkdirSync2(payload);
2173
+ await extract({
2174
+ file: stagedArchive,
2175
+ cwd: payload,
2176
+ strict: true,
2177
+ preserveOwner: false,
2178
+ // The private copy has already passed complete entry-path and link
2179
+ // validation above. Preserve paths here so an intentionally absolute
2180
+ // external linked-Team target is not stripped into a different,
2181
+ // payload-relative path by npm-tar.
2182
+ preservePaths: true
2183
+ });
2184
+ const database = resolve2(payload, "bazilion.db");
2185
+ const authFile = resolve2(payload, "auth.json");
2186
+ if (!lstatSync2(database).isFile() || !lstatSync2(authFile).isFile()) {
2187
+ throw new Error("backup required files did not extract as regular files");
2188
+ }
2189
+ const authToken = validateAuthFile(authFile);
2190
+ validateDatabase(database, authToken);
2191
+ rebaseRestoredHomeDirectories(database, payload, targetHome);
2192
+ validateDatabase(database, authToken);
2193
+ removeRebuildableQmdIndexes(payload);
2194
+ console.log(`installing validated backup ${file} \u2192 ${targetHome}`);
2195
+ try {
2196
+ installValidatedPayload(payload, targetHome, staging, restoreLock);
2197
+ } catch (error) {
2198
+ if (error instanceof RestoreRecoveryRequiredError) {
2199
+ preserveStaging = true;
2200
+ }
2201
+ throw error;
2202
+ }
2203
+ } finally {
2204
+ if (!preserveStaging) {
2205
+ try {
2206
+ rmSync2(staging, { recursive: true, force: true });
2207
+ } catch (error) {
2208
+ const detail = error instanceof Error ? error.message : String(error);
2209
+ console.warn(
2210
+ `warning: could not remove restore staging directory ${staging}: ${detail}`
2211
+ );
2212
+ }
2213
+ }
2214
+ }
2215
+ } finally {
2216
+ restoreLock.release();
1152
2217
  }
1153
- mkdirSync(targetHome, { recursive: true });
1154
- console.log(`extracting ${file} \u2192 ${targetHome}`);
1155
- await new Promise((res, rej) => {
1156
- const proc = spawn("tar", ["-xzf", file, "-C", targetHome], { stdio: "inherit" });
1157
- proc.on("error", rej);
1158
- proc.on("exit", (code) => {
1159
- if (code === 0) res();
1160
- else rej(new Error(`tar exited with code ${code}`));
1161
- });
1162
- });
1163
2218
  console.log(`restored to ${targetHome}`);
1164
2219
  console.log("start the server with: bazilion serve (migrations apply automatically)");
1165
2220
  }
@@ -1388,19 +2443,19 @@ var configCommand = defineCommand6({
1388
2443
  });
1389
2444
 
1390
2445
  // src/commands/dashboard.ts
1391
- import { spawn as spawn2 } from "child_process";
2446
+ import { spawn } from "child_process";
1392
2447
  import { existsSync as existsSync3 } from "fs";
1393
2448
  import { connect } from "net";
1394
- import { join as join2 } from "path";
2449
+ import { join as join3 } from "path";
1395
2450
  import { defineCommand as defineCommand7 } from "citty";
1396
- var bundledDaemonEntry = join2(import.meta.dirname, "daemon.js");
1397
- var sourceDaemonEntry = join2(import.meta.dirname, "..", "..", "..", "daemon", "src", "index.ts");
2451
+ var bundledDaemonEntry = join3(import.meta.dirname, "daemon.js");
2452
+ var sourceDaemonEntry = join3(import.meta.dirname, "..", "..", "..", "daemon", "src", "index.ts");
1398
2453
  var daemonEntry = existsSync3(bundledDaemonEntry) ? bundledDaemonEntry : sourceDaemonEntry;
1399
- var bundledWebServerEntry = join2(import.meta.dirname, "web-server.js");
1400
- var sourceWebServerEntry = join2(import.meta.dirname, "..", "web-server.ts");
2454
+ var bundledWebServerEntry = join3(import.meta.dirname, "web-server.js");
2455
+ var sourceWebServerEntry = join3(import.meta.dirname, "..", "web-server.ts");
1401
2456
  var webServerEntry = existsSync3(bundledWebServerEntry) ? bundledWebServerEntry : sourceWebServerEntry;
1402
- var bundledWebDist = join2(import.meta.dirname, "web");
1403
- var sourceWebDist = join2(import.meta.dirname, "..", "..", "..", "web", "dist");
2457
+ var bundledWebDist = join3(import.meta.dirname, "web");
2458
+ var sourceWebDist = join3(import.meta.dirname, "..", "..", "..", "web", "dist");
1404
2459
  var webDist = existsSync3(bundledWebDist) ? bundledWebDist : sourceWebDist;
1405
2460
  function nodeArgs(entry2) {
1406
2461
  return entry2.endsWith(".ts") ? ["--import", "tsx/esm", entry2] : [entry2];
@@ -1419,12 +2474,12 @@ function probeHost(host) {
1419
2474
  return host;
1420
2475
  }
1421
2476
  function isTcpPortOpen(host, port) {
1422
- return new Promise((resolve3) => {
2477
+ return new Promise((resolve4) => {
1423
2478
  const socket = connect({ host: probeHost(host), port: Number(port) });
1424
2479
  const done = (open) => {
1425
2480
  socket.removeAllListeners();
1426
2481
  socket.destroy();
1427
- resolve3(open);
2482
+ resolve4(open);
1428
2483
  };
1429
2484
  socket.setTimeout(600);
1430
2485
  socket.once("connect", () => done(true));
@@ -1474,14 +2529,14 @@ function openBrowser(url) {
1474
2529
  const platform = process.platform;
1475
2530
  const cmd = platform === "darwin" ? "open" : platform === "win32" ? "cmd" : process.env.BROWSER || "xdg-open";
1476
2531
  const args = platform === "win32" ? ["/c", "start", "", url] : [url];
1477
- const proc = spawn2(cmd, args, { detached: true, stdio: "ignore" });
2532
+ const proc = spawn(cmd, args, { detached: true, stdio: "ignore" });
1478
2533
  proc.unref();
1479
2534
  }
1480
2535
  function spawnProcess(label, entry2, env) {
1481
2536
  if (!existsSync3(entry2)) {
1482
2537
  throw new Error(`${label} entry not found at ${entry2}`);
1483
2538
  }
1484
- return spawn2(process.execPath, nodeArgs(entry2), {
2539
+ return spawn(process.execPath, nodeArgs(entry2), {
1485
2540
  env,
1486
2541
  stdio: "inherit"
1487
2542
  });
@@ -1563,7 +2618,7 @@ var dashboardCommand = defineCommand7({
1563
2618
  `web UI port ${webPort} is already in use. Re-run with --port <free-port>.`
1564
2619
  );
1565
2620
  }
1566
- if (!existsSync3(join2(webDist, "server", "server.js"))) {
2621
+ if (!existsSync3(join3(webDist, "server", "server.js"))) {
1567
2622
  throw new Error(
1568
2623
  `bazilion web UI bundle not found at ${webDist}. Run "pnpm --filter @bazilion/web build" from a source checkout, or reinstall the published package.`
1569
2624
  );
@@ -1590,13 +2645,13 @@ var dashboardCommand = defineCommand7({
1590
2645
  if (children.length === 0) return;
1591
2646
  process.on("SIGINT", terminateChildren);
1592
2647
  process.on("SIGTERM", terminateChildren);
1593
- await new Promise((resolve3) => {
2648
+ await new Promise((resolve4) => {
1594
2649
  let remaining = children.length;
1595
2650
  for (const child of children) {
1596
2651
  child.on("close", (code) => {
1597
2652
  remaining--;
1598
2653
  if (!signalled && code && code !== 0) process.exitCode = code;
1599
- if (remaining === 0) resolve3();
2654
+ if (remaining === 0) resolve4();
1600
2655
  });
1601
2656
  }
1602
2657
  });
@@ -1687,6 +2742,36 @@ var doctorCommand = defineCommand8({
1687
2742
  ` ${r.scheduler.enabled ? "\u2713" : "-"} scheduler${r.scheduler.enabled ? ` (tick ${r.scheduler.tickMs}ms)` : " (BAZILION_SCHEDULER=off)"}`
1688
2743
  );
1689
2744
  console.log();
2745
+ console.log("agent shell security");
2746
+ if (!r.shellSecurity.ok) {
2747
+ check("valid shell-security configuration", false, r.shellSecurity.error);
2748
+ } else {
2749
+ if (r.shellSecurity.approvalMode === "dangerous") {
2750
+ console.log(" - dangerous-command approval enabled");
2751
+ console.log(
2752
+ " web and TTY CLI turns can allow once; background and non-TTY turns auto-deny"
2753
+ );
2754
+ } else {
2755
+ console.log(" - dangerous-command approval off");
2756
+ console.log(" set BAZILION_BASH_APPROVAL=dangerous to opt in");
2757
+ }
2758
+ if (r.shellSecurity.sandboxMode === "docker") {
2759
+ console.log(` - Docker sandbox configured (${r.shellSecurity.sandboxImage})`);
2760
+ console.log(
2761
+ " workspace-only writable mount \xB7 bounded inputs/skills/memory read-only \xB7 network disabled"
2762
+ );
2763
+ console.log(" host coding tools hidden");
2764
+ console.log(
2765
+ " policy syntax is valid; Docker and image availability are checked on execution"
2766
+ );
2767
+ console.log(" commands fail closed if either is unavailable");
2768
+ } else {
2769
+ console.log(" - sandbox off (agent shell and coding tools run on the host)");
2770
+ console.log(" approval is a host-execution tripwire, not a filesystem sandbox");
2771
+ console.log(" set BAZILION_BASH_SANDBOX=docker to opt in");
2772
+ }
2773
+ }
2774
+ console.log();
1690
2775
  console.log("operational counts");
1691
2776
  console.log(` ${r.triggers.active} active trigger(s), ${r.triggers.disabled} disabled`);
1692
2777
  console.log(` ${r.tokens.active} active web token(s)`);
@@ -1794,7 +2879,7 @@ var inboxCommand = defineCommand9({
1794
2879
  });
1795
2880
 
1796
2881
  // src/commands/login.ts
1797
- import { existsSync as existsSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
2882
+ import { existsSync as existsSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync2 } from "fs";
1798
2883
  import { defineCommand as defineCommand10 } from "citty";
1799
2884
  function normalizeServer(raw) {
1800
2885
  let url;
@@ -1833,7 +2918,7 @@ var loginCommand = defineCommand10({
1833
2918
  `${paths.authFile} not found. Start the daemon with "bazilion serve" first \u2014 login stores the remote there.`
1834
2919
  );
1835
2920
  }
1836
- const auth = JSON.parse(readFileSync3(paths.authFile, "utf8"));
2921
+ const auth = JSON.parse(readFileSync5(paths.authFile, "utf8"));
1837
2922
  if (args.clear) {
1838
2923
  if (!auth.remote) {
1839
2924
  console.log("no remote stored \u2014 nothing to clear");
@@ -2081,11 +3166,11 @@ var memoryCommand = defineCommand12({
2081
3166
  });
2082
3167
 
2083
3168
  // src/commands/profile.ts
2084
- import { spawn as spawn3 } from "child_process";
3169
+ import { spawn as spawn2 } from "child_process";
2085
3170
  import { randomBytes } from "crypto";
2086
- import { readFileSync as readFileSync4, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
2087
- import { tmpdir } from "os";
2088
- import { join as join3 } from "path";
3171
+ import { readFileSync as readFileSync6, rmSync as rmSync3, writeFileSync as writeFileSync3 } from "fs";
3172
+ import { tmpdir as tmpdir2 } from "os";
3173
+ import { join as join4 } from "path";
2089
3174
 
2090
3175
  // ../../packages/api-types/src/index.ts
2091
3176
  var PROFILE_FILES = [
@@ -2094,8 +3179,7 @@ var PROFILE_FILES = [
2094
3179
  "IDENTITY.md",
2095
3180
  "BOOTSTRAP.md",
2096
3181
  "AGENTS.md",
2097
- "TOOLS.md",
2098
- "HEARTBEAT.md"
3182
+ "TOOLS.md"
2099
3183
  ];
2100
3184
 
2101
3185
  // src/commands/profile.ts
@@ -2151,15 +3235,11 @@ var createCmd2 = defineCommand13({
2151
3235
  "tools-file": {
2152
3236
  type: "string",
2153
3237
  description: "Path to TOOLS.md content (optional; omit to skip)"
2154
- },
2155
- "heartbeat-file": {
2156
- type: "string",
2157
- description: "Path to HEARTBEAT.md content (optional; omit to skip)"
2158
3238
  }
2159
3239
  },
2160
3240
  async run({ args }) {
2161
3241
  const skillsMode = parseSkillsMode(args["skills-mode"]);
2162
- const readTemplate = (path) => path ? readFileSync4(path, "utf8") : void 0;
3242
+ const readTemplate = (path) => path ? readFileSync6(path, "utf8") : void 0;
2163
3243
  const body = {
2164
3244
  id: args.id,
2165
3245
  name: args.name,
@@ -2170,8 +3250,7 @@ var createCmd2 = defineCommand13({
2170
3250
  identity: readTemplate(args["identity-file"]),
2171
3251
  bootstrap: args["skip-bootstrap"] ? null : readTemplate(args["bootstrap-file"]) ?? void 0,
2172
3252
  agents: readTemplate(args["agents-file"]),
2173
- tools: readTemplate(args["tools-file"]),
2174
- heartbeat: readTemplate(args["heartbeat-file"])
3253
+ tools: readTemplate(args["tools-file"])
2175
3254
  };
2176
3255
  const client = createClient2();
2177
3256
  const profile = await client.post("/api/profiles", body);
@@ -2182,12 +3261,12 @@ var listCmd6 = defineCommand13({
2182
3261
  meta: { name: "list", description: "List profiles" },
2183
3262
  async run() {
2184
3263
  const client = createClient2();
2185
- const list3 = await client.get("/api/profiles");
2186
- if (list3.length === 0) {
3264
+ const list4 = await client.get("/api/profiles");
3265
+ if (list4.length === 0) {
2187
3266
  console.log("(no profiles)");
2188
3267
  return;
2189
3268
  }
2190
- const rows = list3.map((p) => [p.id, p.defaultModel, p.name]);
3269
+ const rows = list4.map((p) => [p.id, p.defaultModel, p.name]);
2191
3270
  for (const line of columnize(rows)) console.log(line);
2192
3271
  }
2193
3272
  });
@@ -2210,7 +3289,6 @@ var showCmd4 = defineCommand13({
2210
3289
  console.log(`bootstrap: ${loaded.files.bootstrap !== null ? "yes" : "no"}`);
2211
3290
  console.log(`agents: ${loaded.files.agents !== null ? "yes" : "no"}`);
2212
3291
  console.log(`tools: ${loaded.files.tools !== null ? "yes" : "no"}`);
2213
- console.log(`heartbeat: ${loaded.files.heartbeat !== null ? "yes" : "no"}`);
2214
3292
  if (loaded.identity) {
2215
3293
  const fields = Object.entries(loaded.identity).filter(([, v]) => typeof v === "string" && v.length > 0).map(([k, v]) => `${k}=${v}`).join(", ");
2216
3294
  if (fields) console.log(`identity: ${fields}`);
@@ -2236,17 +3314,17 @@ var editCmd2 = defineCommand13({
2236
3314
  const { content } = await client.get(
2237
3315
  `/api/profiles/${args.id}/files/${file}`
2238
3316
  );
2239
- const tmpPath = join3(tmpdir(), `bazilion-${args.id}-${file}-${randomBytes(4).toString("hex")}`);
3317
+ const tmpPath = join4(tmpdir2(), `bazilion-${args.id}-${file}-${randomBytes(4).toString("hex")}`);
2240
3318
  writeFileSync3(tmpPath, content);
2241
3319
  try {
2242
3320
  const editor = process.env.EDITOR ?? process.env.VISUAL ?? "vi";
2243
- const code = await new Promise((resolve3, reject) => {
2244
- const child = spawn3(editor, [tmpPath], { stdio: "inherit" });
3321
+ const code = await new Promise((resolve4, reject) => {
3322
+ const child = spawn2(editor, [tmpPath], { stdio: "inherit" });
2245
3323
  child.on("error", reject);
2246
- child.on("close", (c) => resolve3(c ?? 0));
3324
+ child.on("close", (c) => resolve4(c ?? 0));
2247
3325
  });
2248
3326
  if (code !== 0) throw new Error(`editor exited with code ${code}`);
2249
- const updated = readFileSync4(tmpPath, "utf8");
3327
+ const updated = readFileSync6(tmpPath, "utf8");
2250
3328
  if (updated === content) {
2251
3329
  console.log("(no changes)");
2252
3330
  return;
@@ -2256,7 +3334,7 @@ var editCmd2 = defineCommand13({
2256
3334
  console.log(`saved ${file}`);
2257
3335
  } finally {
2258
3336
  try {
2259
- rmSync2(tmpPath, { force: true });
3337
+ rmSync3(tmpPath, { force: true });
2260
3338
  } catch {
2261
3339
  }
2262
3340
  }
@@ -2501,12 +3579,12 @@ var sendCommand = defineCommand15({
2501
3579
  });
2502
3580
 
2503
3581
  // src/commands/serve.ts
2504
- import { spawn as spawn4 } from "child_process";
3582
+ import { spawn as spawn3 } from "child_process";
2505
3583
  import { existsSync as existsSync5 } from "fs";
2506
- import { join as join4 } from "path";
3584
+ import { join as join5 } from "path";
2507
3585
  import { defineCommand as defineCommand16 } from "citty";
2508
- var bundledDaemonEntry2 = join4(import.meta.dirname, "daemon.js");
2509
- var sourceDaemonEntry2 = join4(import.meta.dirname, "..", "..", "..", "daemon", "src", "index.ts");
3586
+ var bundledDaemonEntry2 = join5(import.meta.dirname, "daemon.js");
3587
+ var sourceDaemonEntry2 = join5(import.meta.dirname, "..", "..", "..", "daemon", "src", "index.ts");
2510
3588
  var daemonEntry2 = existsSync5(bundledDaemonEntry2) ? bundledDaemonEntry2 : sourceDaemonEntry2;
2511
3589
  var serveCommand = defineCommand16({
2512
3590
  meta: {
@@ -2541,7 +3619,7 @@ var serveCommand = defineCommand16({
2541
3619
  console.log(`starting bazilion daemon at http://${host}:${port}`);
2542
3620
  console.log("(web UI runs separately: cd apps/web && pnpm dev)");
2543
3621
  const nodeArgs2 = daemonEntry2.endsWith(".ts") ? ["--import", "tsx/esm", daemonEntry2] : [daemonEntry2];
2544
- const proc = spawn4("node", nodeArgs2, {
3622
+ const proc = spawn3("node", nodeArgs2, {
2545
3623
  env,
2546
3624
  stdio: "inherit"
2547
3625
  });
@@ -2557,17 +3635,17 @@ var serveCommand = defineCommand16({
2557
3635
  signalled = true;
2558
3636
  });
2559
3637
  process.on("SIGTERM", onSigterm);
2560
- await new Promise((resolve3, reject) => {
3638
+ await new Promise((resolve4, reject) => {
2561
3639
  proc.on("error", (err) => reject(err));
2562
3640
  proc.on("close", (code) => {
2563
3641
  if (signalled) {
2564
3642
  process.exitCode = 0;
2565
- resolve3();
3643
+ resolve4();
2566
3644
  } else if (code === 0 || code === null) {
2567
- resolve3();
3645
+ resolve4();
2568
3646
  } else {
2569
3647
  process.exitCode = code;
2570
- resolve3();
3648
+ resolve4();
2571
3649
  }
2572
3650
  });
2573
3651
  });
@@ -2575,8 +3653,8 @@ var serveCommand = defineCommand16({
2575
3653
  });
2576
3654
 
2577
3655
  // src/commands/skill.ts
2578
- import { existsSync as existsSync6, readFileSync as readFileSync5, statSync } from "fs";
2579
- import { basename as basename2, resolve as resolve2 } from "path";
3656
+ import { existsSync as existsSync6, readFileSync as readFileSync7, statSync } from "fs";
3657
+ import { basename as basename4, resolve as resolve3 } from "path";
2580
3658
  import { defineCommand as defineCommand17 } from "citty";
2581
3659
  function printFindings(findings, prefix = " ") {
2582
3660
  for (const f of findings) {
@@ -2640,13 +3718,13 @@ var importCmd = defineCommand17({
2640
3718
  },
2641
3719
  async run({ args }) {
2642
3720
  const client = createClient2();
2643
- const absFrom = resolve2(args.from);
3721
+ const absFrom = resolve3(args.from);
2644
3722
  const isLocalZip = args.from.toLowerCase().endsWith(".zip") && existsSync6(absFrom) && statSync(absFrom).isFile();
2645
3723
  let result;
2646
3724
  try {
2647
3725
  if (isLocalZip) {
2648
- const bytes = readFileSync5(absFrom);
2649
- const file = new File([bytes], basename2(absFrom), { type: "application/zip" });
3726
+ const bytes = readFileSync7(absFrom);
3727
+ const file = new File([bytes], basename4(absFrom), { type: "application/zip" });
2650
3728
  const fd = new FormData();
2651
3729
  fd.set("file", file);
2652
3730
  if (args.force) fd.set("force", "true");
@@ -2715,12 +3793,12 @@ var skillCommand = defineCommand17({
2715
3793
  });
2716
3794
 
2717
3795
  // src/commands/team.ts
2718
- import { readFileSync as readFileSync7 } from "fs";
3796
+ import { readFileSync as readFileSync9 } from "fs";
2719
3797
  import { stdin as stdin2 } from "process";
2720
3798
  import { defineCommand as defineCommand19 } from "citty";
2721
3799
 
2722
3800
  // src/commands/team-policy.ts
2723
- import { readFileSync as readFileSync6 } from "fs";
3801
+ import { readFileSync as readFileSync8 } from "fs";
2724
3802
  import { defineCommand as defineCommand18 } from "citty";
2725
3803
 
2726
3804
  // src/team-interchange.ts
@@ -2937,7 +4015,7 @@ function edgeDiff(before, after) {
2937
4015
  // src/commands/team-policy.ts
2938
4016
  function readJson(path) {
2939
4017
  try {
2940
- return JSON.parse(readFileSync6(path, "utf8"));
4018
+ return JSON.parse(readFileSync8(path, "utf8"));
2941
4019
  } catch (error) {
2942
4020
  throw new Error(`invalid JSON: ${error.message}`);
2943
4021
  }
@@ -3207,12 +4285,12 @@ var listCmd9 = defineCommand19({
3207
4285
  meta: { name: "list", description: "List teams" },
3208
4286
  async run() {
3209
4287
  const client = createClient2();
3210
- const list3 = await client.get("/api/teams");
3211
- if (list3.length === 0) {
4288
+ const list4 = await client.get("/api/teams");
4289
+ if (list4.length === 0) {
3212
4290
  console.log("(no teams)");
3213
4291
  return;
3214
4292
  }
3215
- const rows = list3.map((g) => [g.id, g.path]);
4293
+ const rows = list4.map((g) => [g.id, g.path]);
3216
4294
  for (const line of columnize(rows)) console.log(line);
3217
4295
  }
3218
4296
  });
@@ -3261,7 +4339,7 @@ var userMdSetCmd = defineCommand19({
3261
4339
  async run({ args }) {
3262
4340
  let content;
3263
4341
  if (args["from-stdin"]) content = await readStdin();
3264
- else if (args.file) content = readFileSync7(args.file, "utf8");
4342
+ else if (args.file) content = readFileSync9(args.file, "utf8");
3265
4343
  else if (args.text !== void 0) content = args.text;
3266
4344
  else {
3267
4345
  console.error("team user-md set: provide --file, --from-stdin, or --text");
@@ -3354,11 +4432,11 @@ var teamCommand = defineCommand19({
3354
4432
  });
3355
4433
 
3356
4434
  // src/commands/team-template.ts
3357
- import { readFileSync as readFileSync8 } from "fs";
4435
+ import { readFileSync as readFileSync10 } from "fs";
3358
4436
  import { defineCommand as defineCommand20 } from "citty";
3359
4437
  function jsonFile(path) {
3360
4438
  try {
3361
- return JSON.parse(readFileSync8(path, "utf8"));
4439
+ return JSON.parse(readFileSync10(path, "utf8"));
3362
4440
  } catch (error) {
3363
4441
  throw new Error(`invalid JSON: ${error.message}`);
3364
4442
  }
@@ -3383,7 +4461,7 @@ function printTeam(detail) {
3383
4461
  console.log(line);
3384
4462
  }
3385
4463
  }
3386
- var list2 = defineCommand20({
4464
+ var list3 = defineCommand20({
3387
4465
  meta: { name: "list", description: "List canonical Team templates" },
3388
4466
  args: { json: { type: "boolean", description: "Emit stable JSON" } },
3389
4467
  async run({ args }) {
@@ -3537,7 +4615,7 @@ var teamTemplateCommand = defineCommand20({
3537
4615
  name: "team-template",
3538
4616
  description: "Inspect and exchange canonical Team templates"
3539
4617
  },
3540
- subCommands: { list: list2, show: show3, export: exportCommand2, import: importCommand2 }
4618
+ subCommands: { list: list3, show: show3, export: exportCommand2, import: importCommand2 }
3541
4619
  });
3542
4620
 
3543
4621
  // src/commands/telegram.ts
@@ -3931,7 +5009,7 @@ var tokenCommand = defineCommand22({
3931
5009
  // src/commands/trigger.ts
3932
5010
  import { defineCommand as defineCommand23 } from "citty";
3933
5011
  var addCmd3 = defineCommand23({
3934
- meta: { name: "add", description: "Add a heartbeat / cron trigger to an agent" },
5012
+ meta: { name: "add", description: "Add an interval / cron trigger to an agent" },
3935
5013
  args: {
3936
5014
  agent: { type: "positional", required: true },
3937
5015
  every: {
@@ -4030,20 +5108,49 @@ var disableCmd3 = defineCommand23({
4030
5108
  console.log(`disabled trigger ${args.id}`);
4031
5109
  }
4032
5110
  });
5111
+ var historyCmd = defineCommand23({
5112
+ meta: { name: "history", description: "Show recent dispatches for a trigger" },
5113
+ args: {
5114
+ id: { type: "positional", required: true },
5115
+ limit: { type: "string", description: "Maximum rows (default 20)" }
5116
+ },
5117
+ async run({ args }) {
5118
+ const limit = args.limit ? Number(args.limit) : 20;
5119
+ if (!Number.isInteger(limit) || limit < 1 || limit > 100) {
5120
+ throw new Error("--limit must be an integer from 1 to 100");
5121
+ }
5122
+ const client = createClient2();
5123
+ const { dispatches } = await client.get(
5124
+ `/api/triggers/${args.id}/dispatches?limit=${limit}`
5125
+ );
5126
+ if (dispatches.length === 0) {
5127
+ console.log("(no dispatches)");
5128
+ return;
5129
+ }
5130
+ const rows = dispatches.map((dispatch) => [
5131
+ new Date(dispatch.scheduledAt).toISOString(),
5132
+ dispatch.status,
5133
+ `attempts: ${dispatch.attemptCount}`,
5134
+ dispatch.lastError ?? ""
5135
+ ]);
5136
+ for (const line of columnize(rows)) console.log(line);
5137
+ }
5138
+ });
4033
5139
  var triggerCommand = defineCommand23({
4034
- meta: { name: "trigger", description: "Manage agent heartbeats / cron triggers" },
5140
+ meta: { name: "trigger", description: "Manage scheduled agent triggers" },
4035
5141
  subCommands: {
4036
5142
  add: addCmd3,
4037
5143
  list: listCmd11,
4038
5144
  rm: rmCmd6,
4039
5145
  enable: enableCmd3,
4040
- disable: disableCmd3
5146
+ disable: disableCmd3,
5147
+ history: historyCmd
4041
5148
  }
4042
5149
  });
4043
5150
 
4044
5151
  // src/commands/uninstall.ts
4045
- import { existsSync as existsSync7, readdirSync as readdirSync2, rmSync as rmSync3 } from "fs";
4046
- import { join as join5 } from "path";
5152
+ import { existsSync as existsSync7, readdirSync as readdirSync2, rmSync as rmSync4 } from "fs";
5153
+ import { join as join6 } from "path";
4047
5154
  import { defineCommand as defineCommand24 } from "citty";
4048
5155
  function makeLineReader() {
4049
5156
  let buffer = "";
@@ -4054,8 +5161,8 @@ function makeLineReader() {
4054
5161
  const nlIdx = buffer.indexOf("\n");
4055
5162
  if (nlIdx === -1) {
4056
5163
  if (ended) {
4057
- const resolve3 = queue.shift();
4058
- resolve3?.(buffer);
5164
+ const resolve4 = queue.shift();
5165
+ resolve4?.(buffer);
4059
5166
  buffer = "";
4060
5167
  } else {
4061
5168
  return;
@@ -4063,8 +5170,8 @@ function makeLineReader() {
4063
5170
  } else {
4064
5171
  const line = buffer.slice(0, nlIdx);
4065
5172
  buffer = buffer.slice(nlIdx + 1);
4066
- const resolve3 = queue.shift();
4067
- resolve3?.(line);
5173
+ const resolve4 = queue.shift();
5174
+ resolve4?.(line);
4068
5175
  }
4069
5176
  }
4070
5177
  };
@@ -4077,8 +5184,8 @@ function makeLineReader() {
4077
5184
  ended = true;
4078
5185
  tryDeliver();
4079
5186
  });
4080
- return () => new Promise((resolve3) => {
4081
- queue.push(resolve3);
5187
+ return () => new Promise((resolve4) => {
5188
+ queue.push(resolve4);
4082
5189
  tryDeliver();
4083
5190
  });
4084
5191
  }
@@ -4089,7 +5196,7 @@ async function askYes(readLine, prompt) {
4089
5196
  }
4090
5197
  function removePath(p) {
4091
5198
  if (!existsSync7(p)) return false;
4092
- rmSync3(p, { recursive: true, force: true });
5199
+ rmSync4(p, { recursive: true, force: true });
4093
5200
  return true;
4094
5201
  }
4095
5202
  var uninstallCommand = defineCommand24({
@@ -4110,12 +5217,12 @@ var uninstallCommand = defineCommand24({
4110
5217
  },
4111
5218
  async run({ args }) {
4112
5219
  const paths = resolveCliPaths(args.home);
4113
- const dbFile = join5(paths.home, "bazilion.db");
4114
- const profilesDir = join5(paths.home, "profiles");
4115
- const agentsDir = join5(paths.home, "agents");
4116
- const teamsDir = join5(paths.home, "teams");
4117
- const skillsDir = join5(paths.home, "skills");
4118
- const logsDir = join5(paths.home, "logs");
5220
+ const dbFile = join6(paths.home, "bazilion.db");
5221
+ const profilesDir = join6(paths.home, "profiles");
5222
+ const agentsDir = join6(paths.home, "agents");
5223
+ const teamsDir = join6(paths.home, "teams");
5224
+ const skillsDir = join6(paths.home, "skills");
5225
+ const logsDir = join6(paths.home, "logs");
4119
5226
  if (!existsSync7(paths.home)) {
4120
5227
  console.log(`nothing to remove: ${paths.home} does not exist`);
4121
5228
  return;
@@ -4148,7 +5255,7 @@ var uninstallCommand = defineCommand24({
4148
5255
  if (removePath(p)) console.log(`removed ${p}`);
4149
5256
  }
4150
5257
  if (readdirSync2(paths.home).length === 0) {
4151
- rmSync3(paths.home, { recursive: true, force: true });
5258
+ rmSync4(paths.home, { recursive: true, force: true });
4152
5259
  console.log(`removed ${paths.home}`);
4153
5260
  } else {
4154
5261
  console.log(`${paths.home} still has unmanaged files; left in place`);
@@ -4275,7 +5382,7 @@ function printTopLevelHelp() {
4275
5382
  ["memory", "Read/write a team's shared memory"],
4276
5383
  ["send", "Send a message from one agent to another"],
4277
5384
  ["inbox", "Inspect agent inboxes"],
4278
- ["trigger", "Manage agent heartbeats / cron triggers"]
5385
+ ["trigger", "Manage scheduled agent triggers"]
4279
5386
  ]
4280
5387
  },
4281
5388
  {