chainlesschain 0.162.34 → 0.162.36

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 (185) hide show
  1. package/package.json +1 -1
  2. package/src/assets/web-panel/assets/{AIOps-BYfi9NYS.js → AIOps-vAVAFNJ4.js} +1 -1
  3. package/src/assets/web-panel/assets/{ActionButton-BiS_tAN7.js → ActionButton-BnRHFCKM.js} +1 -1
  4. package/src/assets/web-panel/assets/{Analytics-jiWl_p-B.js → Analytics-BOjwqWqG.js} +3 -3
  5. package/src/assets/web-panel/assets/{AppLayout-m4sIzDot.js → AppLayout-Dc0D1Txn.js} +5 -5
  6. package/src/assets/web-panel/assets/{Audit-CPla3Erm.js → Audit-dd_2efaZ.js} +1 -1
  7. package/src/assets/web-panel/assets/{Backup-BGeQzTaB.js → Backup-HF1jgm8G.js} +1 -1
  8. package/src/assets/web-panel/assets/{BaseInput-DTf7Z1iU.js → BaseInput-CCtzmoKe.js} +1 -1
  9. package/src/assets/web-panel/assets/{Chat-DPTlQlD-.js → Chat-BNfH1c3p.js} +6 -6
  10. package/src/assets/web-panel/assets/{ChatBubbleRenderer-BgRXce4e.js → ChatBubbleRenderer-DCWFqmI4.js} +1 -1
  11. package/src/assets/web-panel/assets/{Checkbox-DY-XuQMu.js → Checkbox-BOr-NscK.js} +1 -1
  12. package/src/assets/web-panel/assets/{Codegen-B6oxPiZI.js → Codegen-DE058N7-.js} +1 -1
  13. package/src/assets/web-panel/assets/{Col-Dqxb4wSE.js → Col-SOREo1XE.js} +1 -1
  14. package/src/assets/web-panel/assets/{Community-DCIX514p.js → Community-sOvNZo9f.js} +1 -1
  15. package/src/assets/web-panel/assets/{Compact-BGtCzDoJ.js → Compact-DnBe558D.js} +1 -1
  16. package/src/assets/web-panel/assets/{Compliance-zcOYd55o.js → Compliance-o-r6CUbg.js} +1 -1
  17. package/src/assets/web-panel/assets/{Cowork-DVTtdIdM.js → Cowork-D6_k9mHP.js} +4 -4
  18. package/src/assets/web-panel/assets/{Cron-CPUaR69k.js → Cron-CEV3Xkrm.js} +2 -2
  19. package/src/assets/web-panel/assets/{Crosschain-DnjUS6QH.js → Crosschain-eJ1lQWKU.js} +1 -1
  20. package/src/assets/web-panel/assets/{DID-Dnz8VDmx.js → DID-B-WqM9Hp.js} +2 -2
  21. package/src/assets/web-panel/assets/{Dashboard-CtWf27j7.js → Dashboard-ZnKPcsHN.js} +2 -2
  22. package/src/assets/web-panel/assets/{Dropdown-B4GC1ZV4.js → Dropdown-B8uLWDIP.js} +1 -1
  23. package/src/assets/web-panel/assets/{EmailListRenderer-wjij3kzr.js → EmailListRenderer-Jmj2Y7aH.js} +1 -1
  24. package/src/assets/web-panel/assets/{FamilyGuardDashboard-rS-2W4u5.js → FamilyGuardDashboard-Cb2xetG-.js} +1 -1
  25. package/src/assets/web-panel/assets/{Federation-90p5Tnoz.js → Federation-C_07GXoq.js} +1 -1
  26. package/src/assets/web-panel/assets/{FormItemContext-Cnrw7gzq.js → FormItemContext-D3kbYrMU.js} +1 -1
  27. package/src/assets/web-panel/assets/{GenericCardRenderer-C85NsWa3.js → GenericCardRenderer-9xgqvGPg.js} +1 -1
  28. package/src/assets/web-panel/assets/{Git-BFAVM9F8.js → Git-BlwWlMMB.js} +2 -2
  29. package/src/assets/web-panel/assets/{Governance-DBoRonpq.js → Governance-DxN3wQZ_.js} +1 -1
  30. package/src/assets/web-panel/assets/{Inference-DHRyD66j.js → Inference-ls7pSw_D.js} +1 -1
  31. package/src/assets/web-panel/assets/{KnowledgeGraph-CTvUKecD.js → KnowledgeGraph-_n9hYuPI.js} +1 -1
  32. package/src/assets/web-panel/assets/{Logs-CB0dv_Ts.js → Logs-CvEVY5TK.js} +2 -2
  33. package/src/assets/web-panel/assets/{Marketplace-CN7Hm5Uw.js → Marketplace-C3qvQJT7.js} +1 -1
  34. package/src/assets/web-panel/assets/{McpTools-q5H25_8L.js → McpTools-DiwKpnKx.js} +5 -5
  35. package/src/assets/web-panel/assets/{Memory-BCV3pZ1d.js → Memory-CIBPi_da.js} +2 -2
  36. package/src/assets/web-panel/assets/{MobileBridge-C04Mngt4.js → MobileBridge-D-v0Se8y.js} +2 -2
  37. package/src/assets/web-panel/assets/MobileProjects-cP1apTQD.js +1 -0
  38. package/src/assets/web-panel/assets/{Mtc-ByAMz2DN.js → Mtc-BMFWrI65.js} +4 -4
  39. package/src/assets/web-panel/assets/{MtcAudit-B7V7byJq.js → MtcAudit-2s8LaHtR.js} +2 -2
  40. package/src/assets/web-panel/assets/{Multisig-DtKmcVQV.js → Multisig-dL_nvj7d.js} +3 -3
  41. package/src/assets/web-panel/assets/{NLProgramming-CaMbT5SC.js → NLProgramming-BbrJp06R.js} +1 -1
  42. package/src/assets/web-panel/assets/{Notes-DRjbSTCU.js → Notes-jR9irwy3.js} +4 -4
  43. package/src/assets/web-panel/assets/{NotificationSettings-B9YbJID5.js → NotificationSettings-Dk-STCIX.js} +1 -1
  44. package/src/assets/web-panel/assets/{OrderTableRenderer-BcI_-vGS.js → OrderTableRenderer-CqqfY6zq.js} +1 -1
  45. package/src/assets/web-panel/assets/{Organization-oTask4BE.js → Organization-BCK5jylo.js} +4 -4
  46. package/src/assets/web-panel/assets/{Overflow-Bab06ey7.js → Overflow-BRAY7Smt.js} +1 -1
  47. package/src/assets/web-panel/assets/{P2P--wlBeU0N.js → P2P-BltVRGjb.js} +2 -2
  48. package/src/assets/web-panel/assets/{PdhVaultBrowser-D4t77Pwc.js → PdhVaultBrowser-CV8UbXHe.js} +3 -3
  49. package/src/assets/web-panel/assets/{Permissions-B3sf6CJ3.js → Permissions-_tNl47Qh.js} +4 -4
  50. package/src/assets/web-panel/assets/{PersonalDataHub-BXOojk63.js → PersonalDataHub-Cgc4HjpX.js} +4 -4
  51. package/src/assets/web-panel/assets/{Pipeline-DReqtBFN.js → Pipeline-Bn_QU4mu.js} +1 -1
  52. package/src/assets/web-panel/assets/{Privacy-cT1GwKLx.js → Privacy-jzJowp5P.js} +1 -1
  53. package/src/assets/web-panel/assets/{ProjectInit-BhTAzVhH.js → ProjectInit-B_1pJ8qd.js} +2 -2
  54. package/src/assets/web-panel/assets/{ProjectSettings-CK-D8Fyj.js → ProjectSettings-CPVZpXzs.js} +2 -2
  55. package/src/assets/web-panel/assets/{Projects-CbHiwen6.js → Projects-CQsHOWnT.js} +1 -1
  56. package/src/assets/web-panel/assets/{Providers-B-ftiXa8.js → Providers-CzzMiLC0.js} +1 -1
  57. package/src/assets/web-panel/assets/{QuickAsk-CT5XPwTF.js → QuickAsk-MxBKIn9o.js} +1 -1
  58. package/src/assets/web-panel/assets/{Recommend-CohhlBZ_.js → Recommend-D8lN6Lis.js} +1 -1
  59. package/src/assets/web-panel/assets/{Reputation-CrgbixFz.js → Reputation-CfYK-IrV.js} +1 -1
  60. package/src/assets/web-panel/assets/{Row-ClExmBn3.js → Row-Bg7NZDP9.js} +1 -1
  61. package/src/assets/web-panel/assets/{RssFeed-VV0qizCJ.js → RssFeed-BOVNJhj0.js} +3 -3
  62. package/src/assets/web-panel/assets/{Search-CqJapSiL.js → Search-B38qzmhY.js} +1 -1
  63. package/src/assets/web-panel/assets/{Security-DY66Zie6.js → Security-CjqleZpe.js} +4 -4
  64. package/src/assets/web-panel/assets/{Services-RQwxat7-.js → Services-Bu9JSJap.js} +2 -2
  65. package/src/assets/web-panel/assets/{Skeleton-0v37UTU_.js → Skeleton-B2RvRkaX.js} +1 -1
  66. package/src/assets/web-panel/assets/{Skills-B4Vm4DxN.js → Skills-_h42mxMN.js} +1 -1
  67. package/src/assets/web-panel/assets/{Sla-CggphTlo.js → Sla-BssLs56D.js} +1 -1
  68. package/src/assets/web-panel/assets/{SpeechSettings-BAOU08C7.js → SpeechSettings-DCxFYHsd.js} +1 -1
  69. package/src/assets/web-panel/assets/{SyncSettings-DmtC4J1w.js → SyncSettings-D2xQuNLE.js} +2 -2
  70. package/src/assets/web-panel/assets/Tasks-DhpOGOlo.js +1 -0
  71. package/src/assets/web-panel/assets/{Templates-C1QK0YoU.js → Templates-CYG-R-aS.js} +1 -1
  72. package/src/assets/web-panel/assets/{Tenant-CieOfmqp.js → Tenant-BQRYLsvP.js} +1 -1
  73. package/src/assets/web-panel/assets/{Terminal-DWdhrxRq.js → Terminal-imKU7N5j.js} +2 -2
  74. package/src/assets/web-panel/assets/{TimelineRenderer-CjFVUUDU.js → TimelineRenderer-BIZzBftk.js} +1 -1
  75. package/src/assets/web-panel/assets/{Tokens-Bwbk3id9.js → Tokens-uMLH5p_a.js} +1 -1
  76. package/src/assets/web-panel/assets/{Trigger-uJle_yj4.js → Trigger-BzS6XPqx.js} +1 -1
  77. package/src/assets/web-panel/assets/{Trust-BcOuxAA5.js → Trust-R4zhHufZ.js} +1 -1
  78. package/src/assets/web-panel/assets/{UkeySign-DUu7Ufg6.js → UkeySign-DATQCoGe.js} +1 -1
  79. package/src/assets/web-panel/assets/{VideoEditing-Ck8JtQ2n.js → VideoEditing-ClUmKOtS.js} +1 -1
  80. package/src/assets/web-panel/assets/{Wallet-B3jw43on.js → Wallet-DzJTbQzD.js} +4 -4
  81. package/src/assets/web-panel/assets/{WebAuthn-Baf9K0y7.js → WebAuthn-CrXrLmzQ.js} +5 -5
  82. package/src/assets/web-panel/assets/{WorkflowEditor-CTEDl_83.js → WorkflowEditor-CpvZ0Tma.js} +1 -1
  83. package/src/assets/web-panel/assets/{chat-CKV51quV.js → chat-a6wpYmVL.js} +1 -1
  84. package/src/assets/web-panel/assets/{colors-BO_RP_yz.js → colors-CXJADb1t.js} +1 -1
  85. package/src/assets/web-panel/assets/{compact-item-BZsxw_ZG.js → compact-item-CL2pohS_.js} +1 -1
  86. package/src/assets/web-panel/assets/{createContext-CAbvtzVL.js → createContext-xFi_1G5_.js} +1 -1
  87. package/src/assets/web-panel/assets/devWarning-BtmELbtB.js +1 -0
  88. package/src/assets/web-panel/assets/{hasIn-QmHT8zDz.js → hasIn-Bchh1rAi.js} +1 -1
  89. package/src/assets/web-panel/assets/{index-fnDgExTu.js → index-B3Tpv7-d.js} +1 -1
  90. package/src/assets/web-panel/assets/index-B4l4vLTB.js +1 -0
  91. package/src/assets/web-panel/assets/{index-BEJa1FiF.js → index-B4zNisy9.js} +1 -1
  92. package/src/assets/web-panel/assets/{index-jd2r-T4p.js → index-B6NehWty.js} +1 -1
  93. package/src/assets/web-panel/assets/index-B7Ek5iiY.js +1 -0
  94. package/src/assets/web-panel/assets/{index-BPZHeug4.js → index-B7knYOpm.js} +1 -1
  95. package/src/assets/web-panel/assets/{index-GPY0LjCu.js → index-B7wT5VRi.js} +1 -1
  96. package/src/assets/web-panel/assets/{index-DKnngF_f.js → index-BF4xx1_b.js} +1 -1
  97. package/src/assets/web-panel/assets/{index-BRNYA0BV.js → index-BH9t10pe.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-DKquNxL2.js → index-BPH5ESqs.js} +3 -3
  99. package/src/assets/web-panel/assets/{index-CEh2Ry_A.js → index-BmsIKzyu.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-Dob6B6qS.js → index-BoaRB-4a.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-Ha2_56mf.js → index-BrbJBnT-.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-Dln_vjSY.js → index-C2eMYASq.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-CqiKnXtL.js → index-C4yBRKT4.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-B3fwyCjJ.js → index-CGq4HQno.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-B2aiE8jk.js → index-CMybtJY6.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-B5zhcul9.js → index-CR3kFPuC.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-8BMLlHCv.js → index-CTRd7vkq.js} +1 -1
  108. package/src/assets/web-panel/assets/{index-C6i3reUS.js → index-CdU8BwRW.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-BNvTNZ1V.js → index-Cua_P8St.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-DjrDGJP2.js → index-CuehgDOp.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-BCsZiq4i.js → index-D-TT9Swq.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-qPafbZmr.js → index-DEYcLAl7.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-DeC7lehI.js → index-DQ_hw_5P.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-BnPBG3Tr.js → index-DTEu7TSF.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-D8CHQnPl.js → index-DVo1GJoj.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-9IqJODII.js → index-DjdOL159.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-DBCYOypV.js → index-DsbMVBj1.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-BL7gQAuB.js → index-DxahxRP7.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-DC1CFfQU.js → index-EPERz4Pu.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-CVoYeZ5Q.js → index-IkvkNxbc.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-CsBx0u5G.js → index-KCib1PTw.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-5hlO2-JQ.js → index-M8SZI11a.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-CSaI8R_7.js → index-TxbHusq2.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-C6AA-xB2.js → index-dsLc7t6W.js} +1 -1
  125. package/src/assets/web-panel/assets/{index-DRK0oAV5.js → index-jMcv1u5o.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-B9Z83FTS.js → index-majCS3s2.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-C3K1eHDd.js → index-u8K1y_lh.js} +1 -1
  128. package/src/assets/web-panel/assets/{initDefaultProps-Bc2GWeWe.js → initDefaultProps-DYn3Gc09.js} +1 -1
  129. package/src/assets/web-panel/assets/{motion-BI-Rxw6o.js → motion-ZS3eolb9.js} +1 -1
  130. package/src/assets/web-panel/assets/{move-DRPdwDQB.js → move-CEw4uqr3.js} +1 -1
  131. package/src/assets/web-panel/assets/{omit-B4XTl3jW.js → omit-DlHFZnPp.js} +1 -1
  132. package/src/assets/web-panel/assets/{pickAttrs-Do5d86Wr.js → pickAttrs-eZQvV5fA.js} +1 -1
  133. package/src/assets/web-panel/assets/{placementArrow-B8VGZ0ZF.js → placementArrow-B31jQwa-.js} +1 -1
  134. package/src/assets/web-panel/assets/{responsiveObserve-Cf0kI_vN.js → responsiveObserve-DAsNmVto.js} +1 -1
  135. package/src/assets/web-panel/assets/{slide-Cb0psjSL.js → slide-gPQPrYZC.js} +1 -1
  136. package/src/assets/web-panel/assets/{statusUtils-Bjuo5Oal.js → statusUtils-DwWKX5co.js} +1 -1
  137. package/src/assets/web-panel/assets/{styleChecker-BLMhoHJ5.js → styleChecker-B3VOtXuH.js} +1 -1
  138. package/src/assets/web-panel/assets/{useFlexGapSupport-BdCwAfNU.js → useFlexGapSupport-6ADctM2r.js} +1 -1
  139. package/src/assets/web-panel/assets/{useFs-9Jhaz5gG.js → useFs-6Zx1SSKs.js} +1 -1
  140. package/src/assets/web-panel/assets/{usePersonalDataHub-xYFyXKwD.js → usePersonalDataHub-BzReowln.js} +1 -1
  141. package/src/assets/web-panel/assets/{vnode-CVhepE6Z.js → vnode-C8IpEQbD.js} +1 -1
  142. package/src/assets/web-panel/assets/{zoom-IbbtJ4Zr.js → zoom-ruc9vHr0.js} +1 -1
  143. package/src/assets/web-panel/index.html +1 -1
  144. package/src/commands/agent.js +161 -6
  145. package/src/commands/agents.js +199 -0
  146. package/src/commands/command.js +7 -2
  147. package/src/commands/hook.js +136 -28
  148. package/src/commands/ide.js +168 -0
  149. package/src/commands/mcp.js +92 -0
  150. package/src/commands/output-style.js +127 -0
  151. package/src/commands/permissions.js +211 -0
  152. package/src/commands/statusline.js +93 -0
  153. package/src/index.js +8 -0
  154. package/src/lib/agent-core.js +7 -0
  155. package/src/lib/agents.js +147 -0
  156. package/src/lib/hook-manager.js +1 -0
  157. package/src/lib/hook-runner.cjs +183 -0
  158. package/src/lib/ide-bridge.js +310 -0
  159. package/src/lib/image-input.js +156 -0
  160. package/src/lib/mcp-oauth.js +415 -0
  161. package/src/lib/output-styles.js +179 -0
  162. package/src/lib/permission-rules.cjs +325 -0
  163. package/src/lib/provider-options.js +11 -7
  164. package/src/lib/settings-hook-events.cjs +102 -0
  165. package/src/lib/settings-hooks.cjs +163 -0
  166. package/src/lib/settings-loader.cjs +244 -0
  167. package/src/lib/slash-commands.js +21 -13
  168. package/src/lib/status-line.cjs +204 -0
  169. package/src/lib/sub-agent-profiles.js +3 -0
  170. package/src/lib/web-search.js +487 -0
  171. package/src/repl/agent-repl.js +445 -35
  172. package/src/repl/slash-macro.js +45 -0
  173. package/src/runtime/agent-core.js +799 -21
  174. package/src/runtime/coding-agent-contract-shared.cjs +94 -4
  175. package/src/runtime/coding-agent-policy.cjs +24 -0
  176. package/src/runtime/headless-runner.js +162 -6
  177. package/src/runtime/headless-stream.js +133 -7
  178. package/src/runtime/mcp-config.js +161 -15
  179. package/src/runtime/policies/agent-policy.js +1 -0
  180. package/src/runtime/system-prompt.js +6 -1
  181. package/src/assets/web-panel/assets/MobileProjects-CUxONYre.js +0 -1
  182. package/src/assets/web-panel/assets/Tasks-CExqxzL6.js +0 -1
  183. package/src/assets/web-panel/assets/devWarning-DQYatsRR.js +0 -1
  184. package/src/assets/web-panel/assets/index-Bv_y1Ud7.js +0 -1
  185. package/src/assets/web-panel/assets/index-CZZnSJEX.js +0 -1
@@ -0,0 +1,156 @@
1
+ /**
2
+ * image-input — multimodal (vision) input for `cc agent`.
3
+ *
4
+ * cc's internal message format is OpenAI-shaped, so the internal multimodal
5
+ * representation is OpenAI's: a user message whose `content` is an array of
6
+ * `{type:"text"}` / `{type:"image_url", image_url:{url:"data:<media>;base64,..."}}`
7
+ * parts. The OpenAI-compatible providers (openai/volcengine/deepseek/…) accept
8
+ * that verbatim, so their branch needs no conversion. ollama and anthropic use
9
+ * different shapes — `toOllamaMessages` and `imageUrlBlockToAnthropic` convert.
10
+ *
11
+ * All functions are pure (except `resolveImages`, which reads files via the
12
+ * injectable `_deps.fs`).
13
+ */
14
+
15
+ import fs from "fs";
16
+ import path from "path";
17
+
18
+ const EXT_MEDIA = {
19
+ ".png": "image/png",
20
+ ".jpg": "image/jpeg",
21
+ ".jpeg": "image/jpeg",
22
+ ".gif": "image/gif",
23
+ ".webp": "image/webp",
24
+ };
25
+
26
+ /**
27
+ * Read `--image <path>` values into `[{ mediaType, data(base64) }]`.
28
+ * Throws on an unsupported extension so a typo fails loudly instead of sending
29
+ * a broken request.
30
+ */
31
+ export function resolveImages(paths, deps = {}) {
32
+ const _fs = deps.fs || fs;
33
+ if (!Array.isArray(paths) || paths.length === 0) return [];
34
+ return paths.map((p) => {
35
+ const ext = path.extname(String(p)).toLowerCase();
36
+ const mediaType = EXT_MEDIA[ext];
37
+ if (!mediaType) {
38
+ throw new Error(
39
+ `Unsupported image type "${ext || p}" — use png/jpg/jpeg/gif/webp.`,
40
+ );
41
+ }
42
+ const data = _fs.readFileSync(p).toString("base64");
43
+ return { mediaType, data };
44
+ });
45
+ }
46
+
47
+ /**
48
+ * Build a user-message `content`: the plain string when there are no images,
49
+ * else an OpenAI-style multimodal array (the internal representation).
50
+ */
51
+ export function buildUserContent(text, images) {
52
+ if (!Array.isArray(images) || images.length === 0) return text;
53
+ const parts = [];
54
+ if (text) parts.push({ type: "text", text });
55
+ for (const img of images) {
56
+ parts.push({
57
+ type: "image_url",
58
+ image_url: { url: `data:${img.mediaType};base64,${img.data}` },
59
+ });
60
+ }
61
+ return parts;
62
+ }
63
+
64
+ /** Parse a `data:<media>;base64,<data>` URL into `{ mediaType, data }` or null. */
65
+ export function parseDataUrl(url) {
66
+ const m = /^data:([^;,]+);base64,(.*)$/s.exec(String(url || ""));
67
+ return m ? { mediaType: m[1], data: m[2] } : null;
68
+ }
69
+
70
+ /** True when a message carries any image_url part (i.e. needs provider conversion). */
71
+ export function hasImageContent(messages) {
72
+ return (messages || []).some(
73
+ (m) =>
74
+ Array.isArray(m?.content) &&
75
+ m.content.some((p) => p?.type === "image_url"),
76
+ );
77
+ }
78
+
79
+ /**
80
+ * Convert OpenAI-shaped multimodal messages for the ollama `/api/chat` body:
81
+ * ollama wants `{ content: "<text>", images: ["<base64>", …] }` (bare base64,
82
+ * no `data:` prefix). Non-multimodal messages pass through untouched.
83
+ */
84
+ export function toOllamaMessages(messages) {
85
+ return (messages || []).map((m) => {
86
+ if (!m || !Array.isArray(m.content)) return m;
87
+ let text = "";
88
+ const images = [];
89
+ for (const part of m.content) {
90
+ if (part?.type === "text") {
91
+ text += (text ? "\n" : "") + (part.text || "");
92
+ } else if (part?.type === "image_url") {
93
+ const parsed = parseDataUrl(part.image_url?.url);
94
+ if (parsed) images.push(parsed.data);
95
+ else if (part.image_url?.url) images.push(part.image_url.url);
96
+ }
97
+ }
98
+ const out = { ...m, content: text };
99
+ if (images.length) out.images = images;
100
+ return out;
101
+ });
102
+ }
103
+
104
+ /**
105
+ * Convert one OpenAI `image_url` content block into an Anthropic `image` block,
106
+ * or null when it isn't an image_url block / has no usable data URL.
107
+ */
108
+ export function imageUrlBlockToAnthropic(block) {
109
+ if (!block || block.type !== "image_url") return null;
110
+ const parsed = parseDataUrl(block.image_url?.url);
111
+ if (!parsed) return null;
112
+ return {
113
+ type: "image",
114
+ source: { type: "base64", media_type: parsed.mediaType, data: parsed.data },
115
+ };
116
+ }
117
+
118
+ /** Default vision model (Volcengine Ark Doubao-Seed-1.6 Vision) when none configured. */
119
+ export const DEFAULT_VISION_MODEL = "doubao-seed-1-6-vision-250815";
120
+
121
+ /**
122
+ * Resolve the effective LLM config for a run. When an image is attached, default
123
+ * the provider/model/baseUrl/apiKey to the configured vision LLM so
124
+ * `cc agent --image foo.png` works without extra flags — using `llm.visionModel`
125
+ * (a separate, configurable vision model) and falling back to DEFAULT_VISION_MODEL.
126
+ * Explicit flags always win; with no image, behaviour is unchanged (vision config
127
+ * is ignored). `--vision-model` overrides the configured/default vision model.
128
+ *
129
+ * @param {object} p
130
+ * @param {boolean} p.hasImage true when the run carries an attached image
131
+ * @param {object} p.flags { provider, model, baseUrl, apiKey, visionModel }
132
+ * @param {object} p.llm config.llm ({ provider, model, baseUrl, apiKey, visionModel })
133
+ * @returns {{provider, model, baseUrl, apiKey}}
134
+ */
135
+ export function resolveVisionLlm({ hasImage, flags = {}, llm = {} } = {}) {
136
+ // No image → no vision override; the caller falls back to its normal LLM
137
+ // config (`visionLlm.x || options.x`).
138
+ if (!hasImage) {
139
+ return {
140
+ provider: undefined,
141
+ model: undefined,
142
+ baseUrl: undefined,
143
+ apiKey: undefined,
144
+ };
145
+ }
146
+ const visionModel =
147
+ flags.visionModel || llm.visionModel || DEFAULT_VISION_MODEL;
148
+ // `flags.model` must be the EXPLICIT `--model` (not a settings/default), so an
149
+ // attached image uses the vision model unless the user deliberately picked one.
150
+ return {
151
+ provider: flags.provider || llm.provider,
152
+ model: flags.model || visionModel,
153
+ baseUrl: flags.baseUrl || llm.baseUrl,
154
+ apiKey: flags.apiKey || llm.apiKey,
155
+ };
156
+ }
@@ -0,0 +1,415 @@
1
+ /**
2
+ * mcp-oauth — OAuth 2.0 (Authorization Code + PKCE) for remote MCP servers,
3
+ * Claude-Code parity. A remote HTTP/SSE MCP server that requires OAuth is
4
+ * authorized once via `cc mcp login <url>`; the access token is stored and
5
+ * injected as `Authorization: Bearer …` on every connect (refreshed when it
6
+ * expires). Static `-H "Authorization: Bearer …"` headers still work for
7
+ * servers where you already hold a token.
8
+ *
9
+ * The flow (RFC 8414 metadata discovery + RFC 7591 dynamic registration +
10
+ * RFC 7636 PKCE):
11
+ * 1. discover the protected-resource / authorization-server metadata;
12
+ * 2. register a public client (or use a configured client_id);
13
+ * 3. open the browser to the authorize URL, catch the code on a localhost
14
+ * callback, exchange it (with the PKCE verifier) for tokens;
15
+ * 4. persist { access_token, refresh_token, expires_at, client_id, endpoints }.
16
+ *
17
+ * The pure pieces (PKCE, discovery, URL building, token exchange, the store)
18
+ * are `_deps`-injected (fetch / fs / homedir / crypto / http / spawn) so they
19
+ * unit-test without a network, browser, or real OAuth server. The interactive
20
+ * `authorizeInteractive` orchestrator is the thin glue over them.
21
+ */
22
+
23
+ import fsDefault from "node:fs";
24
+ import pathDefault from "node:path";
25
+ import { homedir as homedirDefault } from "node:os";
26
+ import crypto from "node:crypto";
27
+ import http from "node:http";
28
+ import { spawn } from "node:child_process";
29
+
30
+ export const _deps = {
31
+ fetch: (...a) => globalThis.fetch(...a),
32
+ fs: fsDefault,
33
+ homedir: homedirDefault,
34
+ randomBytes: (n) => crypto.randomBytes(n),
35
+ sha256: (s) => crypto.createHash("sha256").update(s).digest(),
36
+ createServer: (h) => http.createServer(h),
37
+ openBrowser: defaultOpenBrowser,
38
+ now: () => Date.now(),
39
+ };
40
+
41
+ const base64url = (buf) =>
42
+ Buffer.from(buf).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
43
+
44
+ /** RFC 7636 PKCE pair (S256). */
45
+ export function generatePkce() {
46
+ const verifier = base64url(_deps.randomBytes(32));
47
+ const challenge = base64url(_deps.sha256(verifier));
48
+ return { verifier, challenge, method: "S256" };
49
+ }
50
+
51
+ /** A random URL-safe state / nonce. */
52
+ export function randomState(bytes = 16) {
53
+ return base64url(_deps.randomBytes(bytes));
54
+ }
55
+
56
+ async function fetchJson(url, opts) {
57
+ const res = await _deps.fetch(url, opts);
58
+ if (!res || !res.ok) {
59
+ const status = res ? res.status : "no-response";
60
+ let body = "";
61
+ try {
62
+ body = res ? await res.text() : "";
63
+ } catch {
64
+ /* ignore */
65
+ }
66
+ const err = new Error(`HTTP ${status}${body ? `: ${body.slice(0, 200)}` : ""}`);
67
+ err.status = res ? res.status : null;
68
+ throw err;
69
+ }
70
+ return res.json();
71
+ }
72
+
73
+ /**
74
+ * Discover the authorization-server metadata for a remote MCP server. Tries the
75
+ * protected-resource doc first (which points at its authorization server), then
76
+ * the authorization-server doc directly at the origin.
77
+ * @returns {Promise<{issuer?,authorization_endpoint,token_endpoint,registration_endpoint?,scopes_supported?}>}
78
+ */
79
+ export async function discoverAuthMetadata(serverUrl, { resourceMetadataUrl } = {}) {
80
+ const origin = new URL(serverUrl).origin;
81
+ // 1. protected-resource metadata (RFC 9728) → authorization_servers[]
82
+ let authServer = origin;
83
+ try {
84
+ const prUrl =
85
+ resourceMetadataUrl || `${origin}/.well-known/oauth-protected-resource`;
86
+ const pr = await fetchJson(prUrl);
87
+ if (Array.isArray(pr.authorization_servers) && pr.authorization_servers[0]) {
88
+ authServer = String(pr.authorization_servers[0]).replace(/\/$/, "");
89
+ }
90
+ } catch {
91
+ // no protected-resource doc → assume the origin is its own auth server
92
+ }
93
+ // 2. authorization-server metadata (RFC 8414)
94
+ const candidates = [
95
+ `${authServer}/.well-known/oauth-authorization-server`,
96
+ `${authServer}/.well-known/openid-configuration`,
97
+ ];
98
+ let lastErr = null;
99
+ for (const url of candidates) {
100
+ try {
101
+ const md = await fetchJson(url);
102
+ if (md && md.authorization_endpoint && md.token_endpoint) {
103
+ return {
104
+ issuer: md.issuer || authServer,
105
+ authorization_endpoint: md.authorization_endpoint,
106
+ token_endpoint: md.token_endpoint,
107
+ registration_endpoint: md.registration_endpoint || null,
108
+ scopes_supported: md.scopes_supported || null,
109
+ };
110
+ }
111
+ } catch (err) {
112
+ lastErr = err;
113
+ }
114
+ }
115
+ throw new Error(
116
+ `could not discover OAuth metadata for ${serverUrl}${lastErr ? ` (${lastErr.message})` : ""}`,
117
+ );
118
+ }
119
+
120
+ /** RFC 7591 dynamic client registration → client_id (public client). */
121
+ export async function registerClient(metadata, { redirectUri, clientName = "chainlesschain-cli" } = {}) {
122
+ if (!metadata.registration_endpoint) {
123
+ throw new Error("server has no registration_endpoint and no --client-id was given");
124
+ }
125
+ const reg = await fetchJson(metadata.registration_endpoint, {
126
+ method: "POST",
127
+ headers: { "content-type": "application/json" },
128
+ body: JSON.stringify({
129
+ client_name: clientName,
130
+ redirect_uris: [redirectUri],
131
+ grant_types: ["authorization_code", "refresh_token"],
132
+ response_types: ["code"],
133
+ token_endpoint_auth_method: "none",
134
+ }),
135
+ });
136
+ if (!reg.client_id) throw new Error("registration did not return a client_id");
137
+ return { clientId: reg.client_id, clientSecret: reg.client_secret || null };
138
+ }
139
+
140
+ /** Build the authorize URL (Authorization Code + PKCE). */
141
+ export function buildAuthorizeUrl(metadata, { clientId, redirectUri, scope, codeChallenge, state, resource }) {
142
+ const u = new URL(metadata.authorization_endpoint);
143
+ u.searchParams.set("response_type", "code");
144
+ u.searchParams.set("client_id", clientId);
145
+ u.searchParams.set("redirect_uri", redirectUri);
146
+ u.searchParams.set("code_challenge", codeChallenge);
147
+ u.searchParams.set("code_challenge_method", "S256");
148
+ u.searchParams.set("state", state);
149
+ if (scope) u.searchParams.set("scope", scope);
150
+ if (resource) u.searchParams.set("resource", resource);
151
+ return u.toString();
152
+ }
153
+
154
+ function _tokenExpiresAt(tok) {
155
+ const ttl = Number(tok.expires_in);
156
+ return Number.isFinite(ttl) && ttl > 0 ? _deps.now() + ttl * 1000 : null;
157
+ }
158
+
159
+ /** Exchange an authorization code for tokens. */
160
+ export async function exchangeCodeForToken(metadata, { code, codeVerifier, clientId, clientSecret, redirectUri, resource }) {
161
+ const body = new URLSearchParams({
162
+ grant_type: "authorization_code",
163
+ code,
164
+ redirect_uri: redirectUri,
165
+ client_id: clientId,
166
+ code_verifier: codeVerifier,
167
+ });
168
+ if (clientSecret) body.set("client_secret", clientSecret);
169
+ if (resource) body.set("resource", resource);
170
+ const tok = await fetchJson(metadata.token_endpoint, {
171
+ method: "POST",
172
+ headers: { "content-type": "application/x-www-form-urlencoded" },
173
+ body: body.toString(),
174
+ });
175
+ return {
176
+ access_token: tok.access_token,
177
+ refresh_token: tok.refresh_token || null,
178
+ token_type: tok.token_type || "Bearer",
179
+ expires_at: _tokenExpiresAt(tok),
180
+ scope: tok.scope || undefined,
181
+ };
182
+ }
183
+
184
+ /** Refresh an access token. */
185
+ export async function refreshAccessToken(metadata, { refreshToken, clientId, clientSecret, resource }) {
186
+ const body = new URLSearchParams({
187
+ grant_type: "refresh_token",
188
+ refresh_token: refreshToken,
189
+ client_id: clientId,
190
+ });
191
+ if (clientSecret) body.set("client_secret", clientSecret);
192
+ if (resource) body.set("resource", resource);
193
+ const tok = await fetchJson(metadata.token_endpoint, {
194
+ method: "POST",
195
+ headers: { "content-type": "application/x-www-form-urlencoded" },
196
+ body: body.toString(),
197
+ });
198
+ return {
199
+ access_token: tok.access_token,
200
+ refresh_token: tok.refresh_token || refreshToken,
201
+ token_type: tok.token_type || "Bearer",
202
+ expires_at: _tokenExpiresAt(tok),
203
+ };
204
+ }
205
+
206
+ // ── token store (~/.chainlesschain/mcp-oauth.json) ────────────────────────
207
+
208
+ /** Stable key for a server (origin) so http/https/path variants share a token. */
209
+ export function serverKey(serverUrl) {
210
+ try {
211
+ return new URL(serverUrl).origin;
212
+ } catch {
213
+ return String(serverUrl || "").trim();
214
+ }
215
+ }
216
+
217
+ export function tokenStorePath() {
218
+ return pathDefault.join(_deps.homedir(), ".chainlesschain", "mcp-oauth.json");
219
+ }
220
+
221
+ export function loadTokenStore() {
222
+ const file = tokenStorePath();
223
+ try {
224
+ if (!_deps.fs.existsSync(file)) return {};
225
+ const data = JSON.parse(_deps.fs.readFileSync(file, "utf-8"));
226
+ return data && typeof data === "object" ? data : {};
227
+ } catch {
228
+ return {};
229
+ }
230
+ }
231
+
232
+ export function getStoredToken(serverUrl) {
233
+ return loadTokenStore()[serverKey(serverUrl)] || null;
234
+ }
235
+
236
+ export function saveStoredToken(serverUrl, record) {
237
+ const file = tokenStorePath();
238
+ const store = loadTokenStore();
239
+ store[serverKey(serverUrl)] = { server: serverKey(serverUrl), ...record };
240
+ _deps.fs.mkdirSync(pathDefault.dirname(file), { recursive: true });
241
+ _deps.fs.writeFileSync(file, JSON.stringify(store, null, 2) + "\n", "utf-8");
242
+ return store[serverKey(serverUrl)];
243
+ }
244
+
245
+ export function deleteStoredToken(serverUrl) {
246
+ const file = tokenStorePath();
247
+ const store = loadTokenStore();
248
+ const key = serverKey(serverUrl);
249
+ if (!(key in store)) return false;
250
+ delete store[key];
251
+ try {
252
+ _deps.fs.writeFileSync(file, JSON.stringify(store, null, 2) + "\n", "utf-8");
253
+ } catch {
254
+ /* best-effort */
255
+ }
256
+ return true;
257
+ }
258
+
259
+ /** True when a record's access token is missing or within `skewMs` of expiry. */
260
+ export function isTokenExpired(record, { skewMs = 60_000 } = {}) {
261
+ if (!record || !record.access_token) return true;
262
+ if (!record.expires_at) return false; // no expiry info → assume valid
263
+ return _deps.now() >= record.expires_at - skewMs;
264
+ }
265
+
266
+ /**
267
+ * Return a valid bearer token for a server, refreshing if expired. Returns the
268
+ * access_token string, or null if there's no stored token / refresh failed.
269
+ */
270
+ export async function ensureValidToken(serverUrl) {
271
+ const record = getStoredToken(serverUrl);
272
+ if (!record) return null;
273
+ if (!isTokenExpired(record)) return record.access_token;
274
+ if (!record.refresh_token || !record.endpoints?.token_endpoint) {
275
+ return record.access_token || null; // can't refresh → use what we have
276
+ }
277
+ try {
278
+ const tok = await refreshAccessToken(
279
+ { token_endpoint: record.endpoints.token_endpoint },
280
+ { refreshToken: record.refresh_token, clientId: record.client_id },
281
+ );
282
+ const updated = saveStoredToken(serverUrl, { ...record, ...tok });
283
+ return updated.access_token;
284
+ } catch {
285
+ return record.access_token || null;
286
+ }
287
+ }
288
+
289
+ // ── interactive orchestrator ──────────────────────────────────────────────
290
+
291
+ function defaultOpenBrowser(url) {
292
+ const platform = process.platform;
293
+ const cmd =
294
+ platform === "win32" ? "cmd" : platform === "darwin" ? "open" : "xdg-open";
295
+ const args =
296
+ platform === "win32" ? ["/c", "start", "", url] : [url];
297
+ try {
298
+ const child = spawn(cmd, args, { stdio: "ignore", detached: true });
299
+ child.unref?.();
300
+ return true;
301
+ } catch {
302
+ return false;
303
+ }
304
+ }
305
+
306
+ /** Wait for the OAuth redirect on a localhost callback server; resolve {code,state}. */
307
+ function waitForCallback({ port, host = "127.0.0.1", path = "/callback", timeout = 300_000 }) {
308
+ return new Promise((resolve, reject) => {
309
+ const server = _deps.createServer((req, res) => {
310
+ let u;
311
+ try {
312
+ u = new URL(req.url, `http://${host}:${port}`);
313
+ } catch {
314
+ res.writeHead(400);
315
+ res.end("bad request");
316
+ return;
317
+ }
318
+ if (u.pathname !== path) {
319
+ res.writeHead(404);
320
+ res.end("not found");
321
+ return;
322
+ }
323
+ const code = u.searchParams.get("code");
324
+ const state = u.searchParams.get("state");
325
+ const error = u.searchParams.get("error");
326
+ res.writeHead(200, { "content-type": "text/html" });
327
+ res.end(
328
+ `<html><body style="font-family:sans-serif"><h3>${error ? "Authorization failed" : "Authorized — you can close this tab."}</h3></body></html>`,
329
+ );
330
+ server.close();
331
+ clearTimeout(timer);
332
+ if (error) reject(new Error(`authorization error: ${error}`));
333
+ else if (!code) reject(new Error("no authorization code in callback"));
334
+ else resolve({ code, state });
335
+ });
336
+ const timer = setTimeout(() => {
337
+ server.close();
338
+ reject(new Error("timed out waiting for the OAuth callback"));
339
+ }, timeout);
340
+ server.on("error", reject);
341
+ server.listen(port, host);
342
+ });
343
+ }
344
+
345
+ /**
346
+ * Run the full interactive Authorization Code + PKCE flow and persist the token.
347
+ *
348
+ * @param {string} serverUrl
349
+ * @param {object} [opts] { scope, clientId, port=53682, host, redirectPath,
350
+ * timeout, writeOut }
351
+ * @returns {Promise<{server, access_token, ...}>} the stored record
352
+ */
353
+ export async function authorizeInteractive(serverUrl, opts = {}) {
354
+ const {
355
+ scope,
356
+ clientId: cfgClientId,
357
+ port = 53682,
358
+ host = "127.0.0.1",
359
+ redirectPath = "/callback",
360
+ timeout = 300_000,
361
+ writeOut = (s) => process.stdout.write(s),
362
+ } = opts;
363
+
364
+ const metadata = await discoverAuthMetadata(serverUrl);
365
+ const redirectUri = `http://${host}:${port}${redirectPath}`;
366
+
367
+ let clientId = cfgClientId;
368
+ let clientSecret = null;
369
+ if (!clientId) {
370
+ const reg = await registerClient(metadata, { redirectUri });
371
+ clientId = reg.clientId;
372
+ clientSecret = reg.clientSecret;
373
+ }
374
+
375
+ const pkce = generatePkce();
376
+ const state = randomState();
377
+ const authorizeUrl = buildAuthorizeUrl(metadata, {
378
+ clientId,
379
+ redirectUri,
380
+ scope: scope || (metadata.scopes_supported ? metadata.scopes_supported.join(" ") : undefined),
381
+ codeChallenge: pkce.challenge,
382
+ state,
383
+ resource: serverUrl,
384
+ });
385
+
386
+ const callbackPromise = waitForCallback({ port, host, path: redirectPath, timeout });
387
+ const opened = _deps.openBrowser(authorizeUrl);
388
+ writeOut(
389
+ (opened
390
+ ? "Opened your browser to authorize.\n"
391
+ : "Open this URL in your browser to authorize:\n") +
392
+ ` ${authorizeUrl}\n`,
393
+ );
394
+
395
+ const { code, state: returnedState } = await callbackPromise;
396
+ if (returnedState !== state) throw new Error("OAuth state mismatch (possible CSRF)");
397
+
398
+ const tok = await exchangeCodeForToken(metadata, {
399
+ code,
400
+ codeVerifier: pkce.verifier,
401
+ clientId,
402
+ clientSecret,
403
+ redirectUri,
404
+ resource: serverUrl,
405
+ });
406
+
407
+ return saveStoredToken(serverUrl, {
408
+ ...tok,
409
+ client_id: clientId,
410
+ endpoints: {
411
+ authorization_endpoint: metadata.authorization_endpoint,
412
+ token_endpoint: metadata.token_endpoint,
413
+ },
414
+ });
415
+ }