chainlesschain 0.162.158 → 0.162.159

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 (201) hide show
  1. package/package.json +1 -1
  2. package/src/assets/web-panel/assets/{AIOps-BwpTJTN_.js → AIOps-ByIiF5bT.js} +1 -1
  3. package/src/assets/web-panel/assets/{ActionButton-BtQOoo3l.js → ActionButton-NlgSFZq5.js} +1 -1
  4. package/src/assets/web-panel/assets/{Analytics-Duf_rXZa.js → Analytics-D17cOUcb.js} +3 -3
  5. package/src/assets/web-panel/assets/{AppLayout-XQK3wjvB.js → AppLayout-B4mEZcbl.js} +4 -4
  6. package/src/assets/web-panel/assets/Artifacts-CASsmyQw.js +1 -0
  7. package/src/assets/web-panel/assets/{Audit-DNjOGQLh.js → Audit-DYAia2e2.js} +1 -1
  8. package/src/assets/web-panel/assets/{BackgroundAgents-FH9RAfB5.js → BackgroundAgents-C1lvYUSu.js} +1 -1
  9. package/src/assets/web-panel/assets/{Backup-ZIbrR4-U.js → Backup-B6agK6bA.js} +1 -1
  10. package/src/assets/web-panel/assets/{BaseInput-DSt3NaND.js → BaseInput-DM43R2vy.js} +1 -1
  11. package/src/assets/web-panel/assets/{Chat-CY-wvbjv.js → Chat-Dtdp3eXp.js} +5 -5
  12. package/src/assets/web-panel/assets/{ChatBubbleRenderer-BAV9V-t-.js → ChatBubbleRenderer-BL5c_OQ1.js} +1 -1
  13. package/src/assets/web-panel/assets/{Checkbox-8JVadE_B.js → Checkbox-D2xf7uZt.js} +1 -1
  14. package/src/assets/web-panel/assets/{Codegen-CN83nPYI.js → Codegen-D3j45jJL.js} +1 -1
  15. package/src/assets/web-panel/assets/{Col-xPSvqdN8.js → Col-voPJ9fUZ.js} +1 -1
  16. package/src/assets/web-panel/assets/{Community-D78cnIjG.js → Community-DEsyTESF.js} +1 -1
  17. package/src/assets/web-panel/assets/{Compact-CSw6Csbp.js → Compact-OHV_QXLD.js} +1 -1
  18. package/src/assets/web-panel/assets/{Compliance-E_HhjcYR.js → Compliance-eDTJWC-J.js} +1 -1
  19. package/src/assets/web-panel/assets/{Cowork-DybeQjmm.js → Cowork-BBvy-6d1.js} +3 -3
  20. package/src/assets/web-panel/assets/{Cron-DS0BJeLY.js → Cron-Dw2eoJxu.js} +2 -2
  21. package/src/assets/web-panel/assets/{Crosschain-BOwVgkbs.js → Crosschain-DQHms_Bh.js} +1 -1
  22. package/src/assets/web-panel/assets/{DID-DRUr9YuX.js → DID-Cyu7Id1c.js} +2 -2
  23. package/src/assets/web-panel/assets/{Dashboard-Cht-yUKW.js → Dashboard-sIDzkG92.js} +2 -2
  24. package/src/assets/web-panel/assets/{Dropdown-BLr_EJ9X.js → Dropdown-DUW3T2ey.js} +1 -1
  25. package/src/assets/web-panel/assets/{EmailListRenderer-Drexuh5W.js → EmailListRenderer-CECELXW8.js} +1 -1
  26. package/src/assets/web-panel/assets/{FamilyGuardDashboard-D9Q2Kq63.js → FamilyGuardDashboard-BIZR37ov.js} +1 -1
  27. package/src/assets/web-panel/assets/{Federation-C-Z1xI-T.js → Federation-BEZIQX84.js} +1 -1
  28. package/src/assets/web-panel/assets/{FormItemContext-DLHPMkQF.js → FormItemContext-BcdUMznL.js} +1 -1
  29. package/src/assets/web-panel/assets/{GenericCardRenderer-DxxEdn2r.js → GenericCardRenderer-DsN_fr5H.js} +1 -1
  30. package/src/assets/web-panel/assets/{Git-syATcaBt.js → Git-63LhjBx2.js} +2 -2
  31. package/src/assets/web-panel/assets/{Governance-CYe_r8Zf.js → Governance-CYtQtIcH.js} +1 -1
  32. package/src/assets/web-panel/assets/{Inference-GWxErB7k.js → Inference-BwR6UUox.js} +1 -1
  33. package/src/assets/web-panel/assets/{KnowledgeGraph-qQvVCZoi.js → KnowledgeGraph-D9lVNTUA.js} +1 -1
  34. package/src/assets/web-panel/assets/{Logs-BenAU2W8.js → Logs-C4q5h7Ut.js} +2 -2
  35. package/src/assets/web-panel/assets/{MarkdownRenderer-7agE3Or1.js → MarkdownRenderer-DexLlXfN.js} +1 -1
  36. package/src/assets/web-panel/assets/{Marketplace-D4Jn5fpD.js → Marketplace-Plksliuw.js} +1 -1
  37. package/src/assets/web-panel/assets/{McpTools-DtAwoqiQ.js → McpTools-DZUFfeuG.js} +4 -4
  38. package/src/assets/web-panel/assets/{Memory-BI_O50I8.js → Memory-NoDYt29V.js} +2 -2
  39. package/src/assets/web-panel/assets/{MobileBridge-CQWE2lqx.js → MobileBridge-CEXGTkdh.js} +1 -1
  40. package/src/assets/web-panel/assets/{MobileProjects-CVtTqgtg.js → MobileProjects-DnbJkbZo.js} +1 -1
  41. package/src/assets/web-panel/assets/{Mtc-Dnr16qsn.js → Mtc-DTAa0cwy.js} +5 -5
  42. package/src/assets/web-panel/assets/{MtcAudit-CQ5Xe4-a.js → MtcAudit-CVV7LnNS.js} +2 -2
  43. package/src/assets/web-panel/assets/{Multisig-Bjr8O-wy.js → Multisig-BqqGrjt_.js} +3 -3
  44. package/src/assets/web-panel/assets/{NLProgramming-CEe-KirU.js → NLProgramming-C4WP8DRp.js} +1 -1
  45. package/src/assets/web-panel/assets/{Notes-spKJIKF9.js → Notes-Zo4DJUVI.js} +4 -4
  46. package/src/assets/web-panel/assets/{NotificationSettings-DRp83LFG.js → NotificationSettings-DBcjRaz2.js} +1 -1
  47. package/src/assets/web-panel/assets/OrderTableRenderer-BCQel9bD.js +1 -0
  48. package/src/assets/web-panel/assets/{Organization-BlgB0IYg.js → Organization-BT9WPBOX.js} +4 -4
  49. package/src/assets/web-panel/assets/{Overflow-a_35HqZI.js → Overflow-D6b4bZ3q.js} +1 -1
  50. package/src/assets/web-panel/assets/{P2P-CSNHCNPC.js → P2P-Cw3OsYx-.js} +2 -2
  51. package/src/assets/web-panel/assets/{PdhVaultBrowser-C8M1JH-1.js → PdhVaultBrowser-C0nyiK2b.js} +5 -5
  52. package/src/assets/web-panel/assets/{Permissions-DmjJi9Ms.js → Permissions-zVBZEB_C.js} +3 -3
  53. package/src/assets/web-panel/assets/{PersonalDataHub-DfuwRyf4.js → PersonalDataHub-BOspsFew.js} +2 -2
  54. package/src/assets/web-panel/assets/{Pipeline-B2-S4ekE.js → Pipeline-CQWfZ4XH.js} +1 -1
  55. package/src/assets/web-panel/assets/{Privacy-B1dOiBLC.js → Privacy-C2oGJ_WL.js} +1 -1
  56. package/src/assets/web-panel/assets/{ProjectInit-_pNmc-DX.js → ProjectInit-BZMR4qY1.js} +2 -2
  57. package/src/assets/web-panel/assets/{ProjectSettings-DmwG2Ii1.js → ProjectSettings-DAujoP63.js} +2 -2
  58. package/src/assets/web-panel/assets/{Projects-COATmHxF.js → Projects-DvH_P2Uq.js} +1 -1
  59. package/src/assets/web-panel/assets/{Providers-C6HSjg1d.js → Providers-D_RGHM-v.js} +1 -1
  60. package/src/assets/web-panel/assets/{QrScannerModal-CrDmS7l1.js → QrScannerModal-Mf6KfL2J.js} +1 -1
  61. package/src/assets/web-panel/assets/{QuickAsk-CfKvWt-x.js → QuickAsk-5D8URoI_.js} +1 -1
  62. package/src/assets/web-panel/assets/{Recommend-DMa6Kuzv.js → Recommend-Df4kC8Gy.js} +1 -1
  63. package/src/assets/web-panel/assets/{RemoteSession-DrktUo5x.js → RemoteSession-DGi3f3vO.js} +2 -2
  64. package/src/assets/web-panel/assets/{Reputation-BTPQ1vjL.js → Reputation-jmkGFNWb.js} +1 -1
  65. package/src/assets/web-panel/assets/{Row-B6Q7QkMG.js → Row-DP6Pcu3m.js} +1 -1
  66. package/src/assets/web-panel/assets/{RssFeed-B1IJhAmX.js → RssFeed-D60hSuPO.js} +3 -3
  67. package/src/assets/web-panel/assets/{Search-xbea1kGD.js → Search-0mf1BQUK.js} +1 -1
  68. package/src/assets/web-panel/assets/{Security-7rs0cO3F.js → Security-CfZtLFYV.js} +4 -4
  69. package/src/assets/web-panel/assets/{Services-0fjixK0S.js → Services-D9zpZLWz.js} +2 -2
  70. package/src/assets/web-panel/assets/{Skeleton-B_rVq9Cu.js → Skeleton-D1_44eMD.js} +1 -1
  71. package/src/assets/web-panel/assets/{Skills-C5uM90b9.js → Skills-cVFa8zkz.js} +1 -1
  72. package/src/assets/web-panel/assets/{Sla-U4pYCIZH.js → Sla-Cinpzc3L.js} +1 -1
  73. package/src/assets/web-panel/assets/{SpeechSettings-Cdz4yTWx.js → SpeechSettings-bqdiJE_P.js} +1 -1
  74. package/src/assets/web-panel/assets/{SyncSettings-Me3aetHv.js → SyncSettings-Dgl_SLgj.js} +2 -2
  75. package/src/assets/web-panel/assets/{Tasks-Cf6aNkL_.js → Tasks-DawWAEhy.js} +1 -1
  76. package/src/assets/web-panel/assets/{Templates-CYatk4tU.js → Templates-Ca0nY651.js} +1 -1
  77. package/src/assets/web-panel/assets/{Tenant-DIMKLw7o.js → Tenant-CgaGEhLm.js} +1 -1
  78. package/src/assets/web-panel/assets/{Terminal-C_cp-uSJ.js → Terminal-D-9EnpRx.js} +2 -2
  79. package/src/assets/web-panel/assets/{TimelineRenderer-cUPVNADp.js → TimelineRenderer-CSOxDHZ6.js} +1 -1
  80. package/src/assets/web-panel/assets/{Tokens-CiaEIXVf.js → Tokens-BpwLZpL3.js} +1 -1
  81. package/src/assets/web-panel/assets/{Trigger-DB1WU5lQ.js → Trigger-DgowwBK5.js} +1 -1
  82. package/src/assets/web-panel/assets/{Trust-Cfht9Ijy.js → Trust-BZutapjE.js} +1 -1
  83. package/src/assets/web-panel/assets/{UkeySign-K3P4Vt9S.js → UkeySign-QZPt8_Sp.js} +1 -1
  84. package/src/assets/web-panel/assets/{VideoEditing-DWjhjXRh.js → VideoEditing-DlQx5UDJ.js} +1 -1
  85. package/src/assets/web-panel/assets/{Wallet-D9-Q1Kan.js → Wallet-ZfdTgtWA.js} +4 -4
  86. package/src/assets/web-panel/assets/{WebAuthn-BDszy-oV.js → WebAuthn-CAp0y_Cr.js} +5 -5
  87. package/src/assets/web-panel/assets/{WorkflowEditor-DIhij4rA.js → WorkflowEditor-BVITZEmr.js} +1 -1
  88. package/src/assets/web-panel/assets/{chat-D7VCiKbh.js → chat-CC2tPaNg.js} +1 -1
  89. package/src/assets/web-panel/assets/{colors-BvOd9pVn.js → colors-31brc9CA.js} +1 -1
  90. package/src/assets/web-panel/assets/{compact-item-DoqXBiAO.js → compact-item-DLMkC9C-.js} +1 -1
  91. package/src/assets/web-panel/assets/{createContext-CrKRzUtp.js → createContext-BMtveI6F.js} +1 -1
  92. package/src/assets/web-panel/assets/devWarning-CB8pPOtd.js +1 -0
  93. package/src/assets/web-panel/assets/{hasIn-BWAev4WC.js → hasIn-DbD6WHW2.js} +1 -1
  94. package/src/assets/web-panel/assets/{index-B30NvR21.js → index-3oBsXZmF.js} +1 -1
  95. package/src/assets/web-panel/assets/{index-Cx7M7PSF.js → index-ALvoNTRF.js} +1 -1
  96. package/src/assets/web-panel/assets/{index-BHOcVYrx.js → index-AaIKG60c.js} +1 -1
  97. package/src/assets/web-panel/assets/{index-BXyl8O23.js → index-BFRLmCR3.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-YGDy7nNq.js → index-BKAB6-6V.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-BoYrS_L2.js → index-BOM-VFlv.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-Bq_ctRpz.js → index-BPpS-lGb.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-CDGLdn12.js → index-Bbc2JeQG.js} +1 -1
  102. package/src/assets/web-panel/assets/index-BhXiaiev.js +1 -0
  103. package/src/assets/web-panel/assets/{index-ISvPZ05r.js → index-BiRQUoyE.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-B0vGEbJT.js → index-BnFLzejT.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-Bj7EgmOp.js → index-BoO4WEJB.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-BZGk8AbZ.js → index-C-cGlCDW.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-BkgnGX7v.js → index-C1PfeI88.js} +1 -1
  108. package/src/assets/web-panel/assets/{index-HwHdTusY.js → index-C21pdBqs.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-Bf9YlbDn.js → index-C7LHNw_N.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-oY45XrkG.js → index-CEXa-4zd.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-KR1-Em9C.js → index-CEwjBA9q.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-C5kRmj3P.js → index-CFXKbOek.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-BN9m4Ux2.js → index-CJhREOlG.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-PDxGjKRu.js → index-COHHQ9cU.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-D23L1V76.js → index-CUmLMFsy.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-zH-tuuGZ.js → index-CWgfipcC.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-CmJ6xKBO.js → index-C_eUKGYP.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-CEPHMSih.js → index-CcVszn2b.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-DV-Rfmfx.js → index-Cod97Urs.js} +3 -3
  120. package/src/assets/web-panel/assets/{index-BWWsaimP.js → index-Cwbqh0KZ.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-8g5TJCZ3.js → index-D67kIagl.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-CEXfKSiI.js → index-D7cV0tu0.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-CoD31Ndm.js → index-DJvxFyIz.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-CSb-tnIo.js → index-DZl-UXB6.js} +1 -1
  125. package/src/assets/web-panel/assets/{index-DvDOpsJB.js → index-Dm0byjm1.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-DlGu7As6.js → index-DndNnYc0.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-gmxqu5Kk.js → index-WQUO2vvK.js} +1 -1
  128. package/src/assets/web-panel/assets/index-WpaYaE3D.js +1 -0
  129. package/src/assets/web-panel/assets/{index-ww1QtLnX.js → index-bdaJwTXJ.js} +1 -1
  130. package/src/assets/web-panel/assets/{index-JzUmJMvn.js → index-ep6JchNG.js} +1 -1
  131. package/src/assets/web-panel/assets/{index-BCyP071m.js → index-jG6h22Qg.js} +1 -1
  132. package/src/assets/web-panel/assets/{index-1poMpMU0.js → index-ntVQqSML.js} +1 -1
  133. package/src/assets/web-panel/assets/{initDefaultProps-XW-OR3mL.js → initDefaultProps-D5UoBRhN.js} +1 -1
  134. package/src/assets/web-panel/assets/{motion-CeuEHh20.js → motion-DltOw0CR.js} +1 -1
  135. package/src/assets/web-panel/assets/{move-Dva79Keg.js → move-_oPkk5zj.js} +1 -1
  136. package/src/assets/web-panel/assets/{mtc-parser-C0B6bD2j.js → mtc-parser-EES8ROCS.js} +1 -1
  137. package/src/assets/web-panel/assets/{omit-AkZvO-a2.js → omit-B-_39x_M.js} +1 -1
  138. package/src/assets/web-panel/assets/{pickAttrs-B4r55qGc.js → pickAttrs-l6D0yJHz.js} +1 -1
  139. package/src/assets/web-panel/assets/{placementArrow-BLH-z7YE.js → placementArrow-DIeUK31L.js} +1 -1
  140. package/src/assets/web-panel/assets/{responsiveObserve-ksEGZ2Wf.js → responsiveObserve-dG4EzUQo.js} +1 -1
  141. package/src/assets/web-panel/assets/{slide-Cth4OD1Z.js → slide-DWFzYGdH.js} +1 -1
  142. package/src/assets/web-panel/assets/{statusUtils-9S83ezxw.js → statusUtils-CECdTA5H.js} +1 -1
  143. package/src/assets/web-panel/assets/{styleChecker-7yQmQGZ4.js → styleChecker-D6kP-mMK.js} +1 -1
  144. package/src/assets/web-panel/assets/{useFlexGapSupport-DpGnMmPc.js → useFlexGapSupport-BWfna_rd.js} +1 -1
  145. package/src/assets/web-panel/assets/{useFs-C4zDZIXH.js → useFs-Bb-j6rzo.js} +1 -1
  146. package/src/assets/web-panel/assets/{usePersonalDataHub-CoLICdUZ.js → usePersonalDataHub-D5OIsYM4.js} +1 -1
  147. package/src/assets/web-panel/assets/{vnode-CKyH-35X.js → vnode-DNpwzFTM.js} +1 -1
  148. package/src/assets/web-panel/assets/{zoom-DRDGit17.js → zoom-vNLpFQBW.js} +1 -1
  149. package/src/assets/web-panel/index.html +1 -1
  150. package/src/command-manifest.json +17 -3
  151. package/src/commands/agent.js +7 -0
  152. package/src/commands/background-session.js +143 -3
  153. package/src/commands/browse.js +86 -0
  154. package/src/commands/cloud.js +232 -0
  155. package/src/commands/doctor.js +61 -6
  156. package/src/commands/mcp.js +61 -0
  157. package/src/commands/plugin.js +63 -1
  158. package/src/commands/routine.js +367 -0
  159. package/src/commands/session.js +326 -0
  160. package/src/data/changelog.json +7 -2
  161. package/src/harness/jsonl-session-store.js +82 -2
  162. package/src/harness/session-index.js +295 -0
  163. package/src/harness/session-mirror.js +142 -0
  164. package/src/harness/transcript-integrity.js +182 -0
  165. package/src/index.js +8 -0
  166. package/src/lib/background-agent-supervisor.js +126 -2
  167. package/src/lib/channels/channel-manager.js +126 -0
  168. package/src/lib/channels/telegram-channel.js +112 -0
  169. package/src/lib/channels/webhook-channel.js +140 -0
  170. package/src/lib/chrome-connector.js +386 -0
  171. package/src/lib/cloud/bundle.js +110 -0
  172. package/src/lib/cloud/cloud-client.js +152 -0
  173. package/src/lib/computer-use/authorization.js +173 -0
  174. package/src/lib/computer-use/computer-use-server.js +276 -0
  175. package/src/lib/computer-use/control-backend.js +150 -0
  176. package/src/lib/doctor-checkup.js +652 -0
  177. package/src/lib/jsonl-session-store.js +4 -0
  178. package/src/lib/lsp/lsp-server-registry.js +48 -21
  179. package/src/lib/plugin-runtime/governance.js +174 -0
  180. package/src/lib/pr-link-ledger.js +221 -0
  181. package/src/lib/routine-store.js +352 -0
  182. package/src/lib/session-store-guard.js +40 -0
  183. package/src/lib/session-usage.js +318 -2
  184. package/src/lib/sub-agent-context.js +23 -0
  185. package/src/repl/agent-repl.js +361 -3
  186. package/src/repl/bg-dashboard.js +355 -0
  187. package/src/repl/fast-mode.js +103 -0
  188. package/src/repl/fullscreen-tui.js +197 -0
  189. package/src/repl/slash-command-registry.js +11 -3
  190. package/src/repl/voice-dictation.js +132 -0
  191. package/src/runtime/agent-core.js +242 -1
  192. package/src/runtime/coding-agent-contract-shared.cjs +88 -0
  193. package/src/runtime/coding-agent-policy.cjs +17 -0
  194. package/src/runtime/headless-runner.js +71 -7
  195. package/src/runtime/headless-stream.js +14 -8
  196. package/src/runtime/policies/agent-policy.js +3 -0
  197. package/src/assets/web-panel/assets/Artifacts-DQvJoQ6E.js +0 -1
  198. package/src/assets/web-panel/assets/OrderTableRenderer-h1A0XB2a.js +0 -1
  199. package/src/assets/web-panel/assets/devWarning-nDB--2xr.js +0 -1
  200. package/src/assets/web-panel/assets/index-B1_pRTtB.js +0 -1
  201. package/src/assets/web-panel/assets/index-TMM9FuW-.js +0 -1
@@ -0,0 +1,295 @@
1
+ /**
2
+ * Session index (gap-2026-07-11 P2#14) — a SQLite index over the append-only
3
+ * JSONL session store.
4
+ *
5
+ * The JSONL files stay the source of truth (append-only, hash-chained,
6
+ * tamper-evident — see jsonl-session-store.js + transcript-integrity.js). But
7
+ * listing/searching them means reading and parsing every file on every call,
8
+ * which does not scale past a few hundred sessions. This index gives O(1) list
9
+ * ordering and indexed content search, and stays in sync incrementally by
10
+ * file mtime+size so a resume/fork/search/verify is cheap.
11
+ *
12
+ * The index is a derived cache: it can be deleted and fully rebuilt from the
13
+ * JSONL at any time (`syncIndex({ force: true })`). Nothing authoritative lives
14
+ * only here.
15
+ *
16
+ * `Database` is injected via `_deps` so tests run against an in-memory SQLite
17
+ * without touching the user's home dir.
18
+ */
19
+
20
+ import {
21
+ existsSync,
22
+ mkdirSync,
23
+ readdirSync,
24
+ statSync,
25
+ readFileSync,
26
+ } from "node:fs";
27
+ import { join, basename } from "node:path";
28
+ import { createRequire } from "node:module";
29
+ import { getHomeDir } from "../lib/paths.js";
30
+ import { readEvents, toIsoSafe } from "./jsonl-session-store.js";
31
+ import { latestChainHash } from "./transcript-integrity.js";
32
+
33
+ const requireCjs = createRequire(import.meta.url);
34
+
35
+ // Lazy so importing this module never forces the native addon to load (matters
36
+ // for fast CLI paths and for environments where better-sqlite3 is unavailable).
37
+ export const _deps = {
38
+ Database: null,
39
+ loadDatabase() {
40
+ if (!_deps.Database) {
41
+ _deps.Database = requireCjs("better-sqlite3");
42
+ }
43
+ return _deps.Database;
44
+ },
45
+ };
46
+
47
+ function sessionsDir() {
48
+ return join(getHomeDir(), "sessions");
49
+ }
50
+
51
+ function indexPath() {
52
+ const dir = getHomeDir();
53
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
54
+ return join(dir, "session-index.db");
55
+ }
56
+
57
+ const SCHEMA = `
58
+ CREATE TABLE IF NOT EXISTS sessions (
59
+ id TEXT PRIMARY KEY,
60
+ title TEXT,
61
+ provider TEXT,
62
+ model TEXT,
63
+ message_count INTEGER,
64
+ event_count INTEGER,
65
+ created_at TEXT,
66
+ updated_at TEXT,
67
+ last_ts INTEGER,
68
+ chain_hash TEXT,
69
+ mtime_ms INTEGER,
70
+ size_bytes INTEGER
71
+ );
72
+ CREATE TABLE IF NOT EXISTS session_content (
73
+ id TEXT PRIMARY KEY,
74
+ text TEXT
75
+ );
76
+ CREATE INDEX IF NOT EXISTS idx_sessions_last_ts ON sessions(last_ts DESC);
77
+ `;
78
+
79
+ /**
80
+ * Open (and migrate) the index database. Caller owns closing it. Pass
81
+ * `{ file: ":memory:" }` for tests.
82
+ */
83
+ export function openIndex({ file } = {}) {
84
+ const Database = _deps.loadDatabase();
85
+ const db = new Database(file || indexPath());
86
+ db.pragma("journal_mode = WAL");
87
+ db.exec(SCHEMA);
88
+ return db;
89
+ }
90
+
91
+ /** Max characters of concatenated message content indexed per session. */
92
+ const CONTENT_CAP = 200_000;
93
+
94
+ function summarizeEvents(id, events) {
95
+ const startEvent = events.find((e) => e.type === "session_start");
96
+ const lastEvent = events[events.length - 1];
97
+ const messageCount = events.filter(
98
+ (e) => e.type === "user_message" || e.type === "assistant_message",
99
+ ).length;
100
+ const parts = [];
101
+ let used = 0;
102
+ for (const e of events) {
103
+ if (e.type !== "user_message" && e.type !== "assistant_message") continue;
104
+ const c = e.data?.content;
105
+ const s = typeof c === "string" ? c : c == null ? "" : JSON.stringify(c);
106
+ if (!s) continue;
107
+ parts.push(s);
108
+ used += s.length;
109
+ if (used >= CONTENT_CAP) break;
110
+ }
111
+ return {
112
+ id,
113
+ title: startEvent?.data?.title || "Untitled",
114
+ provider: startEvent?.data?.provider || "",
115
+ model: startEvent?.data?.model || "",
116
+ message_count: messageCount,
117
+ event_count: events.length,
118
+ created_at: toIsoSafe(startEvent?.timestamp),
119
+ updated_at: toIsoSafe(lastEvent?.timestamp),
120
+ last_ts: lastEvent?.timestamp || 0,
121
+ content: parts.join("\n").slice(0, CONTENT_CAP),
122
+ };
123
+ }
124
+
125
+ const UPSERT_SESSION = `
126
+ INSERT INTO sessions
127
+ (id,title,provider,model,message_count,event_count,created_at,updated_at,last_ts,chain_hash,mtime_ms,size_bytes)
128
+ VALUES (@id,@title,@provider,@model,@message_count,@event_count,@created_at,@updated_at,@last_ts,@chain_hash,@mtime_ms,@size_bytes)
129
+ ON CONFLICT(id) DO UPDATE SET
130
+ title=@title, provider=@provider, model=@model, message_count=@message_count,
131
+ event_count=@event_count, created_at=@created_at, updated_at=@updated_at,
132
+ last_ts=@last_ts, chain_hash=@chain_hash, mtime_ms=@mtime_ms, size_bytes=@size_bytes
133
+ `;
134
+
135
+ /**
136
+ * Index (or re-index) a single session from its JSONL into `db`.
137
+ * @returns {boolean} true if a row was written.
138
+ */
139
+ export function indexOneSession(db, id, { mtimeMs = 0, sizeBytes = 0 } = {}) {
140
+ const events = readEvents(id);
141
+ if (events.length === 0) return false;
142
+ const s = summarizeEvents(id, events);
143
+ const filePath = join(sessionsDir(), `${id}.jsonl`);
144
+ let chainHash = "";
145
+ try {
146
+ chainHash = latestChainHash(readFileSync(filePath, "utf-8")) || "";
147
+ } catch {
148
+ /* chain hash is best-effort */
149
+ }
150
+ db.prepare(UPSERT_SESSION).run({
151
+ id: s.id,
152
+ title: s.title,
153
+ provider: s.provider,
154
+ model: s.model,
155
+ message_count: s.message_count,
156
+ event_count: s.event_count,
157
+ created_at: s.created_at,
158
+ updated_at: s.updated_at,
159
+ last_ts: s.last_ts,
160
+ chain_hash: chainHash,
161
+ mtime_ms: Math.floor(mtimeMs),
162
+ size_bytes: sizeBytes,
163
+ });
164
+ db.prepare(
165
+ "INSERT INTO session_content (id,text) VALUES (?,?) ON CONFLICT(id) DO UPDATE SET text=excluded.text",
166
+ ).run(s.id, s.content);
167
+ return true;
168
+ }
169
+
170
+ /**
171
+ * Incrementally sync the index with the sessions dir. A session is re-indexed
172
+ * only when its file mtime or size changed since last time (or `force`).
173
+ * Deleted JSONL files have their index rows removed.
174
+ * @returns {{ scanned, updated, removed, total }}
175
+ */
176
+ export function syncIndex(db, { force = false } = {}) {
177
+ const dir = sessionsDir();
178
+ let scanned = 0;
179
+ let updated = 0;
180
+ let removed = 0;
181
+ const seen = new Set();
182
+
183
+ if (existsSync(dir)) {
184
+ const known = new Map(
185
+ db
186
+ .prepare("SELECT id, mtime_ms, size_bytes FROM sessions")
187
+ .all()
188
+ .map((r) => [r.id, r]),
189
+ );
190
+ const files = readdirSync(dir).filter((f) => f.endsWith(".jsonl"));
191
+ const tx = db.transaction((entries) => {
192
+ for (const { id, mtimeMs, size } of entries) {
193
+ seen.add(id);
194
+ scanned++;
195
+ const prev = known.get(id);
196
+ if (
197
+ !force &&
198
+ prev &&
199
+ prev.mtime_ms === Math.floor(mtimeMs) &&
200
+ prev.size_bytes === size
201
+ ) {
202
+ continue; // unchanged
203
+ }
204
+ if (indexOneSession(db, id, { mtimeMs, sizeBytes: size })) updated++;
205
+ }
206
+ });
207
+ const entries = [];
208
+ for (const f of files) {
209
+ const id = basename(f, ".jsonl");
210
+ let st;
211
+ try {
212
+ st = statSync(join(dir, f));
213
+ } catch {
214
+ continue;
215
+ }
216
+ entries.push({ id, mtimeMs: st.mtimeMs, size: st.size });
217
+ }
218
+ tx(entries);
219
+
220
+ // Drop rows whose JSONL no longer exists.
221
+ for (const id of known.keys()) {
222
+ if (!seen.has(id)) {
223
+ db.prepare("DELETE FROM sessions WHERE id=?").run(id);
224
+ db.prepare("DELETE FROM session_content WHERE id=?").run(id);
225
+ removed++;
226
+ }
227
+ }
228
+ }
229
+
230
+ const total = db.prepare("SELECT COUNT(*) c FROM sessions").get().c;
231
+ return { scanned, updated, removed, total };
232
+ }
233
+
234
+ const LIST_COLUMNS =
235
+ "id,title,provider,model,message_count,event_count,created_at,updated_at";
236
+
237
+ /** List indexed sessions, newest activity first. */
238
+ export function listSessions(db, { limit = 20 } = {}) {
239
+ return db
240
+ .prepare(
241
+ `SELECT ${LIST_COLUMNS} FROM sessions ORDER BY last_ts DESC, event_count DESC LIMIT ?`,
242
+ )
243
+ .all(Math.max(1, limit | 0));
244
+ }
245
+
246
+ /**
247
+ * Full-text-ish search over session titles and message content (LIKE). Returns
248
+ * matched sessions with a short snippet around the first hit.
249
+ */
250
+ export function searchSessions(db, query, { limit = 20 } = {}) {
251
+ const q = String(query || "").trim();
252
+ if (!q) return [];
253
+ const like = `%${q.replace(/[%_]/g, (m) => "\\" + m)}%`;
254
+ const rows = db
255
+ .prepare(
256
+ `SELECT s.${LIST_COLUMNS.split(",").join(", s.")}, c.text AS _text
257
+ FROM sessions s LEFT JOIN session_content c ON c.id = s.id
258
+ WHERE s.title LIKE ? ESCAPE '\\' OR c.text LIKE ? ESCAPE '\\'
259
+ ORDER BY s.last_ts DESC LIMIT ?`,
260
+ )
261
+ .all(like, like, Math.max(1, limit | 0));
262
+ const needle = q.toLowerCase();
263
+ return rows.map((r) => {
264
+ const text = r._text || "";
265
+ const at = text.toLowerCase().indexOf(needle);
266
+ const snippet =
267
+ at >= 0
268
+ ? text
269
+ .slice(Math.max(0, at - 40), at + q.length + 40)
270
+ .replace(/\s+/g, " ")
271
+ .trim()
272
+ : "";
273
+ const { _text, ...rest } = r;
274
+ return { ...rest, snippet };
275
+ });
276
+ }
277
+
278
+ /** One indexed session row, or null. */
279
+ export function getIndexedSession(db, id) {
280
+ return db.prepare(`SELECT * FROM sessions WHERE id=?`).get(id) || null;
281
+ }
282
+
283
+ /** Index-wide counters for `cc doctor` / `cc session index --stats`. */
284
+ export function indexStats(db) {
285
+ const row = db
286
+ .prepare(
287
+ "SELECT COUNT(*) sessions, COALESCE(SUM(message_count),0) messages, COALESCE(SUM(event_count),0) events FROM sessions",
288
+ )
289
+ .get();
290
+ return {
291
+ sessions: row.sessions,
292
+ messages: row.messages,
293
+ events: row.events,
294
+ };
295
+ }
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Session mirror (gap-2026-07-11 P2#14) — a pluggable off-box copy of the JSONL
3
+ * session store for cross-device / cloud-runner / team scenarios.
4
+ *
5
+ * The mirror is one-way derived from the local JSONL (which stays the source of
6
+ * truth): `push(id)` copies a session's bytes out, `pull(id)` fetches them back,
7
+ * `list()` enumerates what the remote holds. Encryption and access control are
8
+ * the driver's concern (configured independently), so a driver may wrap bytes
9
+ * before they leave the box.
10
+ *
11
+ * Drivers implement a tiny interface `{ push(id, bytes), pull(id), list() }`.
12
+ * Two are built in:
13
+ * - `fs` : mirror to a local/mounted directory (reference impl; also the
14
+ * test target). Covers NFS/SMB-mounted shares for free.
15
+ * - `http` : PUT/GET/list against a self-hosted API mirror (injectable fetch).
16
+ * S3 / WebDAV / Redis are additional drivers that plug in behind the same
17
+ * factory; they need real endpoints so they are configured, not bundled.
18
+ *
19
+ * Pure/DI: filesystem and fetch go through injected deps so the factory and the
20
+ * fs driver are unit-testable without network or a real home dir.
21
+ */
22
+
23
+ import {
24
+ existsSync,
25
+ mkdirSync,
26
+ readFileSync,
27
+ writeFileSync,
28
+ readdirSync,
29
+ } from "node:fs";
30
+ import { join, basename } from "node:path";
31
+ import { isUnsafeSessionId } from "./jsonl-session-store.js";
32
+
33
+ /**
34
+ * Build a mirror driver from config. `config.kind` selects the driver:
35
+ * { kind: "fs", dir: "/mnt/backup/sessions" }
36
+ * { kind: "http", baseUrl: "https://mirror.local", token: "…" }
37
+ * Returns null when no mirror is configured (kind falsy / "none").
38
+ */
39
+ export function createMirror(config = {}, deps = {}) {
40
+ const kind = String(config.kind || config.type || "none").toLowerCase();
41
+ if (!kind || kind === "none") return null;
42
+ if (kind === "fs" || kind === "dir" || kind === "directory") {
43
+ return createFsMirror(config, deps);
44
+ }
45
+ if (kind === "http" || kind === "api") {
46
+ return createHttpMirror(config, deps);
47
+ }
48
+ throw new Error(
49
+ `unknown session mirror kind "${kind}" (use fs | http, or wire an s3/webdav/redis driver)`,
50
+ );
51
+ }
52
+
53
+ /** Directory mirror — the reference driver. */
54
+ export function createFsMirror(config = {}, deps = {}) {
55
+ const dir = config.dir || config.path;
56
+ if (!dir) throw new Error("fs mirror requires a `dir`");
57
+ const _existsSync = deps.existsSync || existsSync;
58
+ const _mkdirSync = deps.mkdirSync || mkdirSync;
59
+ const _readFileSync = deps.readFileSync || readFileSync;
60
+ const _writeFileSync = deps.writeFileSync || writeFileSync;
61
+ const _readdirSync = deps.readdirSync || readdirSync;
62
+
63
+ const ensure = () => {
64
+ if (!_existsSync(dir)) _mkdirSync(dir, { recursive: true });
65
+ return dir;
66
+ };
67
+ const fileFor = (id) => {
68
+ if (isUnsafeSessionId(id)) throw new Error(`unsafe session id: ${id}`);
69
+ return join(dir, `${id}.jsonl`);
70
+ };
71
+
72
+ return {
73
+ kind: "fs",
74
+ target: dir,
75
+ async push(id, bytes) {
76
+ ensure();
77
+ _writeFileSync(fileFor(id), bytes, "utf-8");
78
+ return { id, bytes: Buffer.byteLength(bytes) };
79
+ },
80
+ async pull(id) {
81
+ const f = fileFor(id);
82
+ if (!_existsSync(f)) return null;
83
+ return _readFileSync(f, "utf-8");
84
+ },
85
+ async list() {
86
+ if (!_existsSync(dir)) return [];
87
+ return _readdirSync(dir)
88
+ .filter((f) => f.endsWith(".jsonl"))
89
+ .map((f) => basename(f, ".jsonl"));
90
+ },
91
+ };
92
+ }
93
+
94
+ /** HTTP mirror — PUT/GET/list against a self-hosted API. `fetch` injectable. */
95
+ export function createHttpMirror(config = {}, deps = {}) {
96
+ const baseUrl = String(config.baseUrl || config.url || "").replace(
97
+ /\/+$/,
98
+ "",
99
+ );
100
+ if (!baseUrl) throw new Error("http mirror requires a `baseUrl`");
101
+ const token = config.token || null;
102
+ const doFetch = deps.fetch || ((...a) => globalThis.fetch(...a));
103
+ const headers = (extra = {}) => ({
104
+ ...(token ? { Authorization: `Bearer ${token}` } : {}),
105
+ ...extra,
106
+ });
107
+ const guard = (id) => {
108
+ if (isUnsafeSessionId(id)) throw new Error(`unsafe session id: ${id}`);
109
+ return encodeURIComponent(id);
110
+ };
111
+
112
+ return {
113
+ kind: "http",
114
+ target: baseUrl,
115
+ async push(id, bytes) {
116
+ const res = await doFetch(`${baseUrl}/sessions/${guard(id)}`, {
117
+ method: "PUT",
118
+ headers: headers({ "Content-Type": "application/x-ndjson" }),
119
+ body: bytes,
120
+ });
121
+ if (!res.ok) throw new Error(`mirror PUT ${id} → HTTP ${res.status}`);
122
+ return { id, bytes: Buffer.byteLength(bytes) };
123
+ },
124
+ async pull(id) {
125
+ const res = await doFetch(`${baseUrl}/sessions/${guard(id)}`, {
126
+ headers: headers(),
127
+ });
128
+ if (res.status === 404) return null;
129
+ if (!res.ok) throw new Error(`mirror GET ${id} → HTTP ${res.status}`);
130
+ return res.text();
131
+ },
132
+ async list() {
133
+ const res = await doFetch(`${baseUrl}/sessions`, { headers: headers() });
134
+ if (!res.ok) throw new Error(`mirror list → HTTP ${res.status}`);
135
+ const data = await res.json();
136
+ const ids = Array.isArray(data) ? data : data?.sessions || [];
137
+ return ids
138
+ .map((x) => (typeof x === "string" ? x : x?.id))
139
+ .filter(Boolean);
140
+ },
141
+ };
142
+ }
@@ -0,0 +1,182 @@
1
+ /**
2
+ * Transcript integrity — hash-chained JSONL session transcripts.
3
+ *
4
+ * Every event appended to a session transcript carries `prevHash` (the hash of
5
+ * the previous event, or null for the first chained event) and `hash` — a
6
+ * SHA-256 over the previous hash plus the event's canonical core
7
+ * (`{type, timestamp, data}`). Editing, deleting, inserting, or reordering any
8
+ * chained line breaks the chain and is detected by `verifyTranscriptLines`.
9
+ *
10
+ * Design constraints:
11
+ * - Pure module: no fs, no paths — the store layer (jsonl-session-store.js)
12
+ * owns file IO so this stays cycle-free and unit-testable in isolation.
13
+ * - Backward compatible: transcripts written before chaining have no hash
14
+ * fields. A fully legacy file verifies as `legacy`; a legacy prefix followed
15
+ * by a valid chain is `partial` (chaining starts on the first upgraded
16
+ * append). An UNchained record appearing AFTER chained records is flagged as
17
+ * tampered — our writer never does that, so it is either a downgrade-write
18
+ * or a manual append, both of which the verifier must surface.
19
+ * - Honest limitation: truncating the file tail (deleting the newest N chained
20
+ * lines) is undetectable without an external anchor — the chain is
21
+ * self-contained. A half-written LAST line (crash mid-append) is reported as
22
+ * `truncatedTail: true` rather than tampered.
23
+ */
24
+
25
+ import { createHash } from "node:crypto";
26
+
27
+ export const TRANSCRIPT_CHAIN_STATUS = Object.freeze({
28
+ VERIFIED: "verified", // every record chained + chain valid
29
+ PARTIAL: "partial", // legacy prefix, then a valid chain
30
+ LEGACY: "legacy", // no chained records at all (pre-chaining transcript)
31
+ TAMPERED: "tampered", // chain broken: edit / delete / insert / reorder
32
+ EMPTY: "empty", // no records
33
+ });
34
+
35
+ /**
36
+ * Hash one event's canonical core against the previous chain hash.
37
+ * The core is rebuilt field-by-field (not the raw line) so the hash is
38
+ * independent of the presence/position of the prevHash/hash fields themselves;
39
+ * JSON.parse → JSON.stringify preserves the writer's key insertion order for
40
+ * `data`, so writer and verifier serialize identically.
41
+ */
42
+ export function computeEventHash(prevHash, core) {
43
+ const canonical = JSON.stringify({
44
+ type: core.type,
45
+ timestamp: core.timestamp,
46
+ data: core.data,
47
+ });
48
+ return createHash("sha256")
49
+ .update(`${prevHash || "genesis"}\n${canonical}`, "utf-8")
50
+ .digest("hex");
51
+ }
52
+
53
+ /**
54
+ * The hash the NEXT appended event must chain from: the `hash` of the last
55
+ * parseable line, or null when the file is empty / fully legacy / the last
56
+ * parseable line is unchained. A trailing half-written line (crash mid-append)
57
+ * is skipped so the writer chains from the last intact record.
58
+ */
59
+ export function latestChainHash(text) {
60
+ const lines = String(text || "").split("\n");
61
+ for (let i = lines.length - 1; i >= 0; i--) {
62
+ const line = lines[i];
63
+ if (!line.trim()) continue;
64
+ try {
65
+ const event = JSON.parse(line);
66
+ return typeof event?.hash === "string" ? event.hash : null;
67
+ } catch {
68
+ // half-written tail line — chain from the previous intact record
69
+ continue;
70
+ }
71
+ }
72
+ return null;
73
+ }
74
+
75
+ /**
76
+ * Verify a transcript's hash chain.
77
+ *
78
+ * @param {string[]} lines raw transcript lines (as split on "\n")
79
+ * @returns {{
80
+ * status: string, // TRANSCRIPT_CHAIN_STATUS value
81
+ * chainedEvents: number,
82
+ * legacyEvents: number,
83
+ * malformedLines: number,
84
+ * truncatedTail: boolean, // last non-empty line is half-written JSON
85
+ * firstInvalidLine: number|null, // 1-based line number of the first break
86
+ * reason: string|null,
87
+ * }}
88
+ */
89
+ export function verifyTranscriptLines(lines) {
90
+ const input = Array.isArray(lines) ? lines : [];
91
+ let lastNonEmpty = -1;
92
+ for (let i = input.length - 1; i >= 0; i--) {
93
+ if (input[i] && input[i].trim()) {
94
+ lastNonEmpty = i;
95
+ break;
96
+ }
97
+ }
98
+
99
+ const result = {
100
+ status: TRANSCRIPT_CHAIN_STATUS.EMPTY,
101
+ chainedEvents: 0,
102
+ legacyEvents: 0,
103
+ malformedLines: 0,
104
+ truncatedTail: false,
105
+ firstInvalidLine: null,
106
+ reason: null,
107
+ };
108
+
109
+ let lastHash = null;
110
+ let sawChain = false;
111
+
112
+ const tampered = (lineNo, reason) => {
113
+ result.status = TRANSCRIPT_CHAIN_STATUS.TAMPERED;
114
+ result.firstInvalidLine = lineNo;
115
+ result.reason = reason;
116
+ return result;
117
+ };
118
+
119
+ for (let i = 0; i < input.length; i++) {
120
+ const line = input[i];
121
+ if (!line || !line.trim()) continue;
122
+ const lineNo = i + 1;
123
+
124
+ let event;
125
+ try {
126
+ event = JSON.parse(line);
127
+ } catch {
128
+ result.malformedLines++;
129
+ if (i === lastNonEmpty) {
130
+ // Crash mid-append: the newest line is half-written. Not tampering —
131
+ // report it so callers can surface a warning.
132
+ result.truncatedTail = true;
133
+ continue;
134
+ }
135
+ if (sawChain) {
136
+ return tampered(lineNo, "malformed line inside hash chain");
137
+ }
138
+ continue; // junk inside the legacy region — validate reports it
139
+ }
140
+
141
+ if (event && typeof event.hash === "string") {
142
+ const expectedPrev = sawChain ? lastHash : null;
143
+ if ((event.prevHash ?? null) !== expectedPrev) {
144
+ return tampered(
145
+ lineNo,
146
+ sawChain
147
+ ? "hash chain linkage broken (record deleted, inserted, or reordered)"
148
+ : "chain does not start at a genesis record (head records removed)",
149
+ );
150
+ }
151
+ if (computeEventHash(expectedPrev, event) !== event.hash) {
152
+ return tampered(lineNo, "event content does not match its hash");
153
+ }
154
+ sawChain = true;
155
+ lastHash = event.hash;
156
+ result.chainedEvents++;
157
+ } else {
158
+ if (sawChain) {
159
+ return tampered(
160
+ lineNo,
161
+ "unchained record after hash chain started (manual append or downgrade write)",
162
+ );
163
+ }
164
+ result.legacyEvents++;
165
+ }
166
+ }
167
+
168
+ if (result.chainedEvents > 0) {
169
+ result.status =
170
+ result.legacyEvents > 0
171
+ ? TRANSCRIPT_CHAIN_STATUS.PARTIAL
172
+ : TRANSCRIPT_CHAIN_STATUS.VERIFIED;
173
+ } else if (result.legacyEvents > 0 || result.malformedLines > 0) {
174
+ result.status = TRANSCRIPT_CHAIN_STATUS.LEGACY;
175
+ }
176
+ return result;
177
+ }
178
+
179
+ /** Convenience wrapper for whole-file text. */
180
+ export function verifyTranscriptText(text) {
181
+ return verifyTranscriptLines(String(text || "").split("\n"));
182
+ }
package/src/index.js CHANGED
@@ -159,6 +159,12 @@ import { registerRemoteControlCommand } from "./commands/remote-control.js";
159
159
  // Agenda: consumer for agent-scheduled wakeups/crons/monitors (第四阶段 #3)
160
160
  import { registerAgendaCommand } from "./commands/agenda.js";
161
161
 
162
+ // Routines: named scheduled/triggered agent tasks with run history (gap-2026-07-11 P1#8)
163
+ import { registerRoutineCommand } from "./commands/routine.js";
164
+
165
+ // Cloud: self-hosted cloud handoff — offload a task to a private runner (gap-2026-07-11 P1#7)
166
+ import { registerCloudCommand } from "./commands/cloud.js";
167
+
162
168
  // Batch: dynamic worktree batch — split a change into parallel units (第四阶段 #4)
163
169
  import { registerBatchCommand } from "./commands/batch.js";
164
170
 
@@ -597,6 +603,8 @@ export function createProgram(opts = {}) {
597
603
 
598
604
  // Agenda: fire agent-scheduled wakeups/crons/monitors
599
605
  registerAgendaCommand(program);
606
+ registerRoutineCommand(program);
607
+ registerCloudCommand(program);
600
608
 
601
609
  // Batch: dynamic worktree batch
602
610
  registerBatchCommand(program);