chainlesschain 0.162.95 → 0.162.96

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 (217) 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-Bo9Ux24z.js} +1 -1
  5. package/src/assets/web-panel/assets/{ActionButton-BxhQTwqd.js → ActionButton-CExgXSQT.js} +1 -1
  6. package/src/assets/web-panel/assets/{Analytics-C4oUXzhG.js → Analytics-BNnZhRTZ.js} +3 -3
  7. package/src/assets/web-panel/assets/{AppLayout-CjzKJdv3.js → AppLayout-D9xfqFCT.js} +5 -5
  8. package/src/assets/web-panel/assets/{Audit-B-MrugUs.js → Audit-Cpgu8uCw.js} +1 -1
  9. package/src/assets/web-panel/assets/{Backup-ecIfEpIZ.js → Backup-CHdEJnNE.js} +1 -1
  10. package/src/assets/web-panel/assets/{BaseInput-BUcSfWnx.js → BaseInput-Cbw-PxBq.js} +1 -1
  11. package/src/assets/web-panel/assets/{Chat-DIc1n_Lk.js → Chat-ChHTZK_6.js} +5 -5
  12. package/src/assets/web-panel/assets/{ChatBubbleRenderer-BGzg7oVA.js → ChatBubbleRenderer-Dl6z6ZH7.js} +1 -1
  13. package/src/assets/web-panel/assets/{Checkbox-BQ3Ueff-.js → Checkbox-Cj8d9bru.js} +1 -1
  14. package/src/assets/web-panel/assets/{Codegen-CvSGLnqE.js → Codegen-C2YobW4j.js} +1 -1
  15. package/src/assets/web-panel/assets/{Col-Br9IXZcE.js → Col-CxfV5FGT.js} +1 -1
  16. package/src/assets/web-panel/assets/{Community-tPuLOf0r.js → Community-CiwOASMj.js} +1 -1
  17. package/src/assets/web-panel/assets/{Compact-vMWH73BE.js → Compact-CTg_2iKW.js} +1 -1
  18. package/src/assets/web-panel/assets/{Compliance-Ct9z699x.js → Compliance-HMbsTmtw.js} +1 -1
  19. package/src/assets/web-panel/assets/{Cowork-Bxs1kL1y.js → Cowork-DyNDqGR2.js} +3 -3
  20. package/src/assets/web-panel/assets/{Cron-QEpr2k4U.js → Cron-9-x5M7qy.js} +2 -2
  21. package/src/assets/web-panel/assets/{Crosschain-u4xbUw6O.js → Crosschain-4msgDaYJ.js} +1 -1
  22. package/src/assets/web-panel/assets/{DID-DXTEvyrW.js → DID--2Beb5Po.js} +2 -2
  23. package/src/assets/web-panel/assets/{Dashboard-C4pZgzkg.js → Dashboard-T6nq0Lzs.js} +2 -2
  24. package/src/assets/web-panel/assets/{Dropdown-LRuqREGe.js → Dropdown-D_Q6T10v.js} +1 -1
  25. package/src/assets/web-panel/assets/{EmailListRenderer-D7XI1yBM.js → EmailListRenderer-TnN21bJx.js} +1 -1
  26. package/src/assets/web-panel/assets/{FamilyGuardDashboard-3y_SuAMQ.js → FamilyGuardDashboard-kky_uIij.js} +1 -1
  27. package/src/assets/web-panel/assets/{Federation-DbjbJJCW.js → Federation-C1GkNY4B.js} +1 -1
  28. package/src/assets/web-panel/assets/{FormItemContext-WPpMZcUw.js → FormItemContext-5-_Fc5YV.js} +1 -1
  29. package/src/assets/web-panel/assets/{GenericCardRenderer-nNT9Tn4E.js → GenericCardRenderer-CmHiB7sr.js} +1 -1
  30. package/src/assets/web-panel/assets/{Git-DqDaVzwV.js → Git-Cuci_BPI.js} +2 -2
  31. package/src/assets/web-panel/assets/{Governance-BquBZpcg.js → Governance-BU0cO2LD.js} +1 -1
  32. package/src/assets/web-panel/assets/{Inference-RcPPYijs.js → Inference-BDFNgEcx.js} +1 -1
  33. package/src/assets/web-panel/assets/{KnowledgeGraph-ByXfZV13.js → KnowledgeGraph-DITXUzHz.js} +1 -1
  34. package/src/assets/web-panel/assets/{Logs-CHT3DUcq.js → Logs-Ea-07CRd.js} +2 -2
  35. package/src/assets/web-panel/assets/{Marketplace-Bi0HJ9pN.js → Marketplace-D6OBbRVG.js} +1 -1
  36. package/src/assets/web-panel/assets/{McpTools-QCBxPRRc.js → McpTools-eiKpIJMi.js} +4 -4
  37. package/src/assets/web-panel/assets/{Memory-D4RlxGXn.js → Memory-M-X-T8UF.js} +2 -2
  38. package/src/assets/web-panel/assets/{MobileBridge-CU_PV7e-.js → MobileBridge-xcZb9Ekv.js} +2 -2
  39. package/src/assets/web-panel/assets/{MobileProjects-c1CMFD8z.js → MobileProjects-DCfoTPRC.js} +1 -1
  40. package/src/assets/web-panel/assets/{Mtc-BezDB1-s.js → Mtc-BWfGDzD8.js} +5 -5
  41. package/src/assets/web-panel/assets/{MtcAudit-CgLpiHpE.js → MtcAudit-imBuld-U.js} +2 -2
  42. package/src/assets/web-panel/assets/{Multisig-vx6bz-bA.js → Multisig-C2kcsq7I.js} +3 -3
  43. package/src/assets/web-panel/assets/{NLProgramming-CGOagg3-.js → NLProgramming-DObvXSXP.js} +1 -1
  44. package/src/assets/web-panel/assets/{Notes-BcuDGYRS.js → Notes-C-6OGJMe.js} +4 -4
  45. package/src/assets/web-panel/assets/{NotificationSettings-ZAGCDUpc.js → NotificationSettings-ub7Ozxci.js} +1 -1
  46. package/src/assets/web-panel/assets/{OrderTableRenderer-B5onE0eX.js → OrderTableRenderer-CR2W89Vk.js} +1 -1
  47. package/src/assets/web-panel/assets/{Organization-y_y20-5Y.js → Organization-DNA_Wty7.js} +4 -4
  48. package/src/assets/web-panel/assets/{Overflow-BEICS31D.js → Overflow-rT4Wb4rQ.js} +1 -1
  49. package/src/assets/web-panel/assets/{P2P-D_WRMzh4.js → P2P-CK-hARKv.js} +2 -2
  50. package/src/assets/web-panel/assets/PdhVaultBrowser-C2pbtNbT.js +7 -0
  51. package/src/assets/web-panel/assets/{Permissions-3aqCc_4f.js → Permissions-B8EIEqGJ.js} +4 -4
  52. package/src/assets/web-panel/assets/{PersonalDataHub-DT3R9g9Y.js → PersonalDataHub-BxNVNU3s.js} +2 -2
  53. package/src/assets/web-panel/assets/{Pipeline-MiMWIhw0.js → Pipeline-CT9pbkyY.js} +1 -1
  54. package/src/assets/web-panel/assets/{Privacy-C3JsLuDa.js → Privacy-DhCNd2CS.js} +1 -1
  55. package/src/assets/web-panel/assets/{ProjectInit-EhHVi1ff.js → ProjectInit-Qngnx-vQ.js} +2 -2
  56. package/src/assets/web-panel/assets/{ProjectSettings-D_dNEA37.js → ProjectSettings-Dfn-nQc1.js} +2 -2
  57. package/src/assets/web-panel/assets/Projects-BKdiSaPR.js +1 -0
  58. package/src/assets/web-panel/assets/{Providers-CPEhi2iZ.js → Providers-4o9B_n-X.js} +1 -1
  59. package/src/assets/web-panel/assets/{QuickAsk-BFQpMleX.js → QuickAsk-bs1qvF30.js} +1 -1
  60. package/src/assets/web-panel/assets/{Recommend-DzbovRC8.js → Recommend-CuEnKA2K.js} +1 -1
  61. package/src/assets/web-panel/assets/{Reputation-DoBeV3TD.js → Reputation-DbfvmRb2.js} +1 -1
  62. package/src/assets/web-panel/assets/{Row-DthhwX_B.js → Row-DvMJGAI3.js} +1 -1
  63. package/src/assets/web-panel/assets/{RssFeed-Dsf5U0lP.js → RssFeed-oJ0BZvmA.js} +2 -2
  64. package/src/assets/web-panel/assets/{Search-CJPB8kKl.js → Search-DnIVrY9O.js} +1 -1
  65. package/src/assets/web-panel/assets/{Security-DI9MfpdD.js → Security-CIWhI_Tc.js} +3 -3
  66. package/src/assets/web-panel/assets/{Services-BdG0TFGf.js → Services-DLrnpjwm.js} +2 -2
  67. package/src/assets/web-panel/assets/{Skeleton-DSYazY_B.js → Skeleton-CYmqgtVb.js} +1 -1
  68. package/src/assets/web-panel/assets/{Skills-BD309VTT.js → Skills-BUzCkoeu.js} +1 -1
  69. package/src/assets/web-panel/assets/{Sla-CaUlw4_6.js → Sla-DYRE4bnS.js} +1 -1
  70. package/src/assets/web-panel/assets/{SpeechSettings-CZAPkR7H.js → SpeechSettings-_6MurdOg.js} +1 -1
  71. package/src/assets/web-panel/assets/{SyncSettings-CFRSowN3.js → SyncSettings-UIdnF5Li.js} +2 -2
  72. package/src/assets/web-panel/assets/{Tasks-Di8EOluc.js → Tasks-COx2CjT8.js} +1 -1
  73. package/src/assets/web-panel/assets/{Templates-B5FTziwi.js → Templates-BcHAmkZo.js} +1 -1
  74. package/src/assets/web-panel/assets/{Tenant-Dj3XYfBg.js → Tenant-CjYQS8yZ.js} +1 -1
  75. package/src/assets/web-panel/assets/{Terminal-CznH_rJT.js → Terminal-BmfOPcWt.js} +2 -2
  76. package/src/assets/web-panel/assets/{TimelineRenderer-DTHzaXPt.js → TimelineRenderer-D5HuarNQ.js} +1 -1
  77. package/src/assets/web-panel/assets/{Tokens-Bo56nuML.js → Tokens-C_fbProA.js} +1 -1
  78. package/src/assets/web-panel/assets/{Trigger-pyVUVBwb.js → Trigger-JPulpRx8.js} +1 -1
  79. package/src/assets/web-panel/assets/{Trust-Cp7jMkKN.js → Trust-CnO-062O.js} +1 -1
  80. package/src/assets/web-panel/assets/{UkeySign-aPcZCdpz.js → UkeySign-HpNovvau.js} +1 -1
  81. package/src/assets/web-panel/assets/{VideoEditing-DAv5j--w.js → VideoEditing-yMncxH7y.js} +1 -1
  82. package/src/assets/web-panel/assets/{Wallet-DRhK7IDR.js → Wallet-Cug-t7Np.js} +4 -4
  83. package/src/assets/web-panel/assets/{WebAuthn-B2yQJuq8.js → WebAuthn--U1-NR-3.js} +2 -2
  84. package/src/assets/web-panel/assets/{WorkflowEditor-D98DhR54.js → WorkflowEditor-R-XvSJ3S.js} +1 -1
  85. package/src/assets/web-panel/assets/{chat-BXrRNi8C.js → chat-CCI1i6Rp.js} +1 -1
  86. package/src/assets/web-panel/assets/{colors-jQ94T_qn.js → colors-C-mqoaLt.js} +1 -1
  87. package/src/assets/web-panel/assets/{compact-item-KqUJLlF2.js → compact-item-De_N2iRR.js} +1 -1
  88. package/src/assets/web-panel/assets/{createContext-DxM3zuQW.js → createContext-DbTM6sjZ.js} +1 -1
  89. package/src/assets/web-panel/assets/devWarning-OcLPHSl5.js +1 -0
  90. package/src/assets/web-panel/assets/{hasIn-BGm946KV.js → hasIn-D0Adylyc.js} +1 -1
  91. package/src/assets/web-panel/assets/index-7E_mHkDU.js +1 -0
  92. package/src/assets/web-panel/assets/{index-Dib0FsTT.js → index-AY937BxS.js} +1 -1
  93. package/src/assets/web-panel/assets/{index-C5NQai7O.js → index-B6hgy5VC.js} +1 -1
  94. package/src/assets/web-panel/assets/{index-LWTZAud0.js → index-B7XnpLHk.js} +1 -1
  95. package/src/assets/web-panel/assets/{index-BXMZLfd8.js → index-B7gw-tDm.js} +1 -1
  96. package/src/assets/web-panel/assets/{index-DTUs9t5F.js → index-BBgDgkwW.js} +1 -1
  97. package/src/assets/web-panel/assets/{index-Ce-YoL4x.js → index-BMmeiw88.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-BaU_fSK7.js → index-BU6XtnSx.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-BMp41Q1Y.js → index-Bi7VUG0R.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-CbQvgx2e.js → index-BtE3cw9m.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-DWcd5zDG.js → index-C9ej-MF0.js} +3 -3
  102. package/src/assets/web-panel/assets/{index-BbS0cBU9.js → index-CIEiQl1j.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-D1VWL6Lc.js → index-CKgaW9E5.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-DRzsaWQy.js → index-CY0KKR5f.js} +1 -1
  105. package/src/assets/web-panel/assets/index-CbV3-WlB.js +1 -0
  106. package/src/assets/web-panel/assets/{index-DSzb9VOG.js → index-Co1UkcFX.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-B-2HpfWo.js → index-CzP5d5LW.js} +1 -1
  108. package/src/assets/web-panel/assets/{index-DboUBf6R.js → index-D0NJ5lRi.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-DZMzj4ay.js → index-D0gBsqTX.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-CghRL1dh.js → index-D5F6shtn.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-Cz_SFSix.js → index-DVtamtlm.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-T4UJiTi3.js → index-DWT18hDG.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-CreXAr5Y.js → index-DaXb4B9P.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-C7Z2m28C.js → index-DjQA0Vez.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-Crw76vIF.js → index-Ds8NJVEL.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-mZNLT2SP.js → index-DuP-8HWu.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-BnEZfi5D.js → index-DwLTiJ31.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-1P0sXNhG.js → index-T3VhH8cO.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-B4b7KjRq.js → index-TDY1o0w3.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-DOYcemym.js → index-TRy2Gc3V.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-BAPk6ntc.js → index-W1OMxCmI.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-Dgzw6eKr.js → index-ftFRZbuc.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-C7sBLech.js → index-iGPGaORA.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-FSYz5aww.js → index-jhy5KQvg.js} +1 -1
  125. package/src/assets/web-panel/assets/{index-DZpVYD4j.js → index-qUSZJcgw.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-yavsyHif.js → index-w3f6lqkD.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-CfE3jxrT.js → index-xdMc71Kh.js} +1 -1
  128. package/src/assets/web-panel/assets/{index-aOLXuHpG.js → index-xhX5eg2Z.js} +1 -1
  129. package/src/assets/web-panel/assets/{index-BGjCjsf1.js → index-zkTiW6d6.js} +1 -1
  130. package/src/assets/web-panel/assets/{initDefaultProps-BDoZJI7g.js → initDefaultProps-CIHqouWk.js} +1 -1
  131. package/src/assets/web-panel/assets/{motion-sceLjgp-.js → motion-D1iUKftk.js} +1 -1
  132. package/src/assets/web-panel/assets/{move-CTWQpFa7.js → move-YOpWjmjL.js} +1 -1
  133. package/src/assets/web-panel/assets/{omit-dr-NCrHZ.js → omit-C_1357UY.js} +1 -1
  134. package/src/assets/web-panel/assets/{pickAttrs-DK-GX_Z2.js → pickAttrs-CDb4dq6o.js} +1 -1
  135. package/src/assets/web-panel/assets/{placementArrow-BIPf--18.js → placementArrow-D-OGYyhd.js} +1 -1
  136. package/src/assets/web-panel/assets/{responsiveObserve-BzrWT5tV.js → responsiveObserve-hS-j69A0.js} +1 -1
  137. package/src/assets/web-panel/assets/{slide-D5XhxlET.js → slide-tnjuvphU.js} +1 -1
  138. package/src/assets/web-panel/assets/{statusUtils-6DI-k-9E.js → statusUtils-BERfMMcU.js} +1 -1
  139. package/src/assets/web-panel/assets/{styleChecker-DMw-eyn9.js → styleChecker-DKpl2K9E.js} +1 -1
  140. package/src/assets/web-panel/assets/{useFlexGapSupport-D4oVdliM.js → useFlexGapSupport-C_sX4D3N.js} +1 -1
  141. package/src/assets/web-panel/assets/{useFs-D3PPZnvy.js → useFs-DBcxC1vK.js} +1 -1
  142. package/src/assets/web-panel/assets/{usePersonalDataHub-Gh5ba4KP.js → usePersonalDataHub-CeoDf2XI.js} +1 -1
  143. package/src/assets/web-panel/assets/{vnode-CVEM1u4x.js → vnode-DC9Pmj_u.js} +1 -1
  144. package/src/assets/web-panel/assets/{zoom-ClT_-9RZ.js → zoom-CZA58J7M.js} +1 -1
  145. package/src/assets/web-panel/index.html +1 -1
  146. package/src/commands/activitypub.js +19 -3
  147. package/src/commands/agent.js +25 -0
  148. package/src/commands/ask.js +13 -0
  149. package/src/commands/config.js +17 -1
  150. package/src/commands/cost.js +6 -1
  151. package/src/commands/dao.js +16 -3
  152. package/src/commands/dlp.js +7 -1
  153. package/src/commands/hub.js +13 -0
  154. package/src/commands/mtc.js +37 -1
  155. package/src/gateways/discord/discord-formatter.js +18 -1
  156. package/src/gateways/telegram/telegram-formatter.js +6 -3
  157. package/src/gateways/ws/ws-server.js +19 -1
  158. package/src/harness/plugin-manager.js +20 -0
  159. package/src/harness/worktree-isolator.js +17 -1
  160. package/src/lib/__tests__/model-deprecation.test.js +194 -0
  161. package/src/lib/a2a-protocol.js +23 -9
  162. package/src/lib/agent-core.js +2 -5
  163. package/src/lib/audit-mtc.js +40 -2
  164. package/src/lib/chat-core.js +141 -18
  165. package/src/lib/cli-anything-bridge.js +5 -0
  166. package/src/lib/cli-numeric.js +38 -0
  167. package/src/lib/config-manager.js +37 -6
  168. package/src/lib/cowork/ab-comparator-cli.js +6 -2
  169. package/src/lib/cowork-cron.js +45 -34
  170. package/src/lib/cowork-learning.js +31 -1
  171. package/src/lib/cowork-share.js +39 -3
  172. package/src/lib/cowork-template-marketplace.js +54 -2
  173. package/src/lib/cowork-workflow.js +33 -6
  174. package/src/lib/cross-chain-mtc.js +41 -12
  175. package/src/lib/crypto-manager.js +12 -2
  176. package/src/lib/dbevo.js +19 -8
  177. package/src/lib/downloader.js +24 -9
  178. package/src/lib/evomap-governance.js +4 -4
  179. package/src/lib/evomap-manager.js +54 -7
  180. package/src/lib/fatal-handler.js +48 -0
  181. package/src/lib/file-checkpoint.js +26 -0
  182. package/src/lib/git-integration.js +28 -0
  183. package/src/lib/goal-store.js +36 -10
  184. package/src/lib/hierarchical-memory.js +6 -0
  185. package/src/lib/llm-pricing.js +52 -2
  186. package/src/lib/mcp-oauth.js +37 -7
  187. package/src/lib/memory-manager.js +23 -2
  188. package/src/lib/model-deprecation.js +249 -0
  189. package/src/lib/pdh-feedback-ledger.js +131 -0
  190. package/src/lib/permanent-memory.js +30 -2
  191. package/src/lib/permission-rules.cjs +11 -3
  192. package/src/lib/personal-data-hub-aichat-wizard.js +33 -3
  193. package/src/lib/personal-data-hub-wiring.js +36 -13
  194. package/src/lib/project-instructions.js +10 -4
  195. package/src/lib/runnable-provider.js +62 -31
  196. package/src/lib/session-usage.js +34 -1
  197. package/src/lib/skill-loader.js +4 -1
  198. package/src/lib/skill-packs/generator.js +25 -2
  199. package/src/lib/stream-retry.js +56 -0
  200. package/src/lib/sync-credentials.js +53 -17
  201. package/src/lib/token-tracker.js +12 -2
  202. package/src/lib/with-file-lock.js +87 -0
  203. package/src/repl/agent-repl.js +39 -7
  204. package/src/repl/chat-repl.js +10 -0
  205. package/src/repl/session-cost.js +31 -3
  206. package/src/runtime/agent-core.js +474 -53
  207. package/src/runtime/coding-agent-contract-shared.cjs +58 -1
  208. package/src/runtime/coding-agent-policy.cjs +10 -0
  209. package/src/runtime/file-ref-expander.js +29 -1
  210. package/src/runtime/headless-runner.js +12 -1
  211. package/src/runtime/headless-stream.js +131 -5
  212. package/src/skills/video-editing/tools/commit.js +15 -1
  213. package/src/assets/web-panel/assets/PdhVaultBrowser-C8f5NbbI.js +0 -7
  214. package/src/assets/web-panel/assets/Projects-CXydUXp8.js +0 -1
  215. package/src/assets/web-panel/assets/devWarning-Bo1r5_zX.js +0 -1
  216. package/src/assets/web-panel/assets/index-BCU6dVQ-.js +0 -1
  217. package/src/assets/web-panel/assets/index-N_oOFphx.js +0 -1
@@ -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
 
@@ -0,0 +1,249 @@
1
+ /**
2
+ * Deprecated-model warning — Claude-Code 2.1.183 parity:
3
+ * "a warning when the requested model is deprecated or automatically updated
4
+ * to a newer model".
5
+ *
6
+ * The "automatically updated to a newer model" half is already covered by the
7
+ * fallback-model machinery (a model-not-found error switches to the next model
8
+ * in the chain and surfaces an onFallback notice — see fallback-model.js +
9
+ * agent.js / agent-repl.js). This module covers the PROACTIVE half: warn once,
10
+ * up front, when the user's *configured / requested* model id is on a small,
11
+ * high-confidence list of provider-retired model snapshots — before the run
12
+ * fails with an opaque "model not found".
13
+ *
14
+ * The Anthropic entries below are kept in sync with the authoritative model
15
+ * deprecation table (platform.claude.com/docs/en/about-claude/model-deprecations,
16
+ * verified 2026-06-21). `state` distinguishes "retired" (requests now fail) from
17
+ * "deprecated" (still works, not recommended, has a retirement date).
18
+ *
19
+ * Design constraints (this project hates model-id churn noise):
20
+ * - Only ids the provider has officially retired or deprecated. A false warning
21
+ * on a live model is worse than a missing one.
22
+ * - CRITICAL: the Claude 4 family has BOTH active and retired members that
23
+ * differ only by date suffix (claude-opus-4-8 active vs claude-opus-4-20250514
24
+ * retired), so those entries use EXACT dated ids and must never be turned into
25
+ * bare prefixes. Older families (1 / 2 / 3 / 3.5 / 3.7) are fully retired, so a
26
+ * family prefix is safe there.
27
+ * - Pure + fail-open: a lookup never throws and is trivially unit-testable.
28
+ * - The notice goes to stderr (keeps stdout clean for JSON/headless output),
29
+ * fires at most once per model per process, and is silenced with
30
+ * CC_MODEL_NOTICE=0.
31
+ */
32
+
33
+ import chalk from "chalk";
34
+
35
+ /**
36
+ * Retired / deprecated model snapshots, most-specific first. `id` is matched
37
+ * case-insensitively as an exact id OR a prefix (so `claude-2` covers
38
+ * `claude-2.0` / `claude-2.1`, and `claude-3-5-sonnet` covers both 3.5 Sonnet
39
+ * snapshots). Prefixes are only used for fully-retired families — never for the
40
+ * Claude 4 family, whose active and retired members share a prefix.
41
+ *
42
+ * @type {Array<{id: string, state: "retired"|"deprecated", reason: string, replacement: string}>}
43
+ */
44
+ export const DEPRECATED_MODELS = [
45
+ // ── Anthropic — Claude 4 family (EXACT dated ids only; prefixes would hit
46
+ // the active claude-opus-4-8 / -4-7 / -4-6 / -4-5-20251101 / sonnet-4-6 /
47
+ // sonnet-4-5-20250929 / haiku-4-5-20251001 models) ──────────────────────
48
+ {
49
+ id: "claude-opus-4-1-20250805",
50
+ state: "deprecated",
51
+ reason: "Anthropic deprecated Claude Opus 4.1 (retires 2026-08-05)",
52
+ replacement: "claude-opus-4-8",
53
+ },
54
+ {
55
+ id: "claude-opus-4-20250514",
56
+ state: "retired",
57
+ reason: "Anthropic retired the original Claude Opus 4 snapshot",
58
+ replacement: "claude-opus-4-8",
59
+ },
60
+ {
61
+ id: "claude-sonnet-4-20250514",
62
+ state: "retired",
63
+ reason: "Anthropic retired the original Claude Sonnet 4 snapshot",
64
+ replacement: "claude-sonnet-4-6",
65
+ },
66
+ // ── Anthropic — fully-retired older families (family prefix is safe) ───────
67
+ {
68
+ id: "claude-3-7-sonnet",
69
+ state: "retired",
70
+ reason: "Anthropic retired Claude Sonnet 3.7",
71
+ replacement: "claude-sonnet-4-6",
72
+ },
73
+ {
74
+ id: "claude-3-5-sonnet",
75
+ state: "retired",
76
+ reason: "Anthropic retired the Claude 3.5 Sonnet snapshots",
77
+ replacement: "claude-sonnet-4-6",
78
+ },
79
+ {
80
+ id: "claude-3-5-haiku",
81
+ state: "retired",
82
+ reason: "Anthropic retired Claude Haiku 3.5",
83
+ replacement: "claude-haiku-4-5",
84
+ },
85
+ {
86
+ id: "claude-3-opus-20240229",
87
+ state: "retired",
88
+ reason: "Anthropic retired Claude 3 Opus",
89
+ replacement: "claude-opus-4-8",
90
+ },
91
+ {
92
+ id: "claude-3-sonnet-20240229",
93
+ state: "retired",
94
+ reason: "Anthropic retired Claude 3 Sonnet",
95
+ replacement: "claude-sonnet-4-6",
96
+ },
97
+ {
98
+ id: "claude-3-haiku-20240307",
99
+ state: "retired",
100
+ reason: "Anthropic retired Claude 3 Haiku",
101
+ replacement: "claude-haiku-4-5",
102
+ },
103
+ {
104
+ id: "claude-2",
105
+ state: "retired",
106
+ reason: "Anthropic retired the Claude 2 family",
107
+ replacement: "claude-sonnet-4-6 or claude-opus-4-8",
108
+ },
109
+ {
110
+ id: "claude-instant",
111
+ state: "retired",
112
+ reason: "Anthropic retired Claude Instant",
113
+ replacement: "claude-haiku-4-5",
114
+ },
115
+ {
116
+ id: "claude-1",
117
+ state: "retired",
118
+ reason: "Anthropic retired the Claude 1 family",
119
+ replacement: "claude-sonnet-4-6 or claude-opus-4-8",
120
+ },
121
+ // ── OpenAI (retired snapshots) ────────────────────────────────────────────
122
+ {
123
+ id: "gpt-4-vision-preview",
124
+ state: "retired",
125
+ reason: "OpenAI retired the gpt-4 vision preview",
126
+ replacement: "gpt-5 (natively multimodal)",
127
+ },
128
+ {
129
+ id: "gpt-4-1106-vision-preview",
130
+ state: "retired",
131
+ reason: "OpenAI retired the gpt-4 vision preview",
132
+ replacement: "gpt-5 (natively multimodal)",
133
+ },
134
+ {
135
+ id: "gpt-4-32k",
136
+ state: "retired",
137
+ reason: "OpenAI retired the gpt-4-32k snapshots",
138
+ replacement: "gpt-5 or gpt-4o",
139
+ },
140
+ {
141
+ id: "gpt-4-0314",
142
+ state: "retired",
143
+ reason: "OpenAI retired this gpt-4 snapshot",
144
+ replacement: "gpt-5 or gpt-4o",
145
+ },
146
+ {
147
+ id: "gpt-4-0613",
148
+ state: "retired",
149
+ reason: "OpenAI retired this gpt-4 snapshot",
150
+ replacement: "gpt-5 or gpt-4o",
151
+ },
152
+ {
153
+ id: "gpt-3.5-turbo-0301",
154
+ state: "retired",
155
+ reason: "OpenAI retired this gpt-3.5-turbo snapshot",
156
+ replacement: "gpt-4o-mini or gpt-5-mini",
157
+ },
158
+ {
159
+ id: "gpt-3.5-turbo-0613",
160
+ state: "retired",
161
+ reason: "OpenAI retired this gpt-3.5-turbo snapshot",
162
+ replacement: "gpt-4o-mini or gpt-5-mini",
163
+ },
164
+ {
165
+ id: "text-davinci-002",
166
+ state: "retired",
167
+ reason: "OpenAI retired the text-davinci completion models",
168
+ replacement: "gpt-4o-mini or gpt-5-mini",
169
+ },
170
+ {
171
+ id: "text-davinci-003",
172
+ state: "retired",
173
+ reason: "OpenAI retired the text-davinci completion models",
174
+ replacement: "gpt-4o-mini or gpt-5-mini",
175
+ },
176
+ ];
177
+
178
+ /**
179
+ * Look up a model id against the retired list.
180
+ *
181
+ * @param {string} modelId
182
+ * @returns {{id: string, state: string, reason: string, replacement: string}|null}
183
+ * the matched entry (with `id` = the configured model), or null.
184
+ */
185
+ export function checkModelDeprecation(modelId) {
186
+ if (!modelId || typeof modelId !== "string") return null;
187
+ const norm = modelId.trim().toLowerCase();
188
+ if (!norm) return null;
189
+ for (const entry of DEPRECATED_MODELS) {
190
+ const pat = entry.id.toLowerCase();
191
+ if (norm === pat || norm.startsWith(pat)) {
192
+ return { ...entry, id: modelId };
193
+ }
194
+ }
195
+ return null;
196
+ }
197
+
198
+ /**
199
+ * Human-readable one-line warning for a matched deprecation.
200
+ *
201
+ * @param {{id: string, state?: string, reason: string, replacement: string}} info
202
+ * @returns {string}
203
+ */
204
+ export function formatModelDeprecationWarning(info) {
205
+ const verb = info.state === "retired" ? "has been retired" : "is deprecated";
206
+ return (
207
+ `Warning: model "${info.id}" ${verb} — ${info.reason}. ` +
208
+ `Switch to ${info.replacement}. (CC_MODEL_NOTICE=0 to hide)`
209
+ );
210
+ }
211
+
212
+ /** Models already warned about in this process — one notice per id. */
213
+ export const _warned = new Set();
214
+
215
+ /**
216
+ * Entry-point hook: warn once on stderr if `model` is a retired snapshot.
217
+ * Cheap, fail-open, never throws.
218
+ *
219
+ * @param {object} opts
220
+ * @param {string} opts.model the resolved model id
221
+ * @param {object} [opts.env] defaults to process.env
222
+ * @param {(line:string)=>void} [opts.print] defaults to a yellow stderr writer
223
+ * @param {Set} [opts.seen] dedup set (defaults to module `_warned`)
224
+ * @returns {{ warned: boolean, info: object|null }}
225
+ */
226
+ export function maybeWarnDeprecatedModel(opts = {}) {
227
+ const env = opts.env || process.env;
228
+ const seen = opts.seen || _warned;
229
+ const print =
230
+ opts.print || ((line) => process.stderr.write(chalk.yellow(line) + "\n"));
231
+ const out = { warned: false, info: null };
232
+ try {
233
+ if (env.CC_MODEL_NOTICE === "0") return out;
234
+ const info = checkModelDeprecation(opts.model);
235
+ if (!info) return out;
236
+ const key = String(opts.model).toLowerCase();
237
+ if (seen.has(key)) {
238
+ out.info = info;
239
+ return out;
240
+ }
241
+ seen.add(key);
242
+ print(formatModelDeprecationWarning(info));
243
+ out.warned = true;
244
+ out.info = info;
245
+ } catch {
246
+ /* fail-open: a deprecation notice must never affect the run */
247
+ }
248
+ return out;
249
+ }