chainlesschain 0.162.95 → 0.162.97

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 (219) hide show
  1. package/README.md +1 -1
  2. package/bin/chainlesschain.js +8 -18
  3. package/package.json +1 -1
  4. package/src/assets/web-panel/assets/{AIOps-DLvvkb9x.js → AIOps-C1YvIYh5.js} +1 -1
  5. package/src/assets/web-panel/assets/{ActionButton-BxhQTwqd.js → ActionButton-jqcXEakj.js} +1 -1
  6. package/src/assets/web-panel/assets/{Analytics-C4oUXzhG.js → Analytics-C8Xwqd3_.js} +3 -3
  7. package/src/assets/web-panel/assets/{AppLayout-CjzKJdv3.js → AppLayout-Dx5ypTfA.js} +5 -5
  8. package/src/assets/web-panel/assets/{Audit-B-MrugUs.js → Audit-1GZ4Tv2f.js} +1 -1
  9. package/src/assets/web-panel/assets/{Backup-ecIfEpIZ.js → Backup-CexR2hyk.js} +1 -1
  10. package/src/assets/web-panel/assets/{BaseInput-BUcSfWnx.js → BaseInput-DwAruKdy.js} +1 -1
  11. package/src/assets/web-panel/assets/{Chat-DIc1n_Lk.js → Chat-W2FkU5BY.js} +6 -6
  12. package/src/assets/web-panel/assets/{ChatBubbleRenderer-BGzg7oVA.js → ChatBubbleRenderer-BCLnhLfu.js} +1 -1
  13. package/src/assets/web-panel/assets/{Checkbox-BQ3Ueff-.js → Checkbox-Dkh5JY5B.js} +1 -1
  14. package/src/assets/web-panel/assets/{Codegen-CvSGLnqE.js → Codegen-LfYOgsdA.js} +1 -1
  15. package/src/assets/web-panel/assets/{Col-Br9IXZcE.js → Col-DEgketAC.js} +1 -1
  16. package/src/assets/web-panel/assets/{Community-tPuLOf0r.js → Community-LeFQMaZm.js} +1 -1
  17. package/src/assets/web-panel/assets/{Compact-vMWH73BE.js → Compact-CrlKYvBc.js} +1 -1
  18. package/src/assets/web-panel/assets/{Compliance-Ct9z699x.js → Compliance-ca_4PGYF.js} +1 -1
  19. package/src/assets/web-panel/assets/{Cowork-Bxs1kL1y.js → Cowork-BcMWqh2a.js} +3 -3
  20. package/src/assets/web-panel/assets/{Cron-QEpr2k4U.js → Cron-pVVEA3dw.js} +2 -2
  21. package/src/assets/web-panel/assets/{Crosschain-u4xbUw6O.js → Crosschain-CiKSGTqp.js} +1 -1
  22. package/src/assets/web-panel/assets/{DID-DXTEvyrW.js → DID-B2bGzdOj.js} +2 -2
  23. package/src/assets/web-panel/assets/{Dashboard-C4pZgzkg.js → Dashboard-sQ6jcgCn.js} +2 -2
  24. package/src/assets/web-panel/assets/{Dropdown-LRuqREGe.js → Dropdown-rFx_8P04.js} +1 -1
  25. package/src/assets/web-panel/assets/{EmailListRenderer-D7XI1yBM.js → EmailListRenderer-smw2hh6f.js} +1 -1
  26. package/src/assets/web-panel/assets/{FamilyGuardDashboard-3y_SuAMQ.js → FamilyGuardDashboard-CZhg2_S2.js} +1 -1
  27. package/src/assets/web-panel/assets/{Federation-DbjbJJCW.js → Federation-DQE-368G.js} +1 -1
  28. package/src/assets/web-panel/assets/{FormItemContext-WPpMZcUw.js → FormItemContext-2XPb9izr.js} +1 -1
  29. package/src/assets/web-panel/assets/{GenericCardRenderer-nNT9Tn4E.js → GenericCardRenderer-DCGMz5E6.js} +1 -1
  30. package/src/assets/web-panel/assets/{Git-DqDaVzwV.js → Git-BFwrxK4Q.js} +2 -2
  31. package/src/assets/web-panel/assets/{Governance-BquBZpcg.js → Governance-ChxBRwG1.js} +1 -1
  32. package/src/assets/web-panel/assets/{Inference-RcPPYijs.js → Inference-C7kyknu_.js} +1 -1
  33. package/src/assets/web-panel/assets/{KnowledgeGraph-ByXfZV13.js → KnowledgeGraph-BsMd4XAt.js} +1 -1
  34. package/src/assets/web-panel/assets/{Logs-CHT3DUcq.js → Logs-Bs3yrpow.js} +2 -2
  35. package/src/assets/web-panel/assets/{Marketplace-Bi0HJ9pN.js → Marketplace-Cc29jdBv.js} +1 -1
  36. package/src/assets/web-panel/assets/{McpTools-QCBxPRRc.js → McpTools-zYpYsUsl.js} +4 -4
  37. package/src/assets/web-panel/assets/{Memory-D4RlxGXn.js → Memory-x9r4yKtS.js} +2 -2
  38. package/src/assets/web-panel/assets/{MobileBridge-CU_PV7e-.js → MobileBridge-CjUo8prx.js} +2 -2
  39. package/src/assets/web-panel/assets/{MobileProjects-c1CMFD8z.js → MobileProjects-Cmz4wVrF.js} +1 -1
  40. package/src/assets/web-panel/assets/{Mtc-BezDB1-s.js → Mtc-Y-SBnNxm.js} +5 -5
  41. package/src/assets/web-panel/assets/{MtcAudit-CgLpiHpE.js → MtcAudit-ogiKnrxj.js} +2 -2
  42. package/src/assets/web-panel/assets/{Multisig-vx6bz-bA.js → Multisig-C1hygkFR.js} +3 -3
  43. package/src/assets/web-panel/assets/{NLProgramming-CGOagg3-.js → NLProgramming-fWWYiTsY.js} +1 -1
  44. package/src/assets/web-panel/assets/{Notes-BcuDGYRS.js → Notes-CkCNF4gC.js} +3 -3
  45. package/src/assets/web-panel/assets/{NotificationSettings-ZAGCDUpc.js → NotificationSettings-DThB7WQG.js} +1 -1
  46. package/src/assets/web-panel/assets/{OrderTableRenderer-B5onE0eX.js → OrderTableRenderer-Dh64aMq6.js} +1 -1
  47. package/src/assets/web-panel/assets/{Organization-y_y20-5Y.js → Organization-BSY16zvP.js} +4 -4
  48. package/src/assets/web-panel/assets/{Overflow-BEICS31D.js → Overflow-C_TDh18K.js} +1 -1
  49. package/src/assets/web-panel/assets/{P2P-D_WRMzh4.js → P2P-CvxZk6og.js} +2 -2
  50. package/src/assets/web-panel/assets/PdhVaultBrowser-0JZ5JF4M.js +7 -0
  51. package/src/assets/web-panel/assets/{Permissions-3aqCc_4f.js → Permissions-DV8ubpjM.js} +4 -4
  52. package/src/assets/web-panel/assets/{PersonalDataHub-DT3R9g9Y.js → PersonalDataHub-g2Dgls8e.js} +2 -2
  53. package/src/assets/web-panel/assets/{Pipeline-MiMWIhw0.js → Pipeline-DGp8zQ0C.js} +1 -1
  54. package/src/assets/web-panel/assets/{Privacy-C3JsLuDa.js → Privacy-C0uHrjWR.js} +1 -1
  55. package/src/assets/web-panel/assets/{ProjectInit-EhHVi1ff.js → ProjectInit-2XGICaFt.js} +2 -2
  56. package/src/assets/web-panel/assets/{ProjectSettings-D_dNEA37.js → ProjectSettings-Cfy72e0Q.js} +2 -2
  57. package/src/assets/web-panel/assets/{Projects-CXydUXp8.js → Projects-Xsvk_kKT.js} +1 -1
  58. package/src/assets/web-panel/assets/{Providers-CPEhi2iZ.js → Providers-vuvDVg7U.js} +1 -1
  59. package/src/assets/web-panel/assets/{QuickAsk-BFQpMleX.js → QuickAsk-BUy7kPyQ.js} +1 -1
  60. package/src/assets/web-panel/assets/{Recommend-DzbovRC8.js → Recommend-DlzpC_JE.js} +1 -1
  61. package/src/assets/web-panel/assets/{Reputation-DoBeV3TD.js → Reputation-BiWFwPMh.js} +1 -1
  62. package/src/assets/web-panel/assets/{Row-DthhwX_B.js → Row-dmfZBK-k.js} +1 -1
  63. package/src/assets/web-panel/assets/{RssFeed-Dsf5U0lP.js → RssFeed-Cf6tlRKq.js} +2 -2
  64. package/src/assets/web-panel/assets/{Search-CJPB8kKl.js → Search-DhzXDRAJ.js} +1 -1
  65. package/src/assets/web-panel/assets/{Security-DI9MfpdD.js → Security-C-bTfgeJ.js} +3 -3
  66. package/src/assets/web-panel/assets/{Services-BdG0TFGf.js → Services-D3KNwBqu.js} +2 -2
  67. package/src/assets/web-panel/assets/{Skeleton-DSYazY_B.js → Skeleton-CHH8i1xY.js} +1 -1
  68. package/src/assets/web-panel/assets/{Skills-BD309VTT.js → Skills-DHiONX0z.js} +1 -1
  69. package/src/assets/web-panel/assets/{Sla-CaUlw4_6.js → Sla-DwWMb-Ui.js} +1 -1
  70. package/src/assets/web-panel/assets/{SpeechSettings-CZAPkR7H.js → SpeechSettings-DAq64iWB.js} +1 -1
  71. package/src/assets/web-panel/assets/{SyncSettings-CFRSowN3.js → SyncSettings-DJU-kHko.js} +2 -2
  72. package/src/assets/web-panel/assets/{Tasks-Di8EOluc.js → Tasks-RAIzA3u-.js} +1 -1
  73. package/src/assets/web-panel/assets/{Templates-B5FTziwi.js → Templates-By4cbadT.js} +1 -1
  74. package/src/assets/web-panel/assets/{Tenant-Dj3XYfBg.js → Tenant-kCAXsxWu.js} +1 -1
  75. package/src/assets/web-panel/assets/{Terminal-CznH_rJT.js → Terminal-D8pup_CO.js} +2 -2
  76. package/src/assets/web-panel/assets/{TimelineRenderer-DTHzaXPt.js → TimelineRenderer-Bk2c9TUI.js} +1 -1
  77. package/src/assets/web-panel/assets/{Tokens-Bo56nuML.js → Tokens-B0wTiZRa.js} +1 -1
  78. package/src/assets/web-panel/assets/{Trigger-pyVUVBwb.js → Trigger-Ca_9t0gk.js} +1 -1
  79. package/src/assets/web-panel/assets/{Trust-Cp7jMkKN.js → Trust-BnfNDDDm.js} +1 -1
  80. package/src/assets/web-panel/assets/{UkeySign-aPcZCdpz.js → UkeySign-fnlF2-Yr.js} +1 -1
  81. package/src/assets/web-panel/assets/{VideoEditing-DAv5j--w.js → VideoEditing-Cfshhzyw.js} +1 -1
  82. package/src/assets/web-panel/assets/{Wallet-DRhK7IDR.js → Wallet-BWMpDOLG.js} +4 -4
  83. package/src/assets/web-panel/assets/{WebAuthn-B2yQJuq8.js → WebAuthn-B6xeXZvw.js} +3 -3
  84. package/src/assets/web-panel/assets/{WorkflowEditor-D98DhR54.js → WorkflowEditor-Cb3-FPCE.js} +1 -1
  85. package/src/assets/web-panel/assets/{chat-BXrRNi8C.js → chat-CP_opuMK.js} +1 -1
  86. package/src/assets/web-panel/assets/{colors-jQ94T_qn.js → colors-RLzfjxRQ.js} +1 -1
  87. package/src/assets/web-panel/assets/{compact-item-KqUJLlF2.js → compact-item-BTRloNKV.js} +1 -1
  88. package/src/assets/web-panel/assets/{createContext-DxM3zuQW.js → createContext-gXgXTLlU.js} +1 -1
  89. package/src/assets/web-panel/assets/devWarning-CYlmApo_.js +1 -0
  90. package/src/assets/web-panel/assets/{hasIn-BGm946KV.js → hasIn-CrIpxkRC.js} +1 -1
  91. package/src/assets/web-panel/assets/{index-CbQvgx2e.js → index-AsI1SqnE.js} +1 -1
  92. package/src/assets/web-panel/assets/{index-C5NQai7O.js → index-B3L5vTSs.js} +1 -1
  93. package/src/assets/web-panel/assets/{index-Crw76vIF.js → index-B5sN_pwd.js} +1 -1
  94. package/src/assets/web-panel/assets/{index-1P0sXNhG.js → index-BEcRPNHv.js} +1 -1
  95. package/src/assets/web-panel/assets/{index-Ce-YoL4x.js → index-BVrcoJe2.js} +1 -1
  96. package/src/assets/web-panel/assets/{index-BaU_fSK7.js → index-BewxG36A.js} +1 -1
  97. package/src/assets/web-panel/assets/{index-BGjCjsf1.js → index-Bi87H9yk.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-DZpVYD4j.js → index-Bl4OQ--4.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-yavsyHif.js → index-BlE2lCS-.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-B4b7KjRq.js → index-BlS91Ac0.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-BbS0cBU9.js → index-Bm0cNeeQ.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-LWTZAud0.js → index-Bo2Fl2j7.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-DZMzj4ay.js → index-Bsz3X-aU.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-DOYcemym.js → index-BurvDWMS.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-T4UJiTi3.js → index-BzBLtthb.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-C7Z2m28C.js → index-C0wmEN2j.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-DSzb9VOG.js → index-C4bBR-Vd.js} +1 -1
  108. package/src/assets/web-panel/assets/{index-CfE3jxrT.js → index-COhACL0I.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-FSYz5aww.js → index-CQ-cnhZM.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-BXMZLfd8.js → index-CRr99L7q.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-DWcd5zDG.js → index-CaTkUkJp.js} +3 -3
  112. package/src/assets/web-panel/assets/{index-D1VWL6Lc.js → index-CfOjPxso.js} +1 -1
  113. package/src/assets/web-panel/assets/index-Cf_SLgtj.js +1 -0
  114. package/src/assets/web-panel/assets/{index-mZNLT2SP.js → index-Ch_TYQNW.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-B-2HpfWo.js → index-CisGGaF3.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-CghRL1dh.js → index-DA-_2qTs.js} +1 -1
  117. package/src/assets/web-panel/assets/index-DYBIDVGn.js +1 -0
  118. package/src/assets/web-panel/assets/{index-Dgzw6eKr.js → index-DZxK7MW9.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-CreXAr5Y.js → index-DiXnGU0A.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-aOLXuHpG.js → index-DsiyfyBE.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-DboUBf6R.js → index-G_OgtHiC.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-BnEZfi5D.js → index-IjV-dnE8.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-BAPk6ntc.js → index-Mltfj22U.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-DTUs9t5F.js → index-U-kmEJdu.js} +1 -1
  125. package/src/assets/web-panel/assets/{index-BMp41Q1Y.js → index-cgf-OVry.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-Cz_SFSix.js → index-ch8zcf-h.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-DRzsaWQy.js → index-cqOT5RiJ.js} +1 -1
  128. package/src/assets/web-panel/assets/{index-C7sBLech.js → index-cywkZJ-I.js} +1 -1
  129. package/src/assets/web-panel/assets/{index-Dib0FsTT.js → index-sE_I7gVL.js} +1 -1
  130. package/src/assets/web-panel/assets/{initDefaultProps-BDoZJI7g.js → initDefaultProps-DMwnVZzH.js} +1 -1
  131. package/src/assets/web-panel/assets/{motion-sceLjgp-.js → motion-Dnpjpvo7.js} +1 -1
  132. package/src/assets/web-panel/assets/{move-CTWQpFa7.js → move-iuRh5U9Y.js} +1 -1
  133. package/src/assets/web-panel/assets/{omit-dr-NCrHZ.js → omit-Bl4WRV59.js} +1 -1
  134. package/src/assets/web-panel/assets/{pickAttrs-DK-GX_Z2.js → pickAttrs-B9KSXaep.js} +1 -1
  135. package/src/assets/web-panel/assets/{placementArrow-BIPf--18.js → placementArrow-TEGIow4r.js} +1 -1
  136. package/src/assets/web-panel/assets/{responsiveObserve-BzrWT5tV.js → responsiveObserve--BECM92v.js} +1 -1
  137. package/src/assets/web-panel/assets/{slide-D5XhxlET.js → slide-Ca8G7XbL.js} +1 -1
  138. package/src/assets/web-panel/assets/{statusUtils-6DI-k-9E.js → statusUtils-8QPG_T3D.js} +1 -1
  139. package/src/assets/web-panel/assets/{styleChecker-DMw-eyn9.js → styleChecker-p3FsA6mg.js} +1 -1
  140. package/src/assets/web-panel/assets/{useFlexGapSupport-D4oVdliM.js → useFlexGapSupport-Ct0yIzrp.js} +1 -1
  141. package/src/assets/web-panel/assets/{useFs-D3PPZnvy.js → useFs-3nlByl_3.js} +1 -1
  142. package/src/assets/web-panel/assets/{usePersonalDataHub-Gh5ba4KP.js → usePersonalDataHub-N3OsoSRM.js} +1 -1
  143. package/src/assets/web-panel/assets/{vnode-CVEM1u4x.js → vnode-Cfe3ywq3.js} +1 -1
  144. package/src/assets/web-panel/assets/{zoom-ClT_-9RZ.js → zoom-DVBuDS_3.js} +1 -1
  145. package/src/assets/web-panel/index.html +1 -1
  146. package/src/commands/activitypub.js +4 -3
  147. package/src/commands/agent.js +25 -0
  148. package/src/commands/agents.js +18 -4
  149. package/src/commands/ask.js +13 -0
  150. package/src/commands/command.js +12 -2
  151. package/src/commands/config.js +17 -1
  152. package/src/commands/cost.js +6 -1
  153. package/src/commands/dao.js +4 -3
  154. package/src/commands/dlp.js +2 -1
  155. package/src/commands/hub.js +138 -0
  156. package/src/commands/mtc.js +37 -1
  157. package/src/gateways/discord/discord-formatter.js +18 -1
  158. package/src/gateways/telegram/telegram-formatter.js +6 -3
  159. package/src/gateways/ws/ws-server.js +19 -1
  160. package/src/harness/plugin-manager.js +20 -0
  161. package/src/harness/worktree-isolator.js +17 -1
  162. package/src/index.js +2 -0
  163. package/src/lib/__tests__/model-deprecation.test.js +194 -0
  164. package/src/lib/a2a-protocol.js +24 -9
  165. package/src/lib/agent-core.js +4 -5
  166. package/src/lib/audit-mtc.js +40 -2
  167. package/src/lib/chat-core.js +146 -18
  168. package/src/lib/cli-anything-bridge.js +5 -0
  169. package/src/lib/cli-numeric.js +38 -0
  170. package/src/lib/config-manager.js +37 -6
  171. package/src/lib/cowork/ab-comparator-cli.js +6 -2
  172. package/src/lib/cowork-cron.js +45 -34
  173. package/src/lib/cowork-learning.js +31 -1
  174. package/src/lib/cowork-share.js +39 -3
  175. package/src/lib/cowork-template-marketplace.js +54 -2
  176. package/src/lib/cowork-workflow.js +33 -6
  177. package/src/lib/cross-chain-mtc.js +41 -12
  178. package/src/lib/crypto-manager.js +12 -2
  179. package/src/lib/dbevo.js +19 -8
  180. package/src/lib/downloader.js +24 -9
  181. package/src/lib/evomap-governance.js +4 -4
  182. package/src/lib/evomap-manager.js +54 -7
  183. package/src/lib/fatal-handler.js +48 -0
  184. package/src/lib/file-checkpoint.js +26 -0
  185. package/src/lib/git-integration.js +28 -0
  186. package/src/lib/goal-store.js +36 -10
  187. package/src/lib/hierarchical-memory.js +6 -0
  188. package/src/lib/llm-pricing.js +52 -2
  189. package/src/lib/mcp-oauth.js +37 -7
  190. package/src/lib/memory-manager.js +23 -2
  191. package/src/lib/model-deprecation.js +249 -0
  192. package/src/lib/pdh-feedback-ledger.js +131 -0
  193. package/src/lib/permanent-memory.js +30 -2
  194. package/src/lib/permission-rules.cjs +11 -3
  195. package/src/lib/personal-data-hub-aichat-wizard.js +33 -3
  196. package/src/lib/personal-data-hub-wiring.js +39 -16
  197. package/src/lib/project-instructions.js +10 -4
  198. package/src/lib/runnable-provider.js +62 -31
  199. package/src/lib/session-usage.js +34 -1
  200. package/src/lib/skill-loader.js +4 -1
  201. package/src/lib/skill-packs/generator.js +25 -2
  202. package/src/lib/stream-retry.js +56 -0
  203. package/src/lib/sync-credentials.js +53 -17
  204. package/src/lib/token-tracker.js +12 -2
  205. package/src/lib/with-file-lock.js +87 -0
  206. package/src/repl/agent-repl.js +47 -7
  207. package/src/repl/chat-repl.js +19 -0
  208. package/src/repl/session-cost.js +31 -3
  209. package/src/runtime/agent-core.js +478 -53
  210. package/src/runtime/coding-agent-contract-shared.cjs +58 -1
  211. package/src/runtime/coding-agent-policy.cjs +10 -0
  212. package/src/runtime/file-ref-expander.js +29 -1
  213. package/src/runtime/headless-runner.js +12 -1
  214. package/src/runtime/headless-stream.js +200 -9
  215. package/src/skills/video-editing/tools/commit.js +15 -1
  216. package/src/assets/web-panel/assets/PdhVaultBrowser-C8f5NbbI.js +0 -7
  217. package/src/assets/web-panel/assets/devWarning-Bo1r5_zX.js +0 -1
  218. package/src/assets/web-panel/assets/index-BCU6dVQ-.js +0 -1
  219. package/src/assets/web-panel/assets/index-N_oOFphx.js +0 -1
@@ -305,8 +305,8 @@ export function createGovernanceProposalV2(db, options = {}) {
305
305
  throw new Error(`Unknown proposal type: ${proposalType}`);
306
306
  }
307
307
 
308
- const quorumValue = typeof quorum === "number" ? quorum : 3;
309
- const thresholdValue = typeof threshold === "number" ? threshold : 0.5;
308
+ const quorumValue = Number.isFinite(quorum) ? quorum : 3;
309
+ const thresholdValue = Number.isFinite(threshold) ? threshold : 0.5;
310
310
  if (quorumValue < 1) throw new Error("Quorum must be >= 1");
311
311
  if (thresholdValue <= 0 || thresholdValue > 1) {
312
312
  throw new Error("Threshold must be in (0, 1]");
@@ -370,7 +370,7 @@ export function castVoteV2(db, options = {}) {
370
370
  if (!Object.values(VOTE_DIRECTION).includes(direction)) {
371
371
  throw new Error(`Unknown vote direction: ${direction}`);
372
372
  }
373
- const weightValue = typeof weight === "number" ? weight : 1;
373
+ const weightValue = Number.isFinite(weight) ? weight : 1;
374
374
  if (weightValue <= 0) throw new Error("Vote weight must be positive");
375
375
 
376
376
  if (direction === VOTE_DIRECTION.FOR) {
@@ -456,7 +456,7 @@ export function cancelProposal(db, proposalId) {
456
456
  }
457
457
 
458
458
  export function expireProposalsV2(db, now) {
459
- const cutoff = typeof now === "number" ? now : Date.now();
459
+ const cutoff = Number.isFinite(now) ? now : Date.now();
460
460
  const expired = [];
461
461
  for (const p of _proposals.values()) {
462
462
  if (p.status === PROPOSAL_STATUS_V2.ACTIVE) {
@@ -14,6 +14,53 @@ export const _deps = {
14
14
  path,
15
15
  };
16
16
 
17
+ /**
18
+ * Atomically write a UTF-8 file via _deps.fs. A gene's gene.json / content.md
19
+ * are persistent store state; a crash mid-write truncates them and getGene's
20
+ * JSON.parse then throws. Temp sibling + rename (atomic within a filesystem).
21
+ * Graceful-degrade to a direct write when the injected fs has no renameSync
22
+ * (partial test mocks); the real fs always has it, so production is atomic.
23
+ */
24
+ function _atomicWriteFile(filePath, data) {
25
+ const fsx = _deps.fs;
26
+ if (typeof fsx.renameSync !== "function") {
27
+ fsx.writeFileSync(filePath, data, "utf-8");
28
+ return;
29
+ }
30
+ const tmp = `${filePath}.${process.pid}.${Math.random().toString(36).slice(2, 8)}.tmp`;
31
+ try {
32
+ fsx.writeFileSync(tmp, data, "utf-8");
33
+ fsx.renameSync(tmp, filePath);
34
+ } catch (err) {
35
+ try {
36
+ if (fsx.existsSync(tmp) && fsx.unlinkSync) fsx.unlinkSync(tmp);
37
+ } catch {
38
+ /* best-effort temp cleanup */
39
+ }
40
+ throw err;
41
+ }
42
+ }
43
+
44
+ /**
45
+ * A gene id is interpolated into a filesystem path (genesDir/<id>/...). Gene ids
46
+ * arrive from downloaded/installed genes (an external hub / MCP response), so a
47
+ * malicious `id` like "../../.ssh/authorized_keys" or "C:\\Windows\\..." would
48
+ * escape genesDir and read/overwrite/delete arbitrary files. Restrict it to a
49
+ * single safe path segment (no separators, no traversal, no drive/NUL).
50
+ */
51
+ function _isSafeGeneId(id) {
52
+ return (
53
+ typeof id === "string" &&
54
+ id.length > 0 &&
55
+ id !== "." &&
56
+ id !== ".." &&
57
+ !id.includes("/") &&
58
+ !id.includes("\\") &&
59
+ !id.includes(":") &&
60
+ !id.includes("\0")
61
+ );
62
+ }
63
+
17
64
  export class EvoMapManager {
18
65
  /**
19
66
  * @param {object} options
@@ -71,6 +118,9 @@ export class EvoMapManager {
71
118
  saveGene(gene, content) {
72
119
  this.initialize();
73
120
  if (!gene || !gene.id) throw new Error("Gene ID required");
121
+ if (!_isSafeGeneId(gene.id)) {
122
+ throw new Error(`Unsafe gene id (path traversal): ${gene.id}`);
123
+ }
74
124
 
75
125
  const geneDir = _deps.path.join(this.genesDir, gene.id);
76
126
  if (!_deps.fs.existsSync(geneDir)) {
@@ -78,19 +128,14 @@ export class EvoMapManager {
78
128
  }
79
129
 
80
130
  // Save metadata
81
- _deps.fs.writeFileSync(
131
+ _atomicWriteFile(
82
132
  _deps.path.join(geneDir, "gene.json"),
83
133
  JSON.stringify(gene, null, 2),
84
- "utf-8",
85
134
  );
86
135
 
87
136
  // Save content
88
137
  if (content) {
89
- _deps.fs.writeFileSync(
90
- _deps.path.join(geneDir, "content.md"),
91
- content,
92
- "utf-8",
93
- );
138
+ _atomicWriteFile(_deps.path.join(geneDir, "content.md"), content);
94
139
  }
95
140
 
96
141
  // Register in DB
@@ -170,6 +215,7 @@ export class EvoMapManager {
170
215
  */
171
216
  getGene(geneId) {
172
217
  this.initialize();
218
+ if (!_isSafeGeneId(geneId)) return null;
173
219
  const geneDir = _deps.path.join(this.genesDir, geneId);
174
220
 
175
221
  if (!_deps.fs.existsSync(geneDir)) return null;
@@ -192,6 +238,7 @@ export class EvoMapManager {
192
238
  */
193
239
  removeGene(geneId) {
194
240
  this.initialize();
241
+ if (!_isSafeGeneId(geneId)) return { removed: false };
195
242
  const geneDir = _deps.path.join(this.genesDir, geneId);
196
243
 
197
244
  if (_deps.fs.existsSync(geneDir)) {
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Centralized friendly-error boundary for the CLI entrypoint.
3
+ *
4
+ * An uncaught error thrown from a command action (malformed --json, a failed DB
5
+ * op) — or, crucially, one that escapes into an async event handler / detached
6
+ * task / third-party lib AFTER the top-level parse — otherwise surfaces as a raw
7
+ * stack trace via Node's default unhandledRejection/uncaughtException, which on
8
+ * modern Node terminates the process. Route all of those through one place that
9
+ * prints a clean `error: <message>` (full stack under --verbose or
10
+ * CC_DEBUG/DEBUG) and exits non-zero.
11
+ *
12
+ * Pure-ish: process bindings are injectable so this is unit-testable.
13
+ */
14
+
15
+ export function reportFatal(
16
+ err,
17
+ {
18
+ stderr = process.stderr,
19
+ exit = process.exit,
20
+ argv = process.argv,
21
+ env = process.env,
22
+ } = {},
23
+ ) {
24
+ const verbose =
25
+ argv.includes("--verbose") || Boolean(env.CC_DEBUG) || Boolean(env.DEBUG);
26
+ if (verbose && err && err.stack) {
27
+ stderr.write(`${err.stack}\n`);
28
+ } else {
29
+ const msg = err && err.message ? err.message : String(err);
30
+ stderr.write(`error: ${msg}\n`);
31
+ }
32
+ exit(1);
33
+ }
34
+
35
+ /**
36
+ * Install process-level safety nets that funnel unhandled rejections and
37
+ * uncaught exceptions through `report`. Node terminates on these by default, so
38
+ * this only swaps the ugly default stack for the friendly boundary — it is not
39
+ * a behavior change (still exits non-zero).
40
+ */
41
+ export function installGlobalErrorHandlers(proc = process, report = reportFatal) {
42
+ proc.on("unhandledRejection", (reason) =>
43
+ report(reason instanceof Error ? reason : new Error(String(reason))),
44
+ );
45
+ proc.on("uncaughtException", (err) =>
46
+ report(err instanceof Error ? err : new Error(String(err))),
47
+ );
48
+ }
@@ -80,6 +80,27 @@ function newId() {
80
80
  return `cp-${Date.now()}-${rand}`;
81
81
  }
82
82
 
83
+ /**
84
+ * A checkpoint id is interpolated into filesystem paths (root/<id>.json and the
85
+ * blob dir root/<id>). The id comes from CLI args (`cc checkpoint show|delete
86
+ * <id>`), so an id like "../../etc/passwd" would read outside the checkpoint
87
+ * store, and "../../important" passed to delete would rmSync outside it. The git
88
+ * engine guards this via sanitizeSession(); mirror it for the copy fallback by
89
+ * restricting the id to a single safe path segment.
90
+ */
91
+ function isSafeCheckpointId(id) {
92
+ return (
93
+ typeof id === "string" &&
94
+ id.length > 0 &&
95
+ id !== "." &&
96
+ id !== ".." &&
97
+ !id.includes("/") &&
98
+ !id.includes("\\") &&
99
+ !id.includes(":") &&
100
+ !id.includes("\0")
101
+ );
102
+ }
103
+
83
104
  /**
84
105
  * Recursively collect regular files under an absolute path, honoring SKIP_DIRS
85
106
  * and a running maxFiles budget. Symlinks are not followed.
@@ -146,6 +167,9 @@ export function createCheckpoint(paths, opts = {}) {
146
167
  const uniqueAbs = [...new Set(absFiles)];
147
168
 
148
169
  const id = opts.id || newId();
170
+ if (!isSafeCheckpointId(id)) {
171
+ throw new Error(`Unsafe checkpoint id (path traversal): ${id}`);
172
+ }
149
173
  const blobDir = path.join(root, id);
150
174
  ensureDir(blobDir);
151
175
 
@@ -181,6 +205,7 @@ export function createCheckpoint(paths, opts = {}) {
181
205
 
182
206
  /** Load a checkpoint manifest by id, or null. */
183
207
  export function getCheckpoint(id, opts = {}) {
208
+ if (!isSafeCheckpointId(id)) return null;
184
209
  const root = opts.root || defaultRoot();
185
210
  const file = path.join(root, `${id}.json`);
186
211
  if (!fs.existsSync(file)) return null;
@@ -310,6 +335,7 @@ export function restoreCheckpoint(id, opts = {}) {
310
335
 
311
336
  /** Delete a checkpoint (manifest + blobs). Returns true if it existed. */
312
337
  export function deleteCheckpoint(id, opts = {}) {
338
+ if (!isSafeCheckpointId(id)) return false;
313
339
  const root = opts.root || defaultRoot();
314
340
  const file = path.join(root, `${id}.json`);
315
341
  const blobDir = path.join(root, id);
@@ -13,6 +13,34 @@ export function isGitRepo(dir) {
13
13
  return existsSync(join(dir, ".git"));
14
14
  }
15
15
 
16
+ /**
17
+ * Reject a git ref (branch / base / tag) that could break out of a
18
+ * shell-interpolated `git` command. gitExec interpolates its args into a shell
19
+ * string, so a ref like `x$(rm -rf ~)`, a backtick, or `--upload-pack=evil`
20
+ * would otherwise execute or inject a flag (double-quoting does NOT stop `$()`
21
+ * / backticks). Legit refs are branch/tag names — alphanumerics plus `. _ / -`;
22
+ * `HEAD` and `origin/main` pass. Throws on anything else. Exported for reuse +
23
+ * tests; callers validate caller/agent-supplied refs before interpolation.
24
+ *
25
+ * @param {string} ref
26
+ * @param {string} [label="git ref"]
27
+ */
28
+ export function assertSafeGitRef(ref, label = "git ref") {
29
+ if (typeof ref !== "string" || ref.length === 0) {
30
+ throw new Error(`Invalid ${label}: expected a non-empty string`);
31
+ }
32
+ if (
33
+ !/^[A-Za-z0-9._/-]+$/.test(ref) || // only ref-safe chars (no space/shell metachars)
34
+ ref.startsWith("-") || // block flag injection (e.g. --upload-pack=…)
35
+ ref.includes("..") || // forbidden in refnames; also blocks range tricks
36
+ ref.includes("//") ||
37
+ ref.endsWith("/") ||
38
+ ref.endsWith(".lock")
39
+ ) {
40
+ throw new Error(`Unsafe ${label}: ${JSON.stringify(ref)}`);
41
+ }
42
+ }
43
+
16
44
  /**
17
45
  * Run a git command and return stdout.
18
46
  */
@@ -19,6 +19,7 @@
19
19
  import fs from "node:fs";
20
20
  import path from "node:path";
21
21
  import { getHomeDir } from "./paths.js";
22
+ import { withFileLock } from "./with-file-lock.js";
22
23
 
23
24
  /** Valid goal lifecycle states. `active` goals are the ones injected. */
24
25
  export const GOAL_STATUS = Object.freeze({
@@ -38,6 +39,28 @@ function ensureDir(dir) {
38
39
  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
39
40
  }
40
41
 
42
+ /**
43
+ * Write a goal file atomically: a crash mid-write must not truncate/corrupt an
44
+ * existing goal (an unparseable goal file is silently dropped by getGoal's catch
45
+ * → the user loses that goal). Temp sibling + rename (atomic within a
46
+ * filesystem). Temp names end in `.tmp` (never `.json`), so a hard-crash
47
+ * leftover is ignored by listGoals.
48
+ */
49
+ function atomicWriteFileSync(filePath, data) {
50
+ const tmp = `${filePath}.${process.pid}.${Math.random().toString(36).slice(2, 8)}.tmp`;
51
+ try {
52
+ fs.writeFileSync(tmp, data, "utf-8");
53
+ fs.renameSync(tmp, filePath);
54
+ } catch (err) {
55
+ try {
56
+ if (fs.existsSync(tmp)) fs.unlinkSync(tmp);
57
+ } catch {
58
+ /* best-effort temp cleanup */
59
+ }
60
+ throw err;
61
+ }
62
+ }
63
+
41
64
  function newId(prefix) {
42
65
  // Date.now/random are fine here (plain CLI lib, not a resumable workflow).
43
66
  const rand = Math.random().toString(36).slice(2, 8);
@@ -98,7 +121,7 @@ export function createGoal(input = {}, opts = {}) {
98
121
  updatedAt: nowIso(),
99
122
  };
100
123
  ensureDir(root);
101
- fs.writeFileSync(goalFile(root, id), JSON.stringify(goal, null, 2), "utf-8");
124
+ atomicWriteFileSync(goalFile(root, id), JSON.stringify(goal, null, 2));
102
125
  return goal;
103
126
  }
104
127
 
@@ -137,11 +160,7 @@ function saveGoal(goal, opts = {}) {
137
160
  const root = opts.root || defaultRoot();
138
161
  ensureDir(root);
139
162
  goal.updatedAt = nowIso();
140
- fs.writeFileSync(
141
- goalFile(root, goal.id),
142
- JSON.stringify(goal, null, 2),
143
- "utf-8",
144
- );
163
+ atomicWriteFileSync(goalFile(root, goal.id), JSON.stringify(goal, null, 2));
145
164
  return goal;
146
165
  }
147
166
 
@@ -162,10 +181,17 @@ export function listGoals(opts = {}) {
162
181
 
163
182
  /** Mutate a goal via `fn(goal)`; throws if not found. */
164
183
  function mutate(id, fn, opts = {}) {
165
- const goal = getGoal(id, opts);
166
- if (!goal) throw new Error(`no such goal: ${id}`);
167
- fn(goal);
168
- return saveGoal(goal, opts);
184
+ // Serialize the read-modify-write across processes (cc+cc, or cc+desktop
185
+ // sharing the goals dir) so concurrent goal edits don't lose an update.
186
+ // Best-effort lock (with-file-lock): on contention timeout it proceeds anyway,
187
+ // so the CLI never hangs.
188
+ const root = opts.root || defaultRoot();
189
+ return withFileLock(goalFile(root, id), () => {
190
+ const goal = getGoal(id, opts);
191
+ if (!goal) throw new Error(`no such goal: ${id}`);
192
+ fn(goal);
193
+ return saveGoal(goal, opts);
194
+ });
169
195
  }
170
196
 
171
197
  /** Add a key result to a goal. */
@@ -1006,6 +1006,12 @@ export function startConsolidationTimer({
1006
1006
  }
1007
1007
  }
1008
1008
  }, intervalMs);
1009
+ // Background maintenance must not by itself keep the CLI process alive (the
1010
+ // process should exit once foreground work is done); matches the unref
1011
+ // discipline used by downloader / PtyManager / background-task-manager.
1012
+ if (_consolidationTimer && typeof _consolidationTimer.unref === "function") {
1013
+ _consolidationTimer.unref();
1014
+ }
1009
1015
  return _consolidationTimer;
1010
1016
  }
1011
1017
 
@@ -30,7 +30,19 @@ export const FREE_PROVIDERS = Object.freeze([
30
30
  */
31
31
  export const PRICE_TABLE = Object.freeze({
32
32
  anthropic: [
33
- { match: "opus", in: 15, out: 75 },
33
+ // Current Anthropic list prices (USD per 1M tokens), verified 2026-06-21
34
+ // against the claude-api reference. The Opus tier dropped to $5/$25 with
35
+ // Opus 4.5 (Nov 2025) — the old $15/$75 applies only to the retired
36
+ // Opus 4 / 4.1. Longest-pattern-first matching (see matchRate) resolves
37
+ // dated ids to the specific rate; bare "opus" defaults to the current price.
38
+ { match: "fable-5", in: 10, out: 50 },
39
+ { match: "mythos-5", in: 10, out: 50 },
40
+ { match: "opus-4-8", in: 5, out: 25 },
41
+ { match: "opus-4-7", in: 5, out: 25 },
42
+ { match: "opus-4-6", in: 5, out: 25 },
43
+ { match: "opus-4-5", in: 5, out: 25 },
44
+ { match: "opus-4-1", in: 15, out: 75 },
45
+ { match: "opus", in: 5, out: 25 },
34
46
  { match: "sonnet", in: 3, out: 15 },
35
47
  { match: "haiku", in: 1, out: 5 },
36
48
  ],
@@ -155,11 +167,33 @@ export function lookupRate(provider, model, table = PRICE_TABLE) {
155
167
  * @returns {{ inputCost:number, outputCost:number, totalCost:number,
156
168
  * currency:string, matched:boolean, free:boolean, rate:object|null }}
157
169
  */
170
+ // Prompt-cache price multipliers relative to the input rate. A cache WRITE
171
+ // (Anthropic 5-minute ephemeral breakpoint) bills at ~125%; only Anthropic
172
+ // reports cache creation. Cache READ pricing differs per provider — Anthropic
173
+ // ~10%, OpenAI ~50%, DeepSeek ~25% of the input rate — so the default below is
174
+ // overridden per provider. Applied only when cache token counts are present, so
175
+ // non-caching usage is priced exactly as before. Public list-price estimates
176
+ // that drift; override the table via config.llm.pricing for the base rates.
177
+ export const CACHE_READ_MULTIPLIER = 0.1;
178
+ export const CACHE_WRITE_MULTIPLIER = 1.25;
179
+ export const CACHE_READ_MULTIPLIER_BY_PROVIDER = Object.freeze({
180
+ anthropic: 0.1,
181
+ openai: 0.5,
182
+ deepseek: 0.25,
183
+ });
184
+
185
+ function cacheReadMultiplier(provider) {
186
+ const p = String(provider || "").toLowerCase();
187
+ return CACHE_READ_MULTIPLIER_BY_PROVIDER[p] ?? CACHE_READ_MULTIPLIER;
188
+ }
189
+
158
190
  export function estimateCost({
159
191
  provider,
160
192
  model,
161
193
  inputTokens = 0,
162
194
  outputTokens = 0,
195
+ cacheReadTokens = 0,
196
+ cacheCreationTokens = 0,
163
197
  table,
164
198
  } = {}) {
165
199
  const rate = lookupRate(provider, model, table);
@@ -167,6 +201,8 @@ export function estimateCost({
167
201
  return {
168
202
  inputCost: 0,
169
203
  outputCost: 0,
204
+ cacheReadCost: 0,
205
+ cacheCreationCost: 0,
170
206
  totalCost: 0,
171
207
  currency: "USD",
172
208
  matched: false,
@@ -176,10 +212,20 @@ export function estimateCost({
176
212
  }
177
213
  const inputCost = round((Number(inputTokens) / 1e6) * rate.in);
178
214
  const outputCost = round((Number(outputTokens) / 1e6) * rate.out);
215
+ const cacheReadCost = round(
216
+ (Number(cacheReadTokens) / 1e6) * rate.in * cacheReadMultiplier(provider),
217
+ );
218
+ const cacheCreationCost = round(
219
+ (Number(cacheCreationTokens) / 1e6) * rate.in * CACHE_WRITE_MULTIPLIER,
220
+ );
179
221
  return {
180
222
  inputCost,
181
223
  outputCost,
182
- totalCost: round(inputCost + outputCost),
224
+ cacheReadCost,
225
+ cacheCreationCost,
226
+ totalCost: round(
227
+ inputCost + outputCost + cacheReadCost + cacheCreationCost,
228
+ ),
183
229
  currency: "USD",
184
230
  matched: true,
185
231
  free: rate.pattern === "free",
@@ -202,6 +248,8 @@ export function priceRollup(aggregate, { table } = {}) {
202
248
  model: row.model,
203
249
  inputTokens: row.inputTokens,
204
250
  outputTokens: row.outputTokens,
251
+ cacheReadTokens: row.cacheReadTokens,
252
+ cacheCreationTokens: row.cacheCreationTokens,
205
253
  table,
206
254
  });
207
255
  return {
@@ -209,6 +257,8 @@ export function priceRollup(aggregate, { table } = {}) {
209
257
  cost: est.totalCost,
210
258
  inputCost: est.inputCost,
211
259
  outputCost: est.outputCost,
260
+ cacheReadCost: est.cacheReadCost,
261
+ cacheCreationCost: est.cacheCreationCost,
212
262
  currency: est.currency,
213
263
  matched: est.matched,
214
264
  free: est.free,
@@ -306,12 +306,46 @@ export function getStoredToken(serverUrl) {
306
306
  return loadTokenStore()[serverKey(serverUrl)] || null;
307
307
  }
308
308
 
309
+ // The store holds OAuth access + refresh tokens, so it must not be world-
310
+ // readable. writeFileSync's `mode` only applies when the file is created, so
311
+ // also chmod (best-effort) to tighten a file an older version left at 0644.
312
+ function _writeStoreSecure(file, store) {
313
+ _deps.fs.mkdirSync(pathDefault.dirname(file), { recursive: true, mode: 0o700 });
314
+ const data = JSON.stringify(store, null, 2) + "\n";
315
+ // Atomic temp+rename: the store holds live OAuth tokens, so a crash (or a
316
+ // concurrent `cc mcp login`) mid-write must not truncate it and lose auth for
317
+ // every server. The temp inherits the 0600 mode. Falls back to a direct write
318
+ // when the injected fs lacks renameSync (test mocks); the real fs always has
319
+ // it, so production is always atomic.
320
+ if (typeof _deps.fs.renameSync !== "function") {
321
+ _deps.fs.writeFileSync(file, data, { encoding: "utf-8", mode: 0o600 });
322
+ } else {
323
+ const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2, 8)}.tmp`;
324
+ try {
325
+ _deps.fs.writeFileSync(tmp, data, { encoding: "utf-8", mode: 0o600 });
326
+ _deps.fs.renameSync(tmp, file);
327
+ } catch (err) {
328
+ try {
329
+ if (_deps.fs.existsSync && _deps.fs.existsSync(tmp) && _deps.fs.unlinkSync)
330
+ _deps.fs.unlinkSync(tmp);
331
+ } catch {
332
+ /* best-effort temp cleanup */
333
+ }
334
+ throw err;
335
+ }
336
+ }
337
+ try {
338
+ _deps.fs.chmodSync(file, 0o600);
339
+ } catch {
340
+ /* chmod unsupported on this platform/fs (e.g. Windows) — mode is advisory */
341
+ }
342
+ }
343
+
309
344
  export function saveStoredToken(serverUrl, record) {
310
345
  const file = tokenStorePath();
311
346
  const store = loadTokenStore();
312
347
  store[serverKey(serverUrl)] = { server: serverKey(serverUrl), ...record };
313
- _deps.fs.mkdirSync(pathDefault.dirname(file), { recursive: true });
314
- _deps.fs.writeFileSync(file, JSON.stringify(store, null, 2) + "\n", "utf-8");
348
+ _writeStoreSecure(file, store);
315
349
  return store[serverKey(serverUrl)];
316
350
  }
317
351
 
@@ -322,11 +356,7 @@ export function deleteStoredToken(serverUrl) {
322
356
  if (!(key in store)) return false;
323
357
  delete store[key];
324
358
  try {
325
- _deps.fs.writeFileSync(
326
- file,
327
- JSON.stringify(store, null, 2) + "\n",
328
- "utf-8",
329
- );
359
+ _writeStoreSecure(file, store);
330
360
  } catch {
331
361
  /* best-effort */
332
362
  }
@@ -8,6 +8,27 @@
8
8
  import fs from "fs";
9
9
  import path from "path";
10
10
 
11
+ /**
12
+ * Atomically write a UTF-8 file: a crash mid-write must not truncate MEMORY.md
13
+ * (the long-term knowledge file, re-read each session). Temp sibling + rename,
14
+ * which is atomic within a filesystem, so a reader/crash sees either the old
15
+ * file or the complete new one.
16
+ */
17
+ function atomicWriteFileSync(filePath, data) {
18
+ const tmp = `${filePath}.${process.pid}.${Math.random().toString(36).slice(2, 8)}.tmp`;
19
+ try {
20
+ fs.writeFileSync(tmp, data, "utf8");
21
+ fs.renameSync(tmp, filePath);
22
+ } catch (err) {
23
+ try {
24
+ if (fs.existsSync(tmp)) fs.unlinkSync(tmp);
25
+ } catch {
26
+ /* best-effort temp cleanup */
27
+ }
28
+ throw err;
29
+ }
30
+ }
31
+
11
32
  /**
12
33
  * Ensure the memory directory structure exists
13
34
  */
@@ -146,7 +167,7 @@ export function appendDailyNote(memoryDir, content) {
146
167
  fs.appendFileSync(filePath, entry, "utf8");
147
168
  } else {
148
169
  const header = `# Daily Note: ${today}\n${entry}`;
149
- fs.writeFileSync(filePath, header, "utf8");
170
+ atomicWriteFileSync(filePath, header);
150
171
  }
151
172
 
152
173
  return { date: today, path: filePath };
@@ -206,7 +227,7 @@ export function getMemoryFile(memoryDir) {
206
227
  export function updateMemoryFile(memoryDir, content) {
207
228
  ensureMemoryDirs(memoryDir);
208
229
  const filePath = path.join(memoryDir, "MEMORY.md");
209
- fs.writeFileSync(filePath, content, "utf8");
230
+ atomicWriteFileSync(filePath, content);
210
231
  return { path: filePath };
211
232
  }
212
233