@iowarp/clio-coder 0.4.2 → 0.4.3

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 (369) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/CONTRIBUTING.md +86 -19
  3. package/README.md +35 -6
  4. package/dist/{acp-TMDQZDIG.js → acp-H2NGRPWO.js} +11 -11
  5. package/dist/{agents-5N5NG3XG.js → agents-TL5LLUQP.js} +54 -53
  6. package/dist/assets/codewiki.json +1 -1
  7. package/dist/{auth-Z5CCBXKQ.js → auth-E5SW4HMS.js} +19 -16
  8. package/dist/{builtins-K6TNDT24.js → builtins-IA7V7FUC.js} +9 -4
  9. package/dist/{chunk-ZW4HH5JJ.js → chunk-2APPQIER.js} +6 -6
  10. package/dist/{chunk-72GZI5EV.js → chunk-2JH2WHGE.js} +2 -2
  11. package/dist/{chunk-UBRFI4HS.js → chunk-2UG5F4C5.js} +127 -47
  12. package/dist/{chunk-UH632ZYL.js → chunk-2UH2KFUP.js} +2 -2
  13. package/dist/{chunk-3F7VUY77.js → chunk-2VIKGWFZ.js} +2 -2
  14. package/dist/{chunk-I66EAJFY.js → chunk-2WZ546HR.js} +267 -232
  15. package/dist/{chunk-RKSR6VSF.js → chunk-4IUZQIJ3.js} +29 -1
  16. package/dist/{chunk-2X4RYJTJ.js → chunk-4UVU7BJ5.js} +2 -2
  17. package/dist/{chunk-M2DAX4F6.js → chunk-4WR7VSYB.js} +2 -2
  18. package/dist/{chunk-5KW52TEP.js → chunk-54CBCGIR.js} +5 -5
  19. package/dist/{chunk-3KIPBMUA.js → chunk-5ICU3EUH.js} +2 -2
  20. package/dist/{chunk-77QIVUZB.js → chunk-5MEZN6CB.js} +4 -4
  21. package/dist/{chunk-O42A54GG.js → chunk-5OIVVPHF.js} +2 -2
  22. package/dist/{chunk-LJID3DYZ.js → chunk-64I3JVYM.js} +2 -2
  23. package/dist/{chunk-4JDLP6ZS.js → chunk-6PTFB5VS.js} +7 -7
  24. package/dist/{chunk-VN3SHNBN.js → chunk-7DICMOS6.js} +2 -2
  25. package/dist/{chunk-HLAFFSEK.js → chunk-7DRAWPTZ.js} +2 -2
  26. package/dist/chunk-7E7I3WLS.js +3762 -0
  27. package/dist/{chunk-YJISEZKC.js → chunk-7ZYNNDKC.js} +6 -6
  28. package/dist/{chunk-I66ZTYNP.js → chunk-AF4YM7Z4.js} +236 -101
  29. package/dist/{chunk-2HFQNRV3.js → chunk-AX2THNSA.js} +12 -12
  30. package/dist/{chunk-PGF63K6I.js → chunk-B4OAX3SI.js} +65 -3
  31. package/dist/{chunk-W6NIE6OW.js → chunk-B4VEBZKF.js} +3 -3
  32. package/dist/{chunk-JBCS7CRR.js → chunk-BEPZRGGU.js} +10 -10
  33. package/dist/{chunk-XGDPUNND.js → chunk-CE5AX47J.js} +2 -2
  34. package/dist/{chunk-I64IFBLB.js → chunk-DWUOQKRU.js} +17 -10
  35. package/dist/{chunk-DZAW46HP.js → chunk-E3TPLWFX.js} +3 -3
  36. package/dist/{chunk-HIICAHCJ.js → chunk-EKCHAPYA.js} +2 -2
  37. package/dist/{chunk-XE3PCIXH.js → chunk-F5JHEYZM.js} +7 -7
  38. package/dist/{chunk-5PFYMY2V.js → chunk-FTMGRKEF.js} +2 -2
  39. package/dist/{chunk-ZNT2M6TG.js → chunk-G76U63X4.js} +17 -17
  40. package/dist/{chunk-34BHNEE3.js → chunk-GHS5EBTQ.js} +58 -7
  41. package/dist/{chunk-2NHR3NAY.js → chunk-GI7YYQ3F.js} +40 -34
  42. package/dist/{chunk-DYHAXKHD.js → chunk-GWZNEVM2.js} +12 -8
  43. package/dist/chunk-GYV6VZOC.js +26 -0
  44. package/dist/{chunk-MQXIVJ35.js → chunk-HAXOFFRH.js} +5 -5
  45. package/dist/{chunk-W4YEMFBX.js → chunk-HEQY7ZFI.js} +2 -2
  46. package/dist/{chunk-IKOZFYBN.js → chunk-I7ZPNEJM.js} +145 -102
  47. package/dist/{chunk-WNP7O5WZ.js → chunk-ID64D7PE.js} +4 -4
  48. package/dist/{chunk-XQRY4DTA.js → chunk-IGLP3ODT.js} +10 -10
  49. package/dist/chunk-IJNZMHLA.js +101 -0
  50. package/dist/{chunk-JWJGP5DQ.js → chunk-INY6HTFL.js} +7 -7
  51. package/dist/{chunk-PBP4B7XR.js → chunk-IUE3Y34X.js} +2 -2
  52. package/dist/{chunk-B74PXLU7.js → chunk-IWT4SF4R.js} +3 -3
  53. package/dist/{chunk-B7HM5Z7T.js → chunk-JDAY6FIL.js} +5 -5
  54. package/dist/{chunk-PJX3WQUQ.js → chunk-JEQ3XTHC.js} +2 -2
  55. package/dist/{chunk-FSP7CMNU.js → chunk-JGRC33J2.js} +50 -4
  56. package/dist/{chunk-X7IARSHT.js → chunk-JKKCYP3C.js} +9 -9
  57. package/dist/{chunk-HJWWJ6IL.js → chunk-JSC3U7TI.js} +16 -4
  58. package/dist/{chunk-SSEYRH53.js → chunk-KK4JZPBQ.js} +19 -140
  59. package/dist/{chunk-IHXBNWMM.js → chunk-KXDSS5WJ.js} +7 -3
  60. package/dist/{chunk-Q4XWMHX6.js → chunk-L47TF46W.js} +2 -2
  61. package/dist/{chunk-O3YUNJZ2.js → chunk-LDJG7DW3.js} +81 -24
  62. package/dist/{chunk-CDNVLKUX.js → chunk-LLDJM5XK.js} +13 -7
  63. package/dist/{chunk-F2I26BDK.js → chunk-MUW2BDDH.js} +4 -4
  64. package/dist/{chunk-HKMD33FO.js → chunk-MWUZBSAQ.js} +79 -76
  65. package/dist/{chunk-QQLGQY2A.js → chunk-N2Z7HLVY.js} +20 -20
  66. package/dist/{chunk-DZEK6CJN.js → chunk-NIQJ66N4.js} +19 -19
  67. package/dist/{chunk-CWVRRIEI.js → chunk-NZMNUPZZ.js} +2 -2
  68. package/dist/{chunk-462T4EGZ.js → chunk-O5CVSAG5.js} +2 -2
  69. package/dist/{chunk-TPEQIQIE.js → chunk-OML5D5V5.js} +8 -8
  70. package/dist/{chunk-IKSLQ4XV.js → chunk-PAJQJ7BS.js} +558 -216
  71. package/dist/{chunk-ZW55JB7N.js → chunk-PUVDKJ2Y.js} +2 -2
  72. package/dist/{chunk-UH347SHR.js → chunk-QWGDJJYJ.js} +11 -11
  73. package/dist/chunk-R6Q67RJH.js +134 -0
  74. package/dist/{chunk-CRFOIAX3.js → chunk-RRNP2ANY.js} +6 -6
  75. package/dist/{chunk-IDNA72AH.js → chunk-RSJ25QSL.js} +2 -2
  76. package/dist/chunk-SKHCAU7K.js +385 -0
  77. package/dist/{chunk-RLYRBIYQ.js → chunk-TM6LQDI3.js} +20 -12
  78. package/dist/chunk-UOIZ7DA4.js +41 -0
  79. package/dist/{chunk-P75RZCJW.js → chunk-UPZU6GE4.js} +3 -3
  80. package/dist/{chunk-MCMZMDAC.js → chunk-V2ANDPVT.js} +4 -4
  81. package/dist/{chunk-AK5XEFVZ.js → chunk-VA5FNYMT.js} +26 -13
  82. package/dist/{chunk-IMXMHHMQ.js → chunk-VW6DOEDG.js} +332 -57
  83. package/dist/{chunk-XOXV5GKE.js → chunk-W6RRQCPQ.js} +16 -7
  84. package/dist/{chunk-CYZW7JHJ.js → chunk-WBKFA554.js} +8 -8
  85. package/dist/{chunk-BO7Y52RY.js → chunk-WCXUNS7U.js} +7 -7
  86. package/dist/{chunk-ZGNYYXQ6.js → chunk-WRBAGUNF.js} +3 -3
  87. package/dist/{chunk-FVDGR2ZL.js → chunk-XIVNBFZS.js} +85 -30
  88. package/dist/{chunk-BYMNWQ7O.js → chunk-XPWWI35G.js} +299 -58
  89. package/dist/{chunk-KPXDY6QF.js → chunk-XRZT5WY5.js} +2 -2
  90. package/dist/{chunk-AZ4WMN4W.js → chunk-Y3CBHOR6.js} +2 -2
  91. package/dist/{chunk-VXMFAE2W.js → chunk-YPC6ZR5L.js} +19 -6
  92. package/dist/{chunk-54ODD65L.js → chunk-YQWYVTMC.js} +4 -4
  93. package/dist/{chunk-M2WXEHER.js → chunk-ZA4VCIGV.js} +2 -2
  94. package/dist/{chunk-7BHIY2MW.js → chunk-ZDN3Y73Y.js} +6 -6
  95. package/dist/{chunk-E7GT7O5N.js → chunk-ZWPRK62N.js} +7 -4
  96. package/dist/cli/index.js +38 -37
  97. package/dist/{clio-7VB377CC.js → clio-CMMK4KRR.js} +7 -7
  98. package/dist/{code-nav-YVLCYA7V.js → code-nav-MDZNQS33.js} +7 -7
  99. package/dist/{components-UBWCQSRW.js → components-UCUQ4QXW.js} +4 -4
  100. package/dist/{config-4HVOS65E.js → config-SVM5P5YI.js} +76 -74
  101. package/dist/{configure-PIWO7B24.js → configure-LE3IK2TJ.js} +26 -24
  102. package/dist/{context-IYEHL3WQ.js → context-2OHRKS42.js} +66 -63
  103. package/dist/{context-N6ZE3LGJ.js → context-E3VC7RX5.js} +15 -11
  104. package/dist/{context-KQYIWPWT.js → context-VNCR7KAG.js} +60 -45
  105. package/dist/{context-clear-G4OGZJDS.js → context-clear-BW4O37TG.js} +61 -59
  106. package/dist/context-map-COB37XXN.js +505 -0
  107. package/dist/{context-working-set-BWLF6LJP.js → context-working-set-VDS25HXZ.js} +17 -16
  108. package/dist/{dispatch-runner-2QQAITS3.js → dispatch-runner-5AHT53RF.js} +85 -74
  109. package/dist/{doctor-LHBD36VU.js → doctor-WNNVO6FY.js} +37 -37
  110. package/dist/{eval-C45FYRJ6.js → eval-7G7SGAYO.js} +285 -114
  111. package/dist/{eval-inventory-6DEJPLBF.js → eval-inventory-Y6QRFOH5.js} +4 -4
  112. package/dist/{evidence-6SHONYAF.js → evidence-VD6736FQ.js} +63 -62
  113. package/dist/{evolve-KRKMV72X.js → evolve-AL3NGVRL.js} +62 -61
  114. package/dist/{extensions-KPZ2UHBB.js → extensions-MOVJ32NM.js} +7 -7
  115. package/dist/{fleet-IVTCKDHT.js → fleet-QZHUMAGI.js} +110 -108
  116. package/dist/{fleet-commands-EDWL3IT7.js → fleet-commands-BAYT5FJZ.js} +10 -10
  117. package/dist/{fleet-decisions-YP3YEFGK.js → fleet-decisions-IREVMRU4.js} +7 -6
  118. package/dist/{fleet-graph-ZFWKHY2M.js → fleet-graph-YCTT3HTI.js} +19 -18
  119. package/dist/{fleet-inspect-FVUNCBML.js → fleet-inspect-QVJTDAVB.js} +55 -54
  120. package/dist/{fleet-preflight-UN5XED4R.js → fleet-preflight-25QAFPK4.js} +4 -4
  121. package/dist/{fleet-validate-XOWC4HSX.js → fleet-validate-5O57AAJ7.js} +23 -22
  122. package/dist/{fleet-verify-UN3SODEL.js → fleet-verify-CPH2W2T6.js} +56 -55
  123. package/dist/{fleet-view-TWHJKCN6.js → fleet-view-SWBR3VGQ.js} +55 -54
  124. package/dist/{init-T2QORQ3Y.js → init-J477LKZH.js} +78 -76
  125. package/dist/{interop-IN5I2A66.js → interop-3FCM6XLG.js} +11 -11
  126. package/dist/{library-LSCATDLZ.js → library-QUQEIUG6.js} +28 -27
  127. package/dist/{memory-HYOKAGGJ.js → memory-SGGSEP65.js} +64 -63
  128. package/dist/{models-2GPMFYCM.js → models-HEKUAXXK.js} +49 -43
  129. package/dist/{monitor-E4ASVUJH.js → monitor-HKU57TYQ.js} +61 -60
  130. package/dist/{orchestrator-DDMPR3PY.js → orchestrator-VDFAEFAI.js} +919 -546
  131. package/dist/{panes-E3RUXOW5.js → panes-DN2SSFOH.js} +3 -3
  132. package/dist/{panes-IXKLOKA2.js → panes-TALGNPZT.js} +8 -8
  133. package/dist/{paths-L7LGY6RN.js → paths-NBMFAIEZ.js} +5 -5
  134. package/dist/reset-EAJFFJVB.js +344 -0
  135. package/dist/{resources-OTRSN34L.js → resources-OVKSEFVE.js} +27 -20
  136. package/dist/{run-5DEYH5QK.js → run-7DP7ZF2J.js} +113 -109
  137. package/dist/{share-IHWTLO3M.js → share-WML67FT3.js} +26 -25
  138. package/dist/{skills-IYMXMKW4.js → skills-SG662R2K.js} +39 -31
  139. package/dist/{skills-eval-DROHSJAR.js → skills-eval-VVZEUU46.js} +74 -73
  140. package/dist/{skills-inventory-D7X4L4ZX.js → skills-inventory-I2E23GET.js} +21 -20
  141. package/dist/{slash-commands-QBM7UZ3B.js → slash-commands-S7MBJDQK.js} +35 -34
  142. package/dist/{steer-Z5DO23FJ.js → steer-2LQOMCPB.js} +3 -3
  143. package/dist/{support-U7QOWY26.js → support-CC2UJBJ6.js} +6 -6
  144. package/dist/{targets-P2FUC4IL.js → targets-4QC3HIEW.js} +48 -45
  145. package/dist/{terminal-lease-YREJ3JX2.js → terminal-lease-TUHIJ6Y2.js} +2 -2
  146. package/dist/{tools-5B7RO6MV.js → tools-TFGJICCU.js} +8 -8
  147. package/dist/{trace-YMGMUM6A.js → trace-FXMXUZUF.js} +7 -7
  148. package/dist/uninstall-5PEVOE5B.js +408 -0
  149. package/dist/upgrade-M4WXY6KN.js +303 -0
  150. package/dist/{usage-ME5MPXGX.js → usage-N7ZNVLEM.js} +147 -102
  151. package/dist/{verifiers-BVZ7IWOO.js → verifiers-DJTP4XX6.js} +15 -15
  152. package/dist/{verify-5K7ZKQFC.js → verify-RWE4PPEK.js} +9 -9
  153. package/dist/{wiki-generate-F5W5QTYY.js → wiki-generate-C7IQOXSP.js} +84 -82
  154. package/dist/{with-panes-BYOJCLAM.js → with-panes-4GCGSL7J.js} +9 -9
  155. package/dist/worker/entry.js +61 -60
  156. package/docs/architecture/artifact-placement.md +1 -0
  157. package/docs/architecture/artifact-versions.md +1 -1
  158. package/docs/architecture/context-engine.md +4 -0
  159. package/docs/architecture/middleware-and-components.md +1 -1
  160. package/docs/architecture/model-catalog.md +21 -10
  161. package/docs/architecture/observability.md +12 -1
  162. package/docs/architecture/prompt-envelope-and-tools.md +2 -0
  163. package/docs/architecture/provider-adapter-cookbook.md +63 -0
  164. package/docs/architecture/safety-model.md +15 -5
  165. package/docs/guide/built-in-agents.md +17 -3
  166. package/docs/guide/commands-and-modes.md +1 -1
  167. package/docs/guide/configuration-and-targets.md +97 -9
  168. package/docs/guide/configuration-reference.md +7 -2
  169. package/docs/guide/environment-variables.md +2 -0
  170. package/docs/guide/installation-and-lifecycle.md +37 -4
  171. package/docs/guide/proactive-memory.md +66 -55
  172. package/docs/guide/skills-marketplace.md +18 -0
  173. package/docs/process/development-pipeline.md +34 -1
  174. package/docs/process/eval-runner.md +67 -3
  175. package/evals/behavioral-model.yaml +3 -2
  176. package/package.json +2 -2
  177. package/skills/README.md +7 -5
  178. package/skills/coding/ast-grep/SKILL.md +101 -30
  179. package/skills/coding/ast-grep/evals.md +26 -0
  180. package/skills/coding/coding-standards/SKILL.md +40 -5
  181. package/skills/coding/coding-standards/evals.md +23 -0
  182. package/skills/coding/prototype/SKILL.md +87 -28
  183. package/skills/coding/prototype/evals.md +19 -0
  184. package/skills/coding/tdd/SKILL.md +80 -53
  185. package/skills/coding/tdd/evals.md +20 -0
  186. package/skills/context/context-handoff/SKILL.md +43 -2
  187. package/skills/context/context-handoff/evals.md +44 -0
  188. package/skills/context/context-prime/SKILL.md +45 -15
  189. package/skills/context/context-prime/evals.md +45 -0
  190. package/skills/git/branch-closeout/SKILL.md +132 -0
  191. package/skills/git/branch-closeout/evals.md +133 -0
  192. package/skills/git/branch-closeout/references/closeout-checklist.md +81 -0
  193. package/skills/git/file-ticket/SKILL.md +77 -63
  194. package/skills/git/file-ticket/assets/issue-template.md +22 -0
  195. package/skills/git/file-ticket/evals.md +31 -26
  196. package/skills/git/file-ticket/references/issue-discovery.md +49 -0
  197. package/skills/git/fix-issue/SKILL.md +87 -64
  198. package/skills/git/fix-issue/evals.md +35 -31
  199. package/skills/git/fix-issue/references/diagnosis-and-rca.md +46 -0
  200. package/skills/git/resolve-merge-conflicts/SKILL.md +100 -51
  201. package/skills/git/resolve-merge-conflicts/evals.md +52 -25
  202. package/skills/git/resolve-merge-conflicts/references/conflict-matrix.md +126 -0
  203. package/skills/git/ship/SKILL.md +103 -67
  204. package/skills/git/ship/assets/pr-template.md +21 -0
  205. package/skills/git/ship/evals.md +44 -28
  206. package/skills/git/ship/references/remote-and-branch-policy.md +62 -0
  207. package/skills/git/worktree-create/SKILL.md +80 -50
  208. package/skills/git/worktree-create/evals.md +40 -33
  209. package/skills/git/worktree-create/references/worktree-setup.md +62 -66
  210. package/skills/git/worktree-merge/SKILL.md +112 -65
  211. package/skills/git/worktree-merge/evals.md +42 -34
  212. package/skills/git/worktree-merge/references/merge-strategies.md +52 -0
  213. package/skills/planning/archify/SKILL.md +196 -0
  214. package/skills/planning/archify/evals.md +65 -0
  215. package/skills/planning/architecture/SKILL.md +61 -12
  216. package/skills/planning/architecture/evals.md +65 -0
  217. package/skills/planning/backlog/SKILL.md +130 -14
  218. package/skills/planning/backlog/evals.md +142 -0
  219. package/skills/planning/prd/SKILL.md +46 -6
  220. package/skills/planning/prd/evals.md +54 -0
  221. package/skills/planning/product-intent/SKILL.md +57 -2
  222. package/skills/planning/product-intent/evals.md +70 -0
  223. package/skills/planning/tech-spec/SKILL.md +53 -2
  224. package/skills/planning/tech-spec/evals.md +73 -0
  225. package/skills/registry.yaml +58 -50
  226. package/skills/remote.yaml +13 -0
  227. package/skills/research/arxiv-literature/SKILL.md +76 -18
  228. package/skills/research/arxiv-literature/evals.md +50 -0
  229. package/skills/research/experiment-protocol/SKILL.md +20 -1
  230. package/skills/research/experiment-protocol/evals.md +23 -0
  231. package/skills/research/scientific-debugging/SKILL.md +23 -1
  232. package/skills/research/scientific-debugging/evals.md +18 -0
  233. package/skills/research/scientific-modernization/SKILL.md +26 -1
  234. package/skills/research/scientific-modernization/evals.md +27 -0
  235. package/skills/skill-marketplace.json +63 -28
  236. package/skills/workflow/cut-it/SKILL.md +65 -5
  237. package/skills/workflow/cut-it/evals.md +101 -0
  238. package/skills/workflow/design-council/SKILL.md +117 -27
  239. package/skills/workflow/design-council/evals.md +161 -0
  240. package/skills/workflow/grill-me/SKILL.md +86 -10
  241. package/skills/workflow/grill-me/evals.md +153 -0
  242. package/skills/workflow/workflow-distiller/SKILL.md +76 -17
  243. package/skills/workflow/workflow-distiller/evals.md +118 -0
  244. package/src/cli/configure-interop.ts +105 -13
  245. package/src/cli/configure-oauth.ts +57 -0
  246. package/src/cli/configure-onboarding.ts +980 -0
  247. package/src/cli/configure-target.ts +594 -0
  248. package/src/cli/configure.ts +1082 -528
  249. package/src/cli/context-map.ts +114 -0
  250. package/src/cli/context.ts +4 -0
  251. package/src/cli/index.ts +1 -0
  252. package/src/cli/lifecycle-presenter.ts +436 -0
  253. package/src/cli/models.ts +10 -2
  254. package/src/cli/modes/print.ts +5 -1
  255. package/src/cli/reset.ts +228 -106
  256. package/src/cli/run.ts +7 -2
  257. package/src/cli/select.ts +664 -0
  258. package/src/cli/skills.ts +9 -2
  259. package/src/cli/targets.ts +3 -0
  260. package/src/cli/uninstall.ts +233 -165
  261. package/src/cli/upgrade.ts +204 -149
  262. package/src/cli/usage.ts +86 -27
  263. package/src/cli/validate-model.ts +3 -3
  264. package/src/core/config.ts +56 -0
  265. package/src/core/external-diagnostic.ts +44 -0
  266. package/src/core/gateway-routing.ts +157 -0
  267. package/src/core/safe-exec.ts +17 -2
  268. package/src/core/skill-activation.ts +89 -2
  269. package/src/domains/agents/builtins/world-knowledge.md +31 -0
  270. package/src/domains/agents/catalog.ts +1 -1
  271. package/src/domains/agents/result-contract.ts +70 -0
  272. package/src/domains/context/wiki/map-seed.ts +589 -0
  273. package/src/domains/context/wiki/plan.ts +2 -2
  274. package/src/domains/dispatch/admission.ts +29 -0
  275. package/src/domains/dispatch/agent-candidates.ts +10 -0
  276. package/src/domains/dispatch/budget-envelope.ts +86 -1
  277. package/src/domains/dispatch/capability-match.ts +1 -0
  278. package/src/domains/dispatch/capacity-lease.ts +17 -0
  279. package/src/domains/dispatch/contract.ts +11 -1
  280. package/src/domains/dispatch/extension.ts +134 -29
  281. package/src/domains/dispatch/types.ts +3 -0
  282. package/src/domains/dispatch/worker-model-metadata.ts +38 -0
  283. package/src/domains/eval/metrics/call-ledger-stream.ts +34 -11
  284. package/src/domains/eval/metrics/token-stream.ts +201 -31
  285. package/src/domains/eval/metrics/tracked.ts +40 -4
  286. package/src/domains/eval/runners/clio-run.ts +5 -2
  287. package/src/domains/eval/schema/suite.ts +28 -0
  288. package/src/domains/eval/schema/verdict.ts +2 -2
  289. package/src/domains/eval/suites/resolve.ts +13 -1
  290. package/src/domains/eval/suites/run.ts +24 -3
  291. package/src/domains/interop/registry.ts +6 -2
  292. package/src/domains/interop/types.ts +4 -0
  293. package/src/domains/lifecycle/migrations/index.ts +4 -0
  294. package/src/domains/memory/task-memory-policy.ts +70 -26
  295. package/src/domains/memory/task-memory-telemetry.ts +1 -0
  296. package/src/domains/middleware/index.ts +0 -1
  297. package/src/domains/middleware/marketplace-offer.ts +3 -35
  298. package/src/domains/middleware/memory-intervention.ts +127 -32
  299. package/src/domains/middleware/memory-step-endpoint.ts +3 -2
  300. package/src/domains/middleware/skills-reminder.ts +31 -2
  301. package/src/domains/observability/compaction-usage.ts +118 -0
  302. package/src/domains/observability/cost.ts +1 -1
  303. package/src/domains/observability/extension.ts +6 -1
  304. package/src/domains/observability/out-of-turn-usage.ts +52 -21
  305. package/src/domains/providers/contract.ts +4 -1
  306. package/src/domains/providers/extension.ts +40 -9
  307. package/src/domains/providers/model-capabilities.ts +9 -0
  308. package/src/domains/providers/model-discovery.ts +2 -0
  309. package/src/domains/providers/model-runtime-capabilities.ts +15 -5
  310. package/src/domains/providers/models/local-models/clio-coder-local-coding-targets.yaml +32 -12
  311. package/src/domains/providers/runtimes/antigravity/antigravity-code.ts +225 -45
  312. package/src/domains/providers/runtimes/common/lmstudio-http.ts +6 -2
  313. package/src/domains/providers/runtimes/common/local-synth.ts +2 -0
  314. package/src/domains/providers/runtimes/protocol/litellm.ts +119 -29
  315. package/src/domains/providers/support.ts +11 -5
  316. package/src/domains/providers/target-model-cache.ts +25 -2
  317. package/src/domains/providers/types/capability-flags.ts +2 -0
  318. package/src/domains/providers/types/runtime-descriptor.ts +20 -1
  319. package/src/domains/providers/types/target-descriptor.ts +19 -0
  320. package/src/domains/resources/index.ts +3 -0
  321. package/src/domains/resources/skills/install.ts +72 -7
  322. package/src/domains/resources/skills/loader.ts +7 -0
  323. package/src/domains/resources/skills/marketplace.ts +63 -11
  324. package/src/domains/safety/autonomy.ts +15 -0
  325. package/src/domains/safety/index.ts +1 -0
  326. package/src/domains/safety/path-policy.ts +1 -1
  327. package/src/domains/safety/policy-engine.ts +34 -11
  328. package/src/domains/safety/protected-artifacts.ts +191 -88
  329. package/src/domains/safety/run-effects.ts +2 -22
  330. package/src/domains/safety/skill-authority.ts +55 -0
  331. package/src/domains/session/compaction/compact.ts +72 -22
  332. package/src/domains/session/entries.ts +6 -0
  333. package/src/domains/session/usage.ts +3 -3
  334. package/src/engine/agent.ts +13 -3
  335. package/src/engine/ai.ts +26 -8
  336. package/src/engine/antigravity/subprocess-runtime.ts +386 -120
  337. package/src/engine/api-registry.ts +3 -0
  338. package/src/engine/apis/openai-completions.ts +117 -14
  339. package/src/engine/external-subprocess.ts +114 -6
  340. package/src/entry/background-model-metadata.ts +18 -0
  341. package/src/entry/compaction-prompt.ts +57 -0
  342. package/src/entry/orchestrator.ts +405 -216
  343. package/src/entry/task-memory-lifecycle.ts +35 -0
  344. package/src/interactive/chat-loop-messages.ts +13 -4
  345. package/src/interactive/chat-loop.ts +65 -2
  346. package/src/interactive/chat-renderer.ts +1 -0
  347. package/src/interactive/cost-overlay.ts +26 -2
  348. package/src/interactive/interactive-slash-runtime.ts +2 -1
  349. package/src/interactive/renderers/worker-entry.ts +32 -0
  350. package/src/interactive/slash-commands.ts +24 -6
  351. package/src/interactive/theme/labels.ts +19 -13
  352. package/src/interactive/turn-context.ts +9 -5
  353. package/src/interactive/turn-recovery.ts +8 -0
  354. package/src/interactive/turn-runtime.ts +27 -11
  355. package/src/interactive/turn-state.ts +7 -0
  356. package/src/interactive/worker-receipts.ts +1 -0
  357. package/src/interactive/worker-stream.ts +6 -1
  358. package/src/tools/context/index.ts +30 -9
  359. package/src/tools/dispatch-arguments.ts +1 -0
  360. package/src/tools/dispatch-event-text.ts +10 -0
  361. package/src/tools/dispatch-plan.ts +1 -0
  362. package/src/tools/dispatch-runner.ts +12 -0
  363. package/src/tools/registry.ts +11 -5
  364. package/src/tools/worker-evidence.ts +3 -1
  365. package/src/worker/spec-contract.ts +4 -0
  366. package/dist/chunk-2Z2IKEXI.js +0 -1554
  367. package/dist/reset-OAQP3W4O.js +0 -230
  368. package/dist/uninstall-N34PCTGJ.js +0 -331
  369. package/dist/upgrade-PXK3S2YM.js +0 -325
@@ -1,40 +1,44 @@
1
+ import { existsSync, readFileSync } from "node:fs";
1
2
  import { stdin as input, stdout as output } from "node:process";
2
3
  import { createInterface } from "node:readline/promises";
3
4
  import {
5
+ bindAgentProfileInSettings,
4
6
  type ClioSettings,
5
7
  readSettings,
6
8
  removeTargetFromSettings,
7
9
  settingsPath,
8
10
  updateSettings,
9
11
  } from "../core/config.js";
12
+ import {
13
+ type AutonomyLevel,
14
+ type OutputVerbosity,
15
+ type PanesSettings,
16
+ type SmoothStreaming,
17
+ THINKING_LEVELS,
18
+ type ThinkingLevel,
19
+ type TuiMode,
20
+ type WorkerPermissionMode,
21
+ } from "../core/defaults.js";
10
22
  import { initializeClioHome } from "../core/init.js";
11
- import { openAuthStorage, resolveAuthTarget, targetRequiresAuth } from "../domains/providers/auth/index.js";
12
- import { getCatalogModelForRuntime } from "../domains/providers/catalog.js";
13
- import { credentialsPresent } from "../domains/providers/credentials.js";
23
+ import { resolveClioDirs } from "../core/xdg.js";
24
+ import { getVersionInfo } from "../domains/lifecycle/version.js";
25
+ import { openAuthStorage, targetRequiresAuth } from "../domains/providers/auth/index.js";
14
26
  import {
15
27
  buildProviderSupportEntry,
16
28
  configuredTargetsForRuntime,
17
29
  describeRuntimeModels,
18
30
  isOrchestratorEligibleRuntime,
19
- listKnownModelsForRuntime,
20
31
  listProviderSupportEntries,
21
32
  type ProviderSupportEntry,
22
- readTargetModelSnapshot,
23
33
  recordTargetModelSnapshot,
24
34
  resolveRuntimeAuthTarget,
25
35
  supportGroupLabel,
26
36
  } from "../domains/providers/index.js";
27
- import { probeCapabilitiesForModel } from "../domains/providers/model-capabilities.js";
28
37
  import { fingerprintNativeRuntime } from "../domains/providers/probe/fingerprint.js";
29
38
  import { getRuntimeRegistry } from "../domains/providers/registry.js";
30
39
  import { registerBuiltinRuntimes } from "../domains/providers/runtimes/builtins.js";
31
40
  import { greetLmStudio } from "../domains/providers/runtimes/common/lmstudio-http.js";
32
- import type {
33
- ProbeContext,
34
- ProbeModelStatus,
35
- ProbeResult,
36
- RuntimeDescriptor,
37
- } from "../domains/providers/types/runtime-descriptor.js";
41
+ import type { ProbeResult, RuntimeDescriptor } from "../domains/providers/types/runtime-descriptor.js";
38
42
  import type { TargetDescriptor } from "../domains/providers/types/target-descriptor.js";
39
43
  import { registerClioOAuthProviders } from "../engine/oauth.js";
40
44
  import { ask, askYesNo } from "./ask.js";
@@ -47,22 +51,57 @@ import {
47
51
  matchCategoryChoice,
48
52
  type RuntimeListRow,
49
53
  } from "./configure-layout.js";
50
- import { createDelayedManualCodeInput } from "./oauth-manual-input.js";
51
- import { promptOAuthSelection } from "./oauth-select.js";
54
+ import { loginOAuthRuntime } from "./configure-oauth.js";
55
+ import { canRunOnboarding, runOnboardingWizard } from "./configure-onboarding.js";
56
+ import {
57
+ applyTarget,
58
+ assertOrchestratorReplacementEligible,
59
+ buildDescriptor,
60
+ defaultUrlFor,
61
+ deriveTargetId,
62
+ describeAuthStatus,
63
+ normalizeUrl,
64
+ PROTOCOL_COMPAT_RUNTIME_IDS,
65
+ probeLines,
66
+ railPrefix,
67
+ resolveSupportedWireModels,
68
+ runtimeProbe,
69
+ runtimesForCategory,
70
+ setBackgroundPointer,
71
+ setOrchestratorPointer,
72
+ setWorkerDefaultPointer,
73
+ setWorkerProfilePointer,
74
+ validateContextWindowOverride,
75
+ validateResolvedModel,
76
+ type WireModelInventory,
77
+ warnUndiscoveredContextWindow,
78
+ } from "./configure-target.js";
79
+ import { createLifecyclePresenter, type LifecyclePresenter, shortenPath } from "./lifecycle-presenter.js";
80
+ import { canSelect, promptSelect } from "./select.js";
52
81
  import { credentialWriteFailed, printError, printOk, printPlaintextCredentialWarning } from "./shared.js";
53
82
  import { terminalColumns, wrapPlain } from "./text-layout.js";
54
- import { type LiveModelInventory, validateModelChoice } from "./validate-model.js";
55
83
 
56
84
  const HELP = `clio-coder configure
57
85
 
58
- Configure model targets for chat and fleet-agent dispatch.
86
+ Configure model targets and runtime settings for chat and fleet dispatch.
59
87
 
60
88
  Usage:
61
- clio-coder configure interactive wizard
89
+ clio-coder configure interactive configuration wizard
90
+ clio-coder configure --section <name> open one section directly:
91
+ targets, models, chat, fleet,
92
+ permissions, panes, skills, diagnostics
93
+ Without a terminal this prints the
94
+ section's values and exits.
95
+ clio-coder configure --json emit the effective settings as JSON
62
96
  clio-coder configure --interop review detected coding agents as delegation peers
63
97
  clio-coder configure --list list target runtimes (user-facing only)
64
98
  clio-coder configure --list --all list every registered runtime including aliases
65
99
  clio-coder configure --id <targetId> [flags] --runtime <runtimeId>
100
+ clio-coder configure --help, -h show this message
101
+
102
+ In the interactive menus: arrow keys move, enter opens, escape goes back, q
103
+ quits. Where the terminal cannot support that, the same menus are numbered and
104
+ answered by typing a number, b for back, or q to quit.
66
105
 
67
106
  Non-interactive flags:
68
107
  --id <targetId> target id to register (required when non-interactive)
@@ -75,6 +114,7 @@ Non-interactive flags:
75
114
  (mutually exclusive with --agent-profile)
76
115
  --agent-profile <name> save this target as a named fleet profile
77
116
  --agent-profile-model <id> model to use for --agent-profile
117
+ --bind-agent <agentId> bind this --agent-profile to an agent
78
118
  --api-key-env <VAR> read API key from this env var at call time
79
119
  --api-key <literal> store API key in credentials.yaml
80
120
  --force save a model outside the runtime catalog, or one the
@@ -90,30 +130,14 @@ Non-interactive flags:
90
130
  --reasoning <true|false> capability override
91
131
  `;
92
132
 
93
- const DEFAULT_PORTS: Record<string, number> = {
94
- llamacpp: 8080,
95
- "llamacpp-anthropic": 8080,
96
- "llamacpp-completion": 8080,
97
- "llamacpp-embed": 8080,
98
- "llamacpp-rerank": 8080,
99
- "lmstudio-native": 1234,
100
- lmstudio: 1234,
101
- "ollama-native": 11434,
102
- vllm: 8000,
103
- sglang: 30000,
104
- "lemonade-anthropic": 8000,
105
- lemonade: 8000,
106
- "anthropic-compat": 8000,
107
- "openai-compat": 8000,
108
- litellm: 4000,
109
- };
110
-
111
133
  interface ParsedArgs {
112
134
  positional: string[];
113
135
  help: boolean;
114
136
  list: boolean;
115
137
  all: boolean;
116
138
  interop: boolean;
139
+ json: boolean;
140
+ section?: string;
117
141
  remove?: string;
118
142
  renameOld?: string;
119
143
  renameNew?: string;
@@ -126,6 +150,7 @@ interface ParsedArgs {
126
150
  workerModel?: string;
127
151
  workerProfile?: string;
128
152
  workerProfileModel?: string;
153
+ bindAgent?: string;
129
154
  apiKeyEnv?: string;
130
155
  apiKey?: string;
131
156
  force: boolean;
@@ -146,6 +171,7 @@ function parseSetupArgs(argv: ReadonlyArray<string>): ParsedArgs {
146
171
  list: false,
147
172
  all: false,
148
173
  interop: false,
174
+ json: false,
149
175
  force: false,
150
176
  gateway: false,
151
177
  setOrchestrator: false,
@@ -165,6 +191,12 @@ function parseSetupArgs(argv: ReadonlyArray<string>): ParsedArgs {
165
191
  case "-h":
166
192
  out.help = true;
167
193
  break;
194
+ case "--json":
195
+ out.json = true;
196
+ break;
197
+ case "--section":
198
+ out.section = need();
199
+ break;
168
200
  case "--list":
169
201
  out.list = true;
170
202
  break;
@@ -208,6 +240,9 @@ function parseSetupArgs(argv: ReadonlyArray<string>): ParsedArgs {
208
240
  case "--agent-profile-model":
209
241
  out.workerProfileModel = need();
210
242
  break;
243
+ case "--bind-agent":
244
+ out.bindAgent = need();
245
+ break;
211
246
  case "--api-key-env":
212
247
  out.apiKeyEnv = need();
213
248
  break;
@@ -269,32 +304,6 @@ function ensureRegistryPopulated(): void {
269
304
  if (registry.list().length === 0) registerBuiltinRuntimes(registry);
270
305
  }
271
306
 
272
- function trimTrailing(url: string): string {
273
- return url.endsWith("/") && url.length > 1 ? url.slice(0, -1) : url;
274
- }
275
-
276
- function normalizeUrl(input: string, runtimeId: string): string {
277
- const trimmed = input.trim();
278
- if (trimmed.length === 0) return defaultUrlFor(runtimeId);
279
- const hasScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed);
280
- let withScheme = hasScheme ? trimmed : `http://${trimmed}`;
281
- if (runtimeId === "lmstudio") withScheme = withScheme.replace(/^ws:/u, "http:").replace(/^wss:/u, "https:");
282
- try {
283
- const parsed = new URL(withScheme);
284
- if (!hasScheme && !parsed.port && DEFAULT_PORTS[runtimeId]) {
285
- parsed.port = String(DEFAULT_PORTS[runtimeId]);
286
- }
287
- return trimTrailing(parsed.toString());
288
- } catch {
289
- return trimTrailing(withScheme);
290
- }
291
- }
292
-
293
- function defaultUrlFor(runtimeId: string): string {
294
- const port = DEFAULT_PORTS[runtimeId];
295
- return port ? `http://127.0.0.1:${port}` : "http://127.0.0.1:8080";
296
- }
297
-
298
307
  const RUNTIME_LIST_CAPTION =
299
308
  "every registered runtime. run `clio-coder auth list` for the ones clio authenticates itself; the rest authenticate through their own tool (claude-cli, aws-sdk) or need no credential.";
300
309
 
@@ -342,256 +351,6 @@ function printRuntimeList(includeHidden: boolean): void {
342
351
  }
343
352
  }
344
353
 
345
- function deriveTargetId(runtimeId: string, existing: ReadonlyArray<TargetDescriptor>): string {
346
- const base = runtimeId;
347
- const taken = new Set(existing.map((e) => e.id));
348
- if (!taken.has(base)) return base;
349
- for (let i = 2; i < 1000; i++) {
350
- const candidate = `${base}-${i}`;
351
- if (!taken.has(candidate)) return candidate;
352
- }
353
- return `${base}-${Date.now()}`;
354
- }
355
-
356
- async function buildProbeContext(runtime?: RuntimeDescriptor, target?: TargetDescriptor): Promise<ProbeContext> {
357
- const probeCtx: ProbeContext = {
358
- credentialsPresent: credentialsPresent(),
359
- httpTimeoutMs: 5000,
360
- };
361
- if (!runtime || !target || !targetRequiresAuth(target, runtime)) return probeCtx;
362
- try {
363
- const resolution = await openAuthStorage().resolveForTarget(resolveAuthTarget(target, runtime), {
364
- includeFallback: false,
365
- });
366
- if (resolution.apiKey) probeCtx.authToken = resolution.apiKey;
367
- } catch {
368
- // Let the runtime's probe report its own missing-auth diagnostic.
369
- }
370
- return probeCtx;
371
- }
372
-
373
- function runtimeKnownModelsFor(runtimeId: string): ReadonlyArray<string> {
374
- return listKnownModelsForRuntime(runtimeId);
375
- }
376
-
377
- function catalogContextWindowFor(runtime: RuntimeDescriptor, modelId: string): number | null {
378
- const catalogModel = getCatalogModelForRuntime(runtime.id, modelId);
379
- if (typeof catalogModel?.contextWindow === "number" && catalogModel.contextWindow > 0) {
380
- return catalogModel.contextWindow;
381
- }
382
- if (runtimeKnownModelsFor(runtime.id).includes(modelId) && runtime.defaultCapabilities.contextWindow > 0) {
383
- return runtime.defaultCapabilities.contextWindow;
384
- }
385
- return null;
386
- }
387
-
388
- /**
389
- * Check the resolved model against the catalog where there is one and against
390
- * the target's own list where there is not. A runtime with no catalog used to
391
- * pass any string here while the same command had a live list that ruled it
392
- * out, so the target saved as `ok` and failed on its first
393
- * turn. Without a catalog or a live list there is nothing to check against, and
394
- * the note says so rather than implying the id was verified.
395
- */
396
- function validateResolvedModel(
397
- runtime: RuntimeDescriptor,
398
- target: Pick<TargetDescriptor, "id" | "url">,
399
- modelId: string | undefined,
400
- force: boolean,
401
- inventory: WireModelInventory,
402
- ): boolean {
403
- if (!modelId) return true;
404
- const knownModels = runtimeKnownModelsFor(runtime.id);
405
- const live = inventory.source === "probe" ? liveInventoryFor(target, inventory) : undefined;
406
- const validation = validateModelChoice({ runtimeId: runtime.id, modelId, knownModels, live, force });
407
- if (!validation.ok) {
408
- process.stderr.write(`error: ${validation.reason}\n`);
409
- return false;
410
- }
411
- if (validation.warning) process.stderr.write(`warning: ${validation.warning}\n`);
412
- if (knownModels.length === 0 && live === undefined && runtime.kind === "http") {
413
- process.stderr.write(
414
- `warning: could not verify model '${modelId}': ${target.url ?? `target '${target.id}'`} returned no model list; it is saved as written\n`,
415
- );
416
- }
417
- return true;
418
- }
419
-
420
- function validateContextWindowOverride(
421
- runtime: RuntimeDescriptor,
422
- modelId: string | undefined,
423
- contextWindow: number | undefined,
424
- force: boolean,
425
- ): boolean {
426
- if (contextWindow === undefined || !modelId) return true;
427
- const catalogMax = catalogContextWindowFor(runtime, modelId);
428
- if (catalogMax === null || contextWindow <= catalogMax) return true;
429
- const message = `--context-window ${contextWindow} exceeds catalog max ${catalogMax} for model '${modelId}'`;
430
- if (!force) {
431
- process.stderr.write(`error: ${message}. Use --force to override.\n`);
432
- return false;
433
- }
434
- process.stderr.write(`warning: ${message}\n`);
435
- return true;
436
- }
437
-
438
- async function runtimeProbe(
439
- runtime: RuntimeDescriptor,
440
- target: TargetDescriptor,
441
- authToken?: string,
442
- ): Promise<ProbeResult | null> {
443
- if (typeof runtime.probe !== "function") return null;
444
- try {
445
- const context = await buildProbeContext(runtime, target);
446
- return await runtime.probe(target, authToken === undefined ? context : { ...context, authToken });
447
- } catch (err) {
448
- return { ok: false, error: err instanceof Error ? err.message : String(err) };
449
- }
450
- }
451
-
452
- async function runtimeProbeModels(
453
- runtime: RuntimeDescriptor,
454
- target: TargetDescriptor,
455
- authToken?: string,
456
- ): Promise<string[]> {
457
- if (typeof runtime.probeModels !== "function") return [];
458
- try {
459
- const context = await buildProbeContext(runtime, target);
460
- return await runtime.probeModels(target, authToken === undefined ? context : { ...context, authToken });
461
- } catch {
462
- return [];
463
- }
464
- }
465
-
466
- function applyTarget(settings: ClioSettings, descriptor: TargetDescriptor): void {
467
- const idx = settings.targets.findIndex((e) => e.id === descriptor.id);
468
- if (idx >= 0) settings.targets[idx] = descriptor;
469
- else settings.targets.push(descriptor);
470
- }
471
-
472
- function setOrchestratorPointer(settings: ClioSettings, descriptor: TargetDescriptor, model?: string | null): void {
473
- const runtime = getRuntimeRegistry().get(descriptor.runtime);
474
- if (!runtime) {
475
- throw new Error(
476
- `cannot use target '${descriptor.id}' as orchestrator target because runtime '${descriptor.runtime}' is not registered`,
477
- );
478
- }
479
- if (!isOrchestratorEligibleRuntime(runtime)) {
480
- throw new Error(
481
- `cannot use target '${descriptor.id}' as orchestrator target because runtime '${runtime.id}' is not an HTTP/native runtime`,
482
- );
483
- }
484
- settings.chat.target = descriptor.id;
485
- settings.chat.model = model ?? descriptor.defaultModel ?? null;
486
- }
487
-
488
- function setWorkerDefaultPointer(settings: ClioSettings, descriptor: TargetDescriptor, model?: string | null): void {
489
- settings.fleet.default.target = descriptor.id;
490
- settings.fleet.default.model = model ?? descriptor.defaultModel ?? null;
491
- }
492
-
493
- function setBackgroundPointer(settings: ClioSettings, descriptor: TargetDescriptor, model?: string | null): void {
494
- const runtime = getRuntimeRegistry().get(descriptor.runtime);
495
- if (!runtime || !isOrchestratorEligibleRuntime(runtime)) {
496
- throw new Error(
497
- `cannot use target '${descriptor.id}' as background target because runtime '${descriptor.runtime}' is not an HTTP/native runtime`,
498
- );
499
- }
500
- settings.context.memory.target = descriptor.id;
501
- settings.context.memory.model = model ?? descriptor.defaultModel ?? null;
502
- }
503
-
504
- function assertOrchestratorReplacementEligible(settings: ClioSettings, descriptor: TargetDescriptor): void {
505
- if (settings.chat.target !== descriptor.id && settings.context.memory.target !== descriptor.id) return;
506
- const role = settings.chat.target === descriptor.id ? "orchestrator" : "background";
507
- const runtime = getRuntimeRegistry().get(descriptor.runtime);
508
- if (!runtime) {
509
- throw new Error(
510
- `cannot update ${role} target '${descriptor.id}' because runtime '${descriptor.runtime}' is not registered`,
511
- );
512
- }
513
- if (!isOrchestratorEligibleRuntime(runtime)) {
514
- throw new Error(`cannot update ${role} target '${descriptor.id}' to non-HTTP/native runtime '${runtime.id}'`);
515
- }
516
- }
517
-
518
- function setWorkerProfilePointer(
519
- settings: ClioSettings,
520
- name: string,
521
- descriptor: TargetDescriptor,
522
- model?: string | null,
523
- ): void {
524
- const trimmed = name.trim();
525
- if (trimmed.length === 0) throw new Error("fleet profile name must be non-empty");
526
- settings.fleet.profiles[trimmed] = {
527
- target: descriptor.id,
528
- model: model ?? descriptor.defaultModel ?? null,
529
- thinkingLevel: "off",
530
- };
531
- }
532
-
533
- /**
534
- * A target that was probed successfully and still reported no context window
535
- * will run against the runtime descriptor's placeholder. Say so at the moment
536
- * the target is written, because the alternative is a plausible-looking number
537
- * the operator never chose and the server never claimed.
538
- */
539
- function warnUndiscoveredContextWindow(descriptor: TargetDescriptor, probe: ProbeResult | null): void {
540
- if (!probe?.ok) return;
541
- if (typeof descriptor.capabilities?.contextWindow === "number" && descriptor.capabilities.contextWindow > 0) return;
542
- const discovered = probeCapabilitiesForModel(
543
- {
544
- target: descriptor,
545
- probeCapabilities: probe.discoveredCapabilities ?? null,
546
- probeModelCapabilities: probe.modelCapabilities ?? null,
547
- probeModelId: probe.capabilityModelId ?? null,
548
- },
549
- descriptor.defaultModel,
550
- );
551
- if (typeof discovered?.contextWindow === "number" && discovered.contextWindow > 0) return;
552
- process.stdout.write(
553
- " warning: the target reported no context window; Clio will use the runtime default as a guess. Set one with --context-window.\n",
554
- );
555
- }
556
-
557
- /**
558
- * What the probe read, as opposed to whether it connected.
559
- *
560
- * A health check can pass while every enrichment read behind it returns
561
- * nothing, and reporting that as `probe ok` makes it indistinguishable from a
562
- * full read. That is how a target gets blessed at configure time and then
563
- * fails on the first turn. Naming the gap is the difference between a user who
564
- * knows to look now and one who finds out from a raw 404 later.
565
- */
566
- function probeReadings(probe: ProbeResult): string[] {
567
- const readings: string[] = [];
568
- if (probe.models && probe.models.length > 0) readings.push(`${probe.models.length} models`);
569
- if (probe.serverVersion) readings.push(probe.serverVersion);
570
- if (probe.discoveredCapabilities && Object.keys(probe.discoveredCapabilities).length > 0) {
571
- readings.push("capabilities");
572
- }
573
- return readings;
574
- }
575
-
576
- /** The probe result as the lines a user reads, cause and next step included. */
577
- function probeLines(descriptor: TargetDescriptor, probe: ProbeResult): string[] {
578
- const latency = probe.latencyMs !== undefined ? ` (${probe.latencyMs}ms)` : "";
579
- if (!probe.ok) {
580
- const where = descriptor.url ? ` ${descriptor.url}` : "";
581
- return [
582
- `probe failed${latency}:${where} ${probe.error ?? "unknown"}`,
583
- ` check the server is running and reachable, then re-run: clio-coder configure --id ${descriptor.id} --url <url>`,
584
- ];
585
- }
586
- const readings = probeReadings(probe);
587
- if (readings.length > 0) return [`probe ok${latency} ${readings.join(", ")}`];
588
- return [
589
- `probe reachable${latency}, but the target answered no model list and no version`,
590
- " Clio cannot verify the model id or the context window from here and will send both as written.",
591
- " re-read them once the server serves them with: clio-coder targets --probe",
592
- ];
593
- }
594
-
595
354
  function printSummary(settings: ClioSettings, descriptor: TargetDescriptor, probe: ProbeResult | null): void {
596
355
  process.stdout.write(`\nsaved target ${descriptor.id} (runtime=${descriptor.runtime})\n`);
597
356
  if (descriptor.url) process.stdout.write(` url ${descriptor.url}\n`);
@@ -612,166 +371,6 @@ function printSummary(settings: ClioSettings, descriptor: TargetDescriptor, prob
612
371
  process.stdout.write(`\nsettings written to ${settingsPath()}\n`);
613
372
  }
614
373
 
615
- function buildDescriptor(
616
- runtime: RuntimeDescriptor,
617
- id: string,
618
- parts: {
619
- url?: string;
620
- model?: string;
621
- apiKeyEnv?: string;
622
- apiKeyRef?: string;
623
- oauthProfile?: string;
624
- gateway?: boolean;
625
- lifecycle?: TargetDescriptor["lifecycle"];
626
- contextWindow?: number;
627
- maxTokens?: number;
628
- reasoning?: boolean;
629
- lmstudio?: TargetDescriptor["lmstudio"];
630
- },
631
- ): TargetDescriptor {
632
- const descriptor: TargetDescriptor = { id, runtime: runtime.id };
633
- if (parts.url) descriptor.url = parts.url;
634
- if (parts.model) descriptor.defaultModel = parts.model;
635
- const auth: NonNullable<TargetDescriptor["auth"]> = {};
636
- if (parts.apiKeyEnv) auth.apiKeyEnvVar = parts.apiKeyEnv;
637
- if (parts.apiKeyRef) auth.apiKeyRef = parts.apiKeyRef;
638
- if (parts.oauthProfile) auth.oauthProfile = parts.oauthProfile;
639
- if (Object.keys(auth).length > 0) descriptor.auth = auth;
640
- if (parts.gateway) descriptor.gateway = true;
641
- if (parts.lifecycle) descriptor.lifecycle = parts.lifecycle;
642
- const caps: NonNullable<TargetDescriptor["capabilities"]> = {};
643
- if (parts.contextWindow !== undefined) caps.contextWindow = parts.contextWindow;
644
- if (parts.maxTokens !== undefined) caps.maxTokens = parts.maxTokens;
645
- if (parts.reasoning !== undefined) caps.reasoning = parts.reasoning;
646
- if (Object.keys(caps).length > 0) descriptor.capabilities = caps;
647
- if (parts.lmstudio) descriptor.lmstudio = parts.lmstudio;
648
- return descriptor;
649
- }
650
-
651
- function describeAuthStatus(runtime: RuntimeDescriptor): string {
652
- const status = openAuthStorage().statusForTarget(resolveRuntimeAuthTarget(runtime), { includeFallback: false });
653
- // This describes a credential, not a network state. It is printed before the
654
- // wizard has asked for a URL, so `not connected` read as a connection that
655
- // had already been tried and failed.
656
- if (!status.available) return "none stored";
657
- if (status.source === "environment") return `environment${status.detail ? ` (${status.detail})` : ""}`;
658
- if (status.source === "stored-api-key") return "stored api key";
659
- if (status.source === "stored-oauth") return "stored oauth";
660
- return status.source;
661
- }
662
-
663
- async function loginOAuthRuntime(rl: ReturnType<typeof createInterface>, runtime: RuntimeDescriptor): Promise<boolean> {
664
- const auth = openAuthStorage();
665
- if (runtime.authNotice) process.stdout.write(`note: ${runtime.authNotice}\n`);
666
- const manualCodeInput = createDelayedManualCodeInput(
667
- rl,
668
- "Paste verification code if browser callback does not complete automatically: ",
669
- );
670
- try {
671
- await auth.login(runtime.oauthProviderId ?? runtime.id, {
672
- onAuth: ({ url, instructions }) => {
673
- process.stdout.write(`\nOpen: ${url}\n`);
674
- if (instructions) process.stdout.write(`${instructions}\n`);
675
- process.stdout.write("Waiting for the browser callback. A manual code prompt will appear if needed.\n");
676
- },
677
- onDeviceCode: ({ verificationUri, userCode }) => {
678
- process.stdout.write(`\nOpen: ${verificationUri}\n`);
679
- process.stdout.write(`Enter code: ${userCode}\n`);
680
- },
681
- onPrompt: async (prompt) => {
682
- const answer = await rl.question(`${prompt.message}${prompt.allowEmpty ? " " : ": "}`);
683
- return prompt.allowEmpty ? answer : answer.trim();
684
- },
685
- onSelect: (prompt) => promptOAuthSelection(rl, prompt),
686
- onManualCodeInput: manualCodeInput.onManualCodeInput,
687
- onProgress: (message) => {
688
- process.stderr.write(`${message}\n`);
689
- },
690
- });
691
- if (credentialWriteFailed(auth, `credential for ${runtime.id} was not stored`)) return false;
692
- printOk(`authenticated ${runtime.id}`);
693
- return true;
694
- } catch (error) {
695
- printError(error instanceof Error ? error.message : String(error));
696
- return false;
697
- } finally {
698
- manualCodeInput.cancel();
699
- }
700
- }
701
-
702
- /**
703
- * The model ids a target can be configured with, and where they came from.
704
- * `probe` is the only source that says anything about the server in front of
705
- * us right now; `catalog` is static provider knowledge, and `existing` is the
706
- * list a previous configure recorded, which the server may no longer match.
707
- */
708
- interface WireModelInventory {
709
- models: string[];
710
- source: "catalog" | "probe" | "cache" | "legacy" | "none";
711
- /** Per-model load state when the probe reported it. */
712
- modelStates?: Readonly<Record<string, ProbeModelStatus>> | undefined;
713
- }
714
-
715
- /**
716
- * Every id the runtime would resolve. LM Studio lists a loaded model under its
717
- * instance id and keeps the model key only in the state map, and the request
718
- * path accepts either, so both count as advertised.
719
- */
720
- function advertisedModelIds(inventory: Pick<WireModelInventory, "models" | "modelStates">): string[] {
721
- const ids = [...inventory.models];
722
- for (const id of Object.keys(inventory.modelStates ?? {})) if (!ids.includes(id)) ids.push(id);
723
- return ids;
724
- }
725
-
726
- function residentModelIds(modelStates: WireModelInventory["modelStates"]): string[] {
727
- return Object.entries(modelStates ?? {})
728
- .filter(([, status]) => status.state === "loaded" || status.state === "loading")
729
- .map(([id]) => id);
730
- }
731
-
732
- function liveInventoryFor(
733
- target: Pick<TargetDescriptor, "id" | "url">,
734
- inventory: WireModelInventory,
735
- ): LiveModelInventory {
736
- return {
737
- targetId: target.id,
738
- url: target.url,
739
- models: advertisedModelIds(inventory),
740
- resident: residentModelIds(inventory.modelStates),
741
- };
742
- }
743
-
744
- async function resolveSupportedWireModels(
745
- runtime: RuntimeDescriptor,
746
- target: TargetDescriptor,
747
- existing?: TargetDescriptor,
748
- authToken?: string,
749
- ): Promise<WireModelInventory> {
750
- const known = listKnownModelsForRuntime(runtime.id);
751
- if (known.length > 0) return { models: known, source: "catalog" };
752
- if (runtime.kind === "http") {
753
- // The full probe carries load state alongside the ids; a runtime that
754
- // lists models only through probeModels still gets its ids checked.
755
- const probe = await runtimeProbe(runtime, target, authToken);
756
- if (probe?.ok && probe.models && probe.models.length > 0) {
757
- recordTargetModelSnapshot(target, probe.models);
758
- return { models: [...probe.models], source: "probe", modelStates: probe.modelStates };
759
- }
760
- const discovered = await runtimeProbeModels(runtime, target, authToken);
761
- if (discovered.length > 0) {
762
- recordTargetModelSnapshot(target, discovered);
763
- return { models: discovered, source: "probe" };
764
- }
765
- }
766
- const cached = readTargetModelSnapshot(target);
767
- if (cached && cached.models.length > 0) return { models: [...cached.models], source: "cache" };
768
- if (existing?.wireModels && existing.wireModels.length > 0) {
769
- recordTargetModelSnapshot(target, existing.wireModels);
770
- return { models: [...existing.wireModels], source: "legacy" };
771
- }
772
- return { models: [], source: "none" };
773
- }
774
-
775
374
  function resolveModelChoice(
776
375
  answer: string,
777
376
  wireModels: ReadonlyArray<string>,
@@ -819,6 +418,10 @@ async function runNonInteractive(runtime: RuntimeDescriptor, args: ParsedArgs):
819
418
  printError("--agent-profile-model requires --agent-profile");
820
419
  return 2;
821
420
  }
421
+ if (args.bindAgent !== undefined && args.workerProfile === undefined) {
422
+ printError("--bind-agent requires --agent-profile");
423
+ return 2;
424
+ }
822
425
  if (args.workerProfile !== undefined && args.workerProfile.trim().length === 0) {
823
426
  printError("--agent-profile must be non-empty");
824
427
  return 2;
@@ -889,13 +492,14 @@ async function runNonInteractive(runtime: RuntimeDescriptor, args: ParsedArgs):
889
492
  ...(args.maxTokens !== undefined ? { maxTokens: args.maxTokens } : {}),
890
493
  ...(args.reasoning !== undefined ? { reasoning: args.reasoning } : {}),
891
494
  ...(existing?.lmstudio ? { lmstudio: existing.lmstudio } : {}),
495
+ ...(existing?.litellm ? { litellm: existing.litellm } : {}),
892
496
  });
893
497
  const inventory = await resolveSupportedWireModels(runtime, seed, existing, args.apiKey);
894
498
  const wireModels = inventory.models;
895
499
  const model =
896
500
  args.model ??
897
501
  existing?.defaultModel ??
898
- support.defaultModel ??
502
+ (inventory.source === "probe" ? wireModels[0] : support.defaultModel) ??
899
503
  (support.modelSource === "catalog" ? undefined : wireModels[0]);
900
504
  if (model === undefined && support.modelSource === "catalog") {
901
505
  refuseCatalogSeededModel(runtime, support);
@@ -919,6 +523,7 @@ async function runNonInteractive(runtime: RuntimeDescriptor, args: ParsedArgs):
919
523
  ...(args.maxTokens !== undefined ? { maxTokens: args.maxTokens } : {}),
920
524
  ...(args.reasoning !== undefined ? { reasoning: args.reasoning } : {}),
921
525
  ...(existing?.lmstudio ? { lmstudio: existing.lmstudio } : {}),
526
+ ...(existing?.litellm ? { litellm: existing.litellm } : {}),
922
527
  });
923
528
  const setOrchestrator = args.setOrchestrator || args.orchestratorModel !== undefined;
924
529
  const setBackground = args.setBackground || args.backgroundModel !== undefined;
@@ -958,6 +563,7 @@ async function runNonInteractive(runtime: RuntimeDescriptor, args: ParsedArgs):
958
563
  descriptor,
959
564
  args.workerProfileModel ?? descriptor.defaultModel ?? null,
960
565
  );
566
+ if (args.bindAgent !== undefined) bindAgentProfileInSettings(target, args.bindAgent, args.workerProfile);
961
567
  }
962
568
  };
963
569
  // Apply to the local snapshot for the summary below, then persist the same
@@ -966,7 +572,9 @@ async function runNonInteractive(runtime: RuntimeDescriptor, args: ParsedArgs):
966
572
  applyConfiguration(settings);
967
573
  updateSettings(applyConfiguration);
968
574
  const probe = await runtimeProbe(runtime, descriptor);
969
- if (probe?.ok && probe.models) recordTargetModelSnapshot(descriptor, probe.models);
575
+ if (probe?.ok && probe.models) {
576
+ recordTargetModelSnapshot(descriptor, probe.models, probe.modelLabels ? { modelLabels: probe.modelLabels } : {});
577
+ }
970
578
  printSummary(settings, descriptor, probe);
971
579
  if (
972
580
  runtime.auth === "oauth" &&
@@ -1037,45 +645,6 @@ async function pickRuntimeFromEntries(
1037
645
  }
1038
646
  }
1039
647
 
1040
- const LOCAL_APP_RUNTIME_IDS: ReadonlySet<string> = new Set(["ollama-native", "lmstudio"]);
1041
-
1042
- // Generic protocol-compatible runtimes. They are classified local-http (they
1043
- // carry a probe and no cloud catalog entry), but a hosted endpoint such as
1044
- // Inception/Mercury is exactly an OpenAI-compatible cloud API, so the wizard
1045
- // also surfaces them under the Cloud API path.
1046
- const PROTOCOL_COMPAT_RUNTIME_IDS: ReadonlySet<string> = new Set(["openai-compat", "anthropic-compat"]);
1047
-
1048
- function runtimesForCategory(
1049
- entries: ReadonlyArray<ProviderSupportEntry>,
1050
- category: ConfigureCategory,
1051
- ): ProviderSupportEntry[] {
1052
- switch (category) {
1053
- case "local-app":
1054
- return entries.filter((entry) => LOCAL_APP_RUNTIME_IDS.has(entry.runtimeId));
1055
- case "local-http":
1056
- return entries.filter((entry) => entry.group === "local-http" && !LOCAL_APP_RUNTIME_IDS.has(entry.runtimeId));
1057
- case "chatgpt":
1058
- return entries.filter((entry) => entry.runtimeId === "openai-codex");
1059
- case "cloud-api": {
1060
- const byLabel = (a: ProviderSupportEntry, b: ProviderSupportEntry) =>
1061
- a.label.localeCompare(b.label) || a.runtimeId.localeCompare(b.runtimeId);
1062
- const named = entries
1063
- .filter((entry) => entry.group === "cloud-api")
1064
- .slice()
1065
- .sort(byLabel);
1066
- // Surface the generic OpenAI/Anthropic-compatible runtimes here too,
1067
- // shown last and under the cloud heading rather than their native group.
1068
- const compat = entries
1069
- .filter((entry) => PROTOCOL_COMPAT_RUNTIME_IDS.has(entry.runtimeId))
1070
- .map((entry) => ({ ...entry, group: "cloud-api" as const }))
1071
- .sort(byLabel);
1072
- return [...named, ...compat];
1073
- }
1074
- case "all":
1075
- return entries.slice();
1076
- }
1077
- }
1078
-
1079
648
  async function pickCategory(rl: ReturnType<typeof createInterface>): Promise<ConfigureCategory | null> {
1080
649
  process.stdout.write("\nHow will you connect Clio to a model?\n\n");
1081
650
  for (const line of formatCategoryMenu(terminalColumns())) process.stdout.write(`${line}\n`);
@@ -1143,16 +712,11 @@ async function maybeSteerToNativeRuntime(
1143
712
  return native;
1144
713
  }
1145
714
 
1146
- async function runInteractive(
715
+ async function runTargetSetupInteractive(
1147
716
  rl: ReturnType<typeof createInterface>,
1148
- preselectedRuntime: RuntimeDescriptor | null,
717
+ runtime: RuntimeDescriptor,
1149
718
  defaults: ParsedArgs,
1150
719
  ): Promise<number> {
1151
- let runtime = preselectedRuntime ?? (await pickRuntimeViaCategory(rl));
1152
- if (!runtime) {
1153
- printError("configuration cancelled");
1154
- return 130;
1155
- }
1156
720
  const auth = openAuthStorage();
1157
721
  const settings = readSettings();
1158
722
  let support = buildProviderSupportEntry(runtime);
@@ -1289,9 +853,10 @@ async function runInteractive(
1289
853
  ...(defaults.maxTokens !== undefined ? { maxTokens: defaults.maxTokens } : {}),
1290
854
  ...(defaults.reasoning !== undefined ? { reasoning: defaults.reasoning } : {}),
1291
855
  ...(existing?.lmstudio ? { lmstudio: existing.lmstudio } : {}),
856
+ ...(existing?.litellm ? { litellm: existing.litellm } : {}),
1292
857
  });
1293
858
 
1294
- if (runtime.kind === "http") {
859
+ if (runtime.kind === "http" || runtime.externalAgentLoop?.modelCatalog === "live-authoritative") {
1295
860
  inventory = await resolveSupportedWireModels(runtime, tentative, existing ?? undefined);
1296
861
  }
1297
862
  const wireModels = inventory.models;
@@ -1299,12 +864,21 @@ async function runInteractive(
1299
864
  // --model here the way the non-interactive path does. It does need to stop
1300
865
  // offering the alphabetically first id as though it were the recommended one.
1301
866
  const catalogOrdered = support.modelSource === "catalog";
1302
- model = model ?? existing?.defaultModel ?? support.defaultModel ?? (catalogOrdered ? undefined : wireModels[0]);
867
+ model =
868
+ model ??
869
+ existing?.defaultModel ??
870
+ (inventory.source === "probe" ? wireModels[0] : support.defaultModel) ??
871
+ (catalogOrdered ? undefined : wireModels[0]);
1303
872
  if (wireModels.length > 0) {
1304
873
  process.stdout.write("\nSelectable models:\n");
1305
874
  for (const [index, wireModel] of wireModels.entries()) {
1306
- process.stdout.write(` ${index + 1}. ${wireModel}${wireModel === model ? " [default]" : ""}\n`);
875
+ const label = inventory.labels?.[wireModel];
876
+ process.stdout.write(
877
+ ` ${index + 1}. ${wireModel}${label && label !== wireModel ? ` — ${label}` : ""}${wireModel === model ? " [default]" : ""}\n`,
878
+ );
1307
879
  }
880
+ if (inventory.source === "cache") process.stdout.write(" cached model snapshot (not verified in this run)\n");
881
+ if (inventory.probeError) process.stdout.write(` live probe unavailable: ${inventory.probeError}\n`);
1308
882
  if (!model && catalogOrdered) {
1309
883
  process.stdout.write(` listed in provider catalog order, which recommends none of them; pick one.\n`);
1310
884
  }
@@ -1375,6 +949,7 @@ async function runInteractive(
1375
949
  ...(defaults.maxTokens !== undefined ? { maxTokens: defaults.maxTokens } : {}),
1376
950
  ...(reasoningChoice !== undefined ? { reasoning: reasoningChoice } : {}),
1377
951
  ...(existing?.lmstudio ? { lmstudio: existing.lmstudio } : {}),
952
+ ...(existing?.litellm ? { litellm: existing.litellm } : {}),
1378
953
  });
1379
954
  try {
1380
955
  assertOrchestratorReplacementEligible(settings, descriptor);
@@ -1460,7 +1035,7 @@ async function runInteractive(
1460
1035
 
1461
1036
  printSummary(settings, descriptor, probe);
1462
1037
  printOk(`target ${targetId} saved`);
1463
- await runInteropReview(rl);
1038
+ await runInteropReview({ rl });
1464
1039
  return 0;
1465
1040
  }
1466
1041
 
@@ -1510,7 +1085,980 @@ export function runTargetRename(oldId: string, newId: string): number {
1510
1085
  return 0;
1511
1086
  }
1512
1087
 
1513
- export async function runConfigureCommand(argv: ReadonlyArray<string>): Promise<number> {
1088
+ /**
1089
+ * The runtime configuration screens.
1090
+ *
1091
+ * Every screen is one `SectionSpec`: a title, the values it shows, and the
1092
+ * actions that change them. Rendering goes through the lifecycle presenter, so
1093
+ * `configure` degrades on a pipe and under NO_COLOR exactly as `reset`,
1094
+ * `upgrade`, and `uninstall` do. Navigation goes through `pickEntry`, which
1095
+ * uses the arrow-key selector on a terminal and the numbered readline prompt
1096
+ * everywhere else, so Escape leaves a screen without the screen having to teach
1097
+ * a token for it, and a scripted run still has a way to answer.
1098
+ */
1099
+
1100
+ interface SectionIo {
1101
+ rl: ReturnType<typeof createInterface>;
1102
+ out: NodeJS.WritableStream;
1103
+ /** Both ends of this run, for an action that opens a picker of its own. */
1104
+ streams: ConfigureStreams;
1105
+ /** Confirm one change on the transcript, in the presenter's voice. */
1106
+ ok: (text: string) => void;
1107
+ }
1108
+
1109
+ interface SectionAction {
1110
+ label: string;
1111
+ hint?: string;
1112
+ run: (io: SectionIo) => Promise<void>;
1113
+ }
1114
+
1115
+ interface SectionSpec {
1116
+ /** Canonical `--section` name. */
1117
+ id: string;
1118
+ title: string;
1119
+ /** What the main menu says this screen covers. */
1120
+ summary: string;
1121
+ /** Other spellings `--section` accepts. */
1122
+ aliases?: ReadonlyArray<string>;
1123
+ fields: () => Array<readonly [string, string]>;
1124
+ actions: ReadonlyArray<SectionAction>;
1125
+ }
1126
+
1127
+ const NONE = "(none)";
1128
+ const onOff = (value: boolean): string => (value ? "enabled" : "disabled");
1129
+ const listOr = (values: ReadonlyArray<string>, fallback = NONE): string =>
1130
+ values.length > 0 ? values.join(", ") : fallback;
1131
+
1132
+ /** Read a bounded integer, leaving the setting alone when the answer is not one. */
1133
+ async function askInteger(
1134
+ io: SectionIo,
1135
+ label: string,
1136
+ current: number,
1137
+ apply: (value: number) => void,
1138
+ min = 0,
1139
+ ): Promise<void> {
1140
+ const answer = await ask(io.rl, label, String(current));
1141
+ if (answer === null) return;
1142
+ const parsed = Number(answer);
1143
+ if (!Number.isFinite(parsed) || parsed < min) {
1144
+ io.out.write(` ${label} must be a number of at least ${min}; left at ${current}\n`);
1145
+ return;
1146
+ }
1147
+ const value = Math.floor(parsed);
1148
+ apply(value);
1149
+ io.ok(`${label} set to ${value}`);
1150
+ }
1151
+
1152
+ /** Read one of a fixed set of words, leaving the setting alone otherwise. */
1153
+ async function askChoice(
1154
+ io: SectionIo,
1155
+ label: string,
1156
+ allowed: ReadonlyArray<string>,
1157
+ current: string,
1158
+ apply: (value: string) => void,
1159
+ ): Promise<void> {
1160
+ const answer = await ask(io.rl, `${label} [${allowed.join("|")}]`, current);
1161
+ if (answer === null) return;
1162
+ const value = answer.trim().toLowerCase();
1163
+ if (!allowed.includes(value)) {
1164
+ io.out.write(` ${label} must be one of ${allowed.join(", ")}; left at ${current}\n`);
1165
+ return;
1166
+ }
1167
+ apply(value);
1168
+ io.ok(`${label} set to ${value}`);
1169
+ }
1170
+
1171
+ const SECTIONS: ReadonlyArray<SectionSpec> = [
1172
+ {
1173
+ id: "targets",
1174
+ title: "Targets & Auth",
1175
+ summary: "providers, endpoints, credentials, models",
1176
+ aliases: ["auth", "target", "providers"],
1177
+ fields: () => {
1178
+ const settings = readSettings();
1179
+ const rows: Array<readonly [string, string]> = [
1180
+ ["Chat target", `${settings.chat.target ?? NONE} (model: ${settings.chat.model ?? "target default"})`],
1181
+ [
1182
+ "Fleet target",
1183
+ `${settings.fleet.default.target ?? NONE} (model: ${settings.fleet.default.model ?? "target default"})`,
1184
+ ],
1185
+ ];
1186
+ if (settings.targets.length === 0) rows.push(["Registered", "(no targets registered)"]);
1187
+ else
1188
+ for (const target of settings.targets) {
1189
+ rows.push([
1190
+ ` ${target.id}`,
1191
+ `${target.runtime}, model ${target.defaultModel ?? "unset"}${
1192
+ settings.chat.target === target.id ? " [chat]" : ""
1193
+ }${settings.fleet.default.target === target.id ? " [fleet]" : ""}`,
1194
+ ]);
1195
+ }
1196
+ return rows;
1197
+ },
1198
+ actions: [
1199
+ {
1200
+ label: "Add a target",
1201
+ hint: "register a provider endpoint",
1202
+ run: async (io) => {
1203
+ const runtime = await pickRuntimeViaCategory(io.rl);
1204
+ if (runtime) await runTargetSetupInteractive(io.rl, runtime, emptyArgs());
1205
+ },
1206
+ },
1207
+ {
1208
+ label: "Set the chat target",
1209
+ hint: "which target answers in chat",
1210
+ run: async (io) => {
1211
+ await assignTarget(io, "chat");
1212
+ },
1213
+ },
1214
+ {
1215
+ label: "Set the fleet default target",
1216
+ hint: "which target dispatched workers use",
1217
+ run: async (io) => {
1218
+ await assignTarget(io, "fleet");
1219
+ },
1220
+ },
1221
+ {
1222
+ label: "Remove a target",
1223
+ run: async (io) => {
1224
+ const settings = readSettings();
1225
+ const ids = settings.targets.map((target) => target.id);
1226
+ if (ids.length === 0) {
1227
+ io.out.write(" No targets to remove.\n");
1228
+ return;
1229
+ }
1230
+ io.out.write(` Registered: ${ids.join(", ")}\n`);
1231
+ const chosen = await ask(io.rl, "Target id to remove");
1232
+ if (chosen === null || !ids.includes(chosen)) return;
1233
+ if (await askYesNo(io.rl, `Remove target '${chosen}'?`, false)) {
1234
+ runTargetRemove(chosen);
1235
+ }
1236
+ },
1237
+ },
1238
+ ],
1239
+ },
1240
+ {
1241
+ id: "models",
1242
+ title: "Models & Thinking",
1243
+ summary: "default model, thinking level, favorites",
1244
+ aliases: ["model", "thinking"],
1245
+ fields: () => {
1246
+ const settings = readSettings();
1247
+ return [
1248
+ ["Chat model", settings.chat.model ?? "(target default)"],
1249
+ ["Chat thinking", settings.chat.thinkingLevel],
1250
+ ["Fleet thinking", settings.fleet.default.thinkingLevel],
1251
+ ["Favorites", listOr(settings.chat.modelPicker.favorites)],
1252
+ ["Cycle set (Alt+J/K)", listOr(settings.chat.modelPicker.cycleSet)],
1253
+ ];
1254
+ },
1255
+ actions: [
1256
+ {
1257
+ label: "Chat thinking level",
1258
+ hint: THINKING_LEVELS.join(" | "),
1259
+ run: async (io) => {
1260
+ await askChoice(io, "Chat thinking level", THINKING_LEVELS, readSettings().chat.thinkingLevel, (value) => {
1261
+ updateSettings((draft) => {
1262
+ draft.chat.thinkingLevel = value as ThinkingLevel;
1263
+ });
1264
+ });
1265
+ },
1266
+ },
1267
+ {
1268
+ label: "Fleet thinking level",
1269
+ hint: THINKING_LEVELS.join(" | "),
1270
+ run: async (io) => {
1271
+ await askChoice(
1272
+ io,
1273
+ "Fleet thinking level",
1274
+ THINKING_LEVELS,
1275
+ readSettings().fleet.default.thinkingLevel,
1276
+ (value) => {
1277
+ updateSettings((draft) => {
1278
+ draft.fleet.default.thinkingLevel = value as ThinkingLevel;
1279
+ });
1280
+ },
1281
+ );
1282
+ },
1283
+ },
1284
+ {
1285
+ label: "Chat default model",
1286
+ hint: "blank clears the override",
1287
+ run: async (io) => {
1288
+ const model = await ask(
1289
+ io.rl,
1290
+ "Chat default model (blank for the target default)",
1291
+ readSettings().chat.model ?? "",
1292
+ );
1293
+ if (model === null) return;
1294
+ updateSettings((draft) => {
1295
+ draft.chat.model = model.length > 0 ? model : null;
1296
+ });
1297
+ io.ok(model.length > 0 ? `Chat default model set to ${model}` : "Chat default model cleared");
1298
+ },
1299
+ },
1300
+ {
1301
+ label: "Model favorites",
1302
+ hint: "comma-separated",
1303
+ run: async (io) => {
1304
+ await askList(io, "Model favorites", readSettings().chat.modelPicker.favorites, (values) => {
1305
+ updateSettings((draft) => {
1306
+ draft.chat.modelPicker.favorites = values;
1307
+ });
1308
+ });
1309
+ },
1310
+ },
1311
+ {
1312
+ label: "Cycle set",
1313
+ hint: "comma-separated, Alt+J/K walks it",
1314
+ run: async (io) => {
1315
+ await askList(io, "Cycle set", readSettings().chat.modelPicker.cycleSet, (values) => {
1316
+ updateSettings((draft) => {
1317
+ draft.chat.modelPicker.cycleSet = values;
1318
+ });
1319
+ });
1320
+ },
1321
+ },
1322
+ ],
1323
+ },
1324
+ {
1325
+ id: "chat",
1326
+ title: "Chat Defaults",
1327
+ summary: "streaming, progress, compaction",
1328
+ fields: () => {
1329
+ const settings = readSettings();
1330
+ return [
1331
+ ["Smooth streaming", settings.interface.smoothStreaming],
1332
+ ["Terminal progress", onOff(settings.interface.terminalProgress)],
1333
+ [
1334
+ "Max output tokens",
1335
+ settings.chat.maxOutputTokens > 0 ? String(settings.chat.maxOutputTokens) : "(runtime default)",
1336
+ ],
1337
+ ["Prompt prewarm", onOff(settings.chat.prewarm)],
1338
+ [
1339
+ "Auto-compaction",
1340
+ settings.context.compaction.auto
1341
+ ? `enabled at ${Math.round(settings.context.compaction.threshold * 100)}%`
1342
+ : "disabled",
1343
+ ],
1344
+ [
1345
+ "Working-set eviction",
1346
+ settings.context.workingSet.enabled ? `enabled (${settings.context.workingSet.policy})` : "disabled",
1347
+ ],
1348
+ ];
1349
+ },
1350
+ actions: [
1351
+ {
1352
+ label: "Smooth streaming",
1353
+ hint: "off | auto | on",
1354
+ run: async (io) => {
1355
+ await askChoice(
1356
+ io,
1357
+ "Smooth streaming",
1358
+ ["off", "auto", "on"],
1359
+ readSettings().interface.smoothStreaming,
1360
+ (value) => {
1361
+ updateSettings((draft) => {
1362
+ draft.interface.smoothStreaming = value as SmoothStreaming;
1363
+ });
1364
+ },
1365
+ );
1366
+ },
1367
+ },
1368
+ {
1369
+ label: "Terminal progress indicator",
1370
+ hint: "toggle",
1371
+ run: async (io) => {
1372
+ let next = false;
1373
+ updateSettings((draft) => {
1374
+ draft.interface.terminalProgress = !draft.interface.terminalProgress;
1375
+ next = draft.interface.terminalProgress;
1376
+ });
1377
+ io.ok(`Terminal progress ${onOff(next)}`);
1378
+ },
1379
+ },
1380
+ {
1381
+ label: "Max output tokens",
1382
+ hint: "0 for the runtime default",
1383
+ run: async (io) => {
1384
+ await askInteger(io, "Max output tokens", readSettings().chat.maxOutputTokens, (value) => {
1385
+ updateSettings((draft) => {
1386
+ draft.chat.maxOutputTokens = value;
1387
+ });
1388
+ });
1389
+ },
1390
+ },
1391
+ {
1392
+ label: "Prompt prewarm",
1393
+ hint: "toggle",
1394
+ run: async (io) => {
1395
+ let next = false;
1396
+ updateSettings((draft) => {
1397
+ draft.chat.prewarm = !draft.chat.prewarm;
1398
+ next = draft.chat.prewarm;
1399
+ });
1400
+ io.ok(`Prompt prewarm ${onOff(next)}`);
1401
+ },
1402
+ },
1403
+ {
1404
+ label: "Auto-compaction",
1405
+ hint: "toggle",
1406
+ run: async (io) => {
1407
+ let next = false;
1408
+ updateSettings((draft) => {
1409
+ draft.context.compaction.auto = !draft.context.compaction.auto;
1410
+ next = draft.context.compaction.auto;
1411
+ });
1412
+ io.ok(`Auto-compaction ${onOff(next)}`);
1413
+ },
1414
+ },
1415
+ ],
1416
+ },
1417
+ {
1418
+ id: "fleet",
1419
+ title: "Fleet",
1420
+ summary: "concurrency, retries, worker timeouts",
1421
+ fields: () => {
1422
+ const settings = readSettings();
1423
+ return [
1424
+ ["Concurrency limit", String(settings.fleet.concurrency)],
1425
+ ["Max retries", String(settings.fleet.retry.maxRetries)],
1426
+ ["Tool calls per run", String(settings.fleet.limits.toolCallsPerRun)],
1427
+ ["Run timeout", `${settings.fleet.limits.internalRunTimeoutMs / 1000}s`],
1428
+ ["Worker profiles", listOr(Object.keys(settings.fleet.profiles))],
1429
+ [
1430
+ "Subagent pins",
1431
+ listOr(Object.entries(settings.fleet.agentProfiles).map(([agent, profile]) => `${agent} -> ${profile}`)),
1432
+ ],
1433
+ ["Remote nodes", listOr(settings.fleet.nodes.map((node) => node.id))],
1434
+ ];
1435
+ },
1436
+ actions: [
1437
+ {
1438
+ label: "Concurrency limit",
1439
+ hint: "auto, or a positive integer",
1440
+ run: async (io) => {
1441
+ const current = String(readSettings().fleet.concurrency);
1442
+ const answer = await ask(io.rl, "Concurrency limit [auto or a number]", current);
1443
+ if (answer === null) return;
1444
+ if (answer.trim().toLowerCase() === "auto") {
1445
+ updateSettings((draft) => {
1446
+ draft.fleet.concurrency = "auto";
1447
+ });
1448
+ io.ok("Concurrency limit set to auto");
1449
+ return;
1450
+ }
1451
+ const parsed = Number(answer);
1452
+ if (!Number.isFinite(parsed) || parsed < 1) {
1453
+ io.out.write(` Concurrency limit must be auto or at least 1; left at ${current}\n`);
1454
+ return;
1455
+ }
1456
+ updateSettings((draft) => {
1457
+ draft.fleet.concurrency = Math.floor(parsed);
1458
+ });
1459
+ io.ok(`Concurrency limit set to ${Math.floor(parsed)}`);
1460
+ },
1461
+ },
1462
+ {
1463
+ label: "Max retries",
1464
+ run: async (io) => {
1465
+ await askInteger(io, "Max retries", readSettings().fleet.retry.maxRetries, (value) => {
1466
+ updateSettings((draft) => {
1467
+ draft.fleet.retry.maxRetries = value;
1468
+ });
1469
+ });
1470
+ },
1471
+ },
1472
+ {
1473
+ label: "Tool calls per run",
1474
+ run: async (io) => {
1475
+ await askInteger(
1476
+ io,
1477
+ "Tool calls per run",
1478
+ readSettings().fleet.limits.toolCallsPerRun,
1479
+ (value) => {
1480
+ updateSettings((draft) => {
1481
+ draft.fleet.limits.toolCallsPerRun = value;
1482
+ });
1483
+ },
1484
+ 1,
1485
+ );
1486
+ },
1487
+ },
1488
+ {
1489
+ label: "Run timeout",
1490
+ hint: "seconds",
1491
+ run: async (io) => {
1492
+ const current = readSettings().fleet.limits.internalRunTimeoutMs / 1000;
1493
+ const answer = await ask(io.rl, "Run timeout in seconds", String(current));
1494
+ if (answer === null) return;
1495
+ const parsed = Number(answer);
1496
+ if (!Number.isFinite(parsed) || parsed < 1) {
1497
+ io.out.write(` Run timeout must be at least 1 second; left at ${current}s\n`);
1498
+ return;
1499
+ }
1500
+ updateSettings((draft) => {
1501
+ draft.fleet.limits.internalRunTimeoutMs = Math.floor(parsed * 1000);
1502
+ });
1503
+ io.ok(`Run timeout set to ${Math.floor(parsed)}s`);
1504
+ },
1505
+ },
1506
+ ],
1507
+ },
1508
+ {
1509
+ id: "permissions",
1510
+ title: "Permissions & Autonomy",
1511
+ summary: "autonomy level, worker permissions, cost limits",
1512
+ aliases: ["autonomy", "safety"],
1513
+ fields: () => {
1514
+ const settings = readSettings();
1515
+ return [
1516
+ ["Autonomy level", settings.safety.autonomy],
1517
+ ["Worker permissions", settings.fleet.permissions.mode],
1518
+ ["Session cost limit", `$${settings.safety.limits.sessionCostUsd} USD`],
1519
+ ["Turn tool budget", String(settings.safety.limits.chatToolCallsPerTurn)],
1520
+ ["Review watchdog", onOff(settings.safety.review.enabled)],
1521
+ ];
1522
+ },
1523
+ actions: [
1524
+ {
1525
+ label: "Autonomy level",
1526
+ hint: "interactive | assisted | auto-edit | full",
1527
+ run: async (io) => {
1528
+ io.out.write(
1529
+ " interactive prompts for everything, assisted auto-reads and confirms writes,\n auto-edit confirms only bash, full runs unattended.\n",
1530
+ );
1531
+ await askChoice(
1532
+ io,
1533
+ "Autonomy level",
1534
+ ["interactive", "assisted", "auto-edit", "full"],
1535
+ readSettings().safety.autonomy,
1536
+ (value) => {
1537
+ updateSettings((draft) => {
1538
+ draft.safety.autonomy = value as AutonomyLevel;
1539
+ });
1540
+ },
1541
+ );
1542
+ },
1543
+ },
1544
+ {
1545
+ label: "Worker permission mode",
1546
+ hint: "deny | fail | escalate",
1547
+ run: async (io) => {
1548
+ await askChoice(
1549
+ io,
1550
+ "Worker permission mode",
1551
+ ["deny", "fail", "escalate"],
1552
+ readSettings().fleet.permissions.mode,
1553
+ (value) => {
1554
+ updateSettings((draft) => {
1555
+ draft.fleet.permissions.mode = value as WorkerPermissionMode;
1556
+ });
1557
+ },
1558
+ );
1559
+ },
1560
+ },
1561
+ {
1562
+ label: "Session cost limit",
1563
+ hint: "USD",
1564
+ run: async (io) => {
1565
+ const current = readSettings().safety.limits.sessionCostUsd;
1566
+ const answer = await ask(io.rl, "Session cost limit in USD", String(current));
1567
+ if (answer === null) return;
1568
+ const parsed = Number(answer);
1569
+ if (!Number.isFinite(parsed) || parsed <= 0) {
1570
+ io.out.write(` Session cost limit must be greater than 0; left at $${current}\n`);
1571
+ return;
1572
+ }
1573
+ updateSettings((draft) => {
1574
+ draft.safety.limits.sessionCostUsd = parsed;
1575
+ });
1576
+ io.ok(`Session cost limit set to $${parsed} USD`);
1577
+ },
1578
+ },
1579
+ {
1580
+ label: "Turn tool budget",
1581
+ run: async (io) => {
1582
+ await askInteger(
1583
+ io,
1584
+ "Turn tool budget",
1585
+ readSettings().safety.limits.chatToolCallsPerTurn,
1586
+ (value) => {
1587
+ updateSettings((draft) => {
1588
+ draft.safety.limits.chatToolCallsPerTurn = value;
1589
+ });
1590
+ },
1591
+ 1,
1592
+ );
1593
+ },
1594
+ },
1595
+ {
1596
+ label: "Turn-end review watchdog",
1597
+ hint: "toggle",
1598
+ run: async (io) => {
1599
+ let next = false;
1600
+ updateSettings((draft) => {
1601
+ draft.safety.review.enabled = !draft.safety.review.enabled;
1602
+ next = draft.safety.review.enabled;
1603
+ });
1604
+ io.ok(`Review watchdog ${onOff(next)}`);
1605
+ },
1606
+ },
1607
+ ],
1608
+ },
1609
+ {
1610
+ id: "panes",
1611
+ title: "Panes & Layout",
1612
+ summary: "terminal panes, startup layout, display mode",
1613
+ aliases: ["layout", "interface", "pane"],
1614
+ fields: () => {
1615
+ const settings = readSettings();
1616
+ return [
1617
+ ["Panes capability", settings.interface.panes.enabled],
1618
+ ["Startup layout", settings.interface.panes.layout],
1619
+ ["TUI mode", settings.interface.mode],
1620
+ ["Output detail", settings.interface.outputDetail],
1621
+ ["Desktop notifications", onOff(settings.interface.desktopNotifications)],
1622
+ ["Git commit attribution", onOff(settings.integrations.git.commitAttribution)],
1623
+ ];
1624
+ },
1625
+ actions: [
1626
+ {
1627
+ label: "Panes capability",
1628
+ hint: "off | auto | embedded",
1629
+ run: async (io) => {
1630
+ await askChoice(
1631
+ io,
1632
+ "Panes capability",
1633
+ ["off", "auto", "embedded"],
1634
+ readSettings().interface.panes.enabled,
1635
+ (value) => {
1636
+ updateSettings((draft) => {
1637
+ draft.interface.panes.enabled = value as PanesSettings["enabled"];
1638
+ });
1639
+ },
1640
+ );
1641
+ },
1642
+ },
1643
+ {
1644
+ label: "Startup layout",
1645
+ hint: "off | workers | cockpit",
1646
+ run: async (io) => {
1647
+ await askChoice(
1648
+ io,
1649
+ "Startup layout",
1650
+ ["off", "workers", "cockpit"],
1651
+ readSettings().interface.panes.layout,
1652
+ (value) => {
1653
+ updateSettings((draft) => {
1654
+ draft.interface.panes.layout = value as PanesSettings["layout"];
1655
+ });
1656
+ },
1657
+ );
1658
+ },
1659
+ },
1660
+ {
1661
+ label: "TUI mode",
1662
+ hint: "regular | fullscreen",
1663
+ run: async (io) => {
1664
+ await askChoice(io, "TUI mode", ["regular", "fullscreen"], readSettings().interface.mode, (value) => {
1665
+ updateSettings((draft) => {
1666
+ draft.interface.mode = value as TuiMode;
1667
+ });
1668
+ });
1669
+ },
1670
+ },
1671
+ {
1672
+ label: "Output detail",
1673
+ hint: "minimal | default | verbose",
1674
+ run: async (io) => {
1675
+ await askChoice(
1676
+ io,
1677
+ "Output detail",
1678
+ ["minimal", "default", "verbose"],
1679
+ readSettings().interface.outputDetail,
1680
+ (value) => {
1681
+ updateSettings((draft) => {
1682
+ draft.interface.outputDetail = value as OutputVerbosity;
1683
+ });
1684
+ },
1685
+ );
1686
+ },
1687
+ },
1688
+ {
1689
+ label: "Desktop notifications",
1690
+ hint: "toggle",
1691
+ run: async (io) => {
1692
+ let next = false;
1693
+ updateSettings((draft) => {
1694
+ draft.interface.desktopNotifications = !draft.interface.desktopNotifications;
1695
+ next = draft.interface.desktopNotifications;
1696
+ });
1697
+ io.ok(`Desktop notifications ${onOff(next)}`);
1698
+ },
1699
+ },
1700
+ {
1701
+ label: "Git commit attribution",
1702
+ hint: "toggle",
1703
+ run: async (io) => {
1704
+ let next = false;
1705
+ updateSettings((draft) => {
1706
+ draft.integrations.git.commitAttribution = !draft.integrations.git.commitAttribution;
1707
+ next = draft.integrations.git.commitAttribution;
1708
+ });
1709
+ io.ok(`Git commit attribution ${onOff(next)}`);
1710
+ },
1711
+ },
1712
+ ],
1713
+ },
1714
+ {
1715
+ id: "skills",
1716
+ title: "Skills & Extensions",
1717
+ summary: "project imports, ACP peers, plugins",
1718
+ aliases: ["extensions", "skill", "interop"],
1719
+ fields: () => {
1720
+ const settings = readSettings();
1721
+ const rows: Array<readonly [string, string]> = [
1722
+ ["Trust project imports", settings.integrations.projectResources.trustProjectImports ? "trusted" : "untrusted"],
1723
+ ["External ACP agents", String(settings.integrations.externalAgents.entries.length)],
1724
+ ];
1725
+ for (const agent of settings.integrations.externalAgents.entries) {
1726
+ rows.push([` ${agent.id}`, `${agent.command} (governance: ${agent.toolGovernance ?? "default"})`]);
1727
+ }
1728
+ rows.push(
1729
+ ["Runtime plugins", listOr(settings.integrations.runtimePlugins)],
1730
+ ["Library remote sync", onOff(settings.integrations.library.sync)],
1731
+ );
1732
+ return rows;
1733
+ },
1734
+ actions: [
1735
+ {
1736
+ label: "Trust project imports",
1737
+ hint: "toggle",
1738
+ run: async (io) => {
1739
+ let next = false;
1740
+ updateSettings((draft) => {
1741
+ draft.integrations.projectResources.trustProjectImports =
1742
+ !draft.integrations.projectResources.trustProjectImports;
1743
+ next = draft.integrations.projectResources.trustProjectImports;
1744
+ });
1745
+ io.ok(`Project imports ${next ? "trusted" : "untrusted"}`);
1746
+ },
1747
+ },
1748
+ {
1749
+ label: "Review external ACP agents",
1750
+ hint: "runs the interop review",
1751
+ run: async (io) => {
1752
+ await runInteropReview({ rl: io.rl, streams: io.streams });
1753
+ },
1754
+ },
1755
+ {
1756
+ label: "Library remote sync",
1757
+ hint: "toggle",
1758
+ run: async (io) => {
1759
+ let next = false;
1760
+ updateSettings((draft) => {
1761
+ draft.integrations.library.sync = !draft.integrations.library.sync;
1762
+ next = draft.integrations.library.sync;
1763
+ });
1764
+ io.ok(`Library remote sync ${onOff(next)}`);
1765
+ },
1766
+ },
1767
+ ],
1768
+ },
1769
+ {
1770
+ id: "diagnostics",
1771
+ title: "Diagnostics",
1772
+ summary: "version, directories, doctor, raw settings",
1773
+ aliases: ["doctor", "diag"],
1774
+ fields: () => {
1775
+ const dirs = resolveClioDirs();
1776
+ const info = getVersionInfo();
1777
+ return [
1778
+ ["Clio Coder", info.clio],
1779
+ ["Node.js", info.node],
1780
+ ["Platform", info.platform],
1781
+ ["Config dir", shortenPath(dirs.config)],
1782
+ ["Data dir", shortenPath(dirs.data)],
1783
+ ["State dir", shortenPath(dirs.state)],
1784
+ ["Cache dir", shortenPath(dirs.cache)],
1785
+ ];
1786
+ },
1787
+ actions: [
1788
+ {
1789
+ label: "Run doctor",
1790
+ hint: "read-only health check",
1791
+ run: async () => {
1792
+ const { runDoctorCommand } = await import("./doctor.js");
1793
+ await runDoctorCommand([]);
1794
+ },
1795
+ },
1796
+ {
1797
+ label: "Show raw settings.yaml",
1798
+ run: async (io) => {
1799
+ const file = settingsPath();
1800
+ if (!existsSync(file)) {
1801
+ io.out.write(" settings.yaml does not exist yet.\n");
1802
+ return;
1803
+ }
1804
+ io.out.write(`\n--- ${shortenPath(file)} ---\n${readFileSync(file, "utf8")}--- end ---\n`);
1805
+ },
1806
+ },
1807
+ ],
1808
+ },
1809
+ ];
1810
+
1811
+ /** A `ParsedArgs` with nothing set, for the target wizard called from a section. */
1812
+ function emptyArgs(): ParsedArgs {
1813
+ return {
1814
+ positional: [],
1815
+ help: false,
1816
+ list: false,
1817
+ all: false,
1818
+ interop: false,
1819
+ json: false,
1820
+ force: false,
1821
+ gateway: false,
1822
+ setOrchestrator: false,
1823
+ setBackground: false,
1824
+ setWorkerDefault: false,
1825
+ };
1826
+ }
1827
+
1828
+ async function askList(
1829
+ io: SectionIo,
1830
+ label: string,
1831
+ current: ReadonlyArray<string>,
1832
+ apply: (values: string[]) => void,
1833
+ ): Promise<void> {
1834
+ const answer = await ask(io.rl, `${label} (comma-separated, blank clears)`, current.join(", "));
1835
+ if (answer === null) return;
1836
+ const values = answer
1837
+ .split(",")
1838
+ .map((entry) => entry.trim())
1839
+ .filter((entry) => entry.length > 0);
1840
+ apply(values);
1841
+ io.ok(values.length === 0 ? `${label} cleared` : `${label} set to ${values.join(", ")}`);
1842
+ }
1843
+
1844
+ async function assignTarget(io: SectionIo, role: "chat" | "fleet"): Promise<void> {
1845
+ const settings = readSettings();
1846
+ const ids = settings.targets.map((target) => target.id);
1847
+ if (ids.length === 0) {
1848
+ io.out.write(" No targets registered yet. Add one first.\n");
1849
+ return;
1850
+ }
1851
+ io.out.write(` Registered: ${ids.join(", ")}\n`);
1852
+ const currentTarget = role === "chat" ? settings.chat.target : settings.fleet.default.target;
1853
+ const chosen = await ask(io.rl, `${role === "chat" ? "Chat" : "Fleet"} target id`, currentTarget ?? ids[0] ?? "");
1854
+ if (chosen === null) return;
1855
+ if (!ids.includes(chosen)) {
1856
+ io.out.write(` ${chosen} is not a registered target; nothing changed.\n`);
1857
+ return;
1858
+ }
1859
+ const target = settings.targets.find((entry) => entry.id === chosen);
1860
+ const model = await ask(io.rl, "Model (blank for the target default)", target?.defaultModel ?? "");
1861
+ if (model === null) return;
1862
+ updateSettings((draft) => {
1863
+ if (role === "chat") {
1864
+ draft.chat.target = chosen;
1865
+ draft.chat.model = model.length > 0 ? model : null;
1866
+ } else {
1867
+ draft.fleet.default.target = chosen;
1868
+ draft.fleet.default.model = model.length > 0 ? model : null;
1869
+ }
1870
+ });
1871
+ io.ok(`${role === "chat" ? "Chat" : "Fleet"} target set to ${chosen}`);
1872
+ }
1873
+
1874
+ interface MenuEntry {
1875
+ label: string;
1876
+ hint?: string;
1877
+ }
1878
+
1879
+ type MenuChoice = { kind: "index"; index: number } | { kind: "back" } | { kind: "quit" };
1880
+
1881
+ /**
1882
+ * Pick one entry, by arrow keys on a terminal and by number everywhere else.
1883
+ * The numbered fallback is not a lesser path: it is what a recorded session, a
1884
+ * pipe, and a dumb terminal get, and it accepts the same `b`/`q` tokens the rest
1885
+ * of this CLI uses.
1886
+ */
1887
+ async function pickEntry(
1888
+ rl: ReturnType<typeof createInterface>,
1889
+ io: ConfigureStreams,
1890
+ entries: ReadonlyArray<MenuEntry>,
1891
+ backLabel: string,
1892
+ plain: boolean,
1893
+ ): Promise<MenuChoice> {
1894
+ const out = io.out;
1895
+ if (canSelect(io.in as NodeJS.ReadStream, out as NodeJS.WriteStream)) {
1896
+ const result = await promptSelect({
1897
+ choices: entries.map((entry, index) => ({
1898
+ value: index,
1899
+ label: entry.label,
1900
+ ...(entry.hint === undefined ? {} : { hint: entry.hint }),
1901
+ })),
1902
+ railPrefix: railPrefix(plain),
1903
+ backLabel,
1904
+ input: io.in as NodeJS.ReadStream,
1905
+ output: out as NodeJS.WriteStream,
1906
+ });
1907
+ if (result.kind === "selected") return { kind: "index", index: result.value };
1908
+ return result.kind === "quit" ? { kind: "quit" } : { kind: "back" };
1909
+ }
1910
+
1911
+ const rail = railPrefix(plain);
1912
+ entries.forEach((entry, index) => {
1913
+ out.write(`${rail}${index + 1}. ${entry.label}${entry.hint === undefined ? "" : ` (${entry.hint})`}\n`);
1914
+ });
1915
+ out.write(`${rail}b. ${backLabel}\n${rail}q. Quit\n`);
1916
+ const answer = await ask(rl, "\nSelection", "b");
1917
+ if (answer === null) return { kind: "quit" };
1918
+ const trimmed = answer.trim().toLowerCase();
1919
+ if (trimmed === "b" || trimmed === "back" || trimmed.length === 0) return { kind: "back" };
1920
+ if (trimmed === "q" || trimmed === "quit") return { kind: "quit" };
1921
+ const index = Number.parseInt(trimmed, 10) - 1;
1922
+ if (Number.isInteger(index) && index >= 0 && index < entries.length) return { kind: "index", index };
1923
+ out.write(`${rail}Not one of the choices: ${answer}\n`);
1924
+ return { kind: "back" };
1925
+ }
1926
+
1927
+ /** Header, source file, and current values for one screen. */
1928
+ function renderSection(spec: SectionSpec, out: NodeJS.WritableStream): LifecyclePresenter {
1929
+ const presenter = createLifecyclePresenter({ stream: out });
1930
+ presenter.header(spec.title, "configure");
1931
+ presenter.note(`Source: ${shortenPath(settingsPath())}`);
1932
+ presenter.fields(spec.fields());
1933
+ return presenter;
1934
+ }
1935
+
1936
+ /** Show one screen and stay on it until the operator leaves or quits. */
1937
+ async function runSection(
1938
+ rl: ReturnType<typeof createInterface>,
1939
+ streams: ConfigureStreams,
1940
+ spec: SectionSpec,
1941
+ backLabel: string,
1942
+ ): Promise<"back" | "quit"> {
1943
+ for (;;) {
1944
+ const presenter = renderSection(spec, streams.out);
1945
+ const io: SectionIo = {
1946
+ rl,
1947
+ out: streams.out,
1948
+ streams,
1949
+ ok: (text) => {
1950
+ presenter.completedStep(text);
1951
+ },
1952
+ };
1953
+ presenter.blank();
1954
+ const choice = await pickEntry(rl, streams, spec.actions, backLabel, presenter.isPlain());
1955
+ if (choice.kind !== "index") return choice.kind;
1956
+ const action = spec.actions[choice.index];
1957
+ if (action !== undefined) await action.run(io);
1958
+ }
1959
+ }
1960
+
1961
+ /** The top-level screen. Leaving it is an ordinary exit, not a cancellation. */
1962
+ async function runConfigSectionsMenu(
1963
+ rl: ReturnType<typeof createInterface>,
1964
+ streams: ConfigureStreams,
1965
+ ): Promise<number> {
1966
+ for (;;) {
1967
+ const presenter = createLifecyclePresenter({ stream: streams.out });
1968
+ presenter.header("Clio Coder Configuration", "configure");
1969
+ presenter.note(`Source: ${shortenPath(settingsPath())}`);
1970
+ presenter.blank();
1971
+ const choice = await pickEntry(
1972
+ rl,
1973
+ streams,
1974
+ SECTIONS.map((section) => ({ label: section.title, hint: section.summary })),
1975
+ "quit",
1976
+ presenter.isPlain(),
1977
+ );
1978
+ // Escape and q both leave the top screen, and leaving a settings menu you
1979
+ // only looked at is not a failure: this used to exit 130 and print
1980
+ // "error: configuration cancelled" over a run that did exactly what was
1981
+ // asked. The first-run wizard still exits 130, because there a cancel
1982
+ // really does leave Clio unconfigured.
1983
+ if (choice.kind !== "index") return 0;
1984
+ const spec = SECTIONS[choice.index];
1985
+ if (spec === undefined) continue;
1986
+ if ((await runSection(rl, streams, spec, "back")) === "quit") return 0;
1987
+ }
1988
+ }
1989
+
1990
+ /** Resolve a `--section` value to exactly one screen, by id or listed alias. */
1991
+ function findSection(name: string): SectionSpec | undefined {
1992
+ const wanted = name.trim().toLowerCase();
1993
+ return SECTIONS.find((section) => section.id === wanted || (section.aliases ?? []).includes(wanted));
1994
+ }
1995
+
1996
+ const SECTION_IDS: ReadonlyArray<string> = SECTIONS.map((section) => section.id);
1997
+
1998
+ /** The streams this run reads from and writes to, which the tests replace. */
1999
+ interface ConfigureStreams {
2000
+ in: NodeJS.ReadableStream;
2001
+ out: NodeJS.WritableStream;
2002
+ }
2003
+
2004
+ async function runInteractive(
2005
+ rl: ReturnType<typeof createInterface>,
2006
+ preselectedRuntime: RuntimeDescriptor | null,
2007
+ defaults: ParsedArgs,
2008
+ streams: ConfigureStreams,
2009
+ ): Promise<number> {
2010
+ if (preselectedRuntime) {
2011
+ return await runTargetSetupInteractive(rl, preselectedRuntime, defaults);
2012
+ }
2013
+
2014
+ if (defaults.section !== undefined) {
2015
+ const spec = findSection(defaults.section);
2016
+ if (spec === undefined) {
2017
+ printError(`unknown section: ${defaults.section}. Sections: ${SECTION_IDS.join(", ")}`);
2018
+ return 2;
2019
+ }
2020
+ // Without a terminal there is nobody to answer, and printing a prompt into
2021
+ // a pipe is a dead end that reads as a hang. The screen is still worth
2022
+ // showing, so a scripted `configure --section fleet` is a read of that
2023
+ // section rather than a refusal.
2024
+ if ((streams.in as { isTTY?: boolean }).isTTY !== true) {
2025
+ renderSection(spec, streams.out).done("Done");
2026
+ return 0;
2027
+ }
2028
+ await runSection(rl, streams, spec, "quit");
2029
+ return 0;
2030
+ }
2031
+
2032
+ if (readSettings().targets.length === 0) {
2033
+ // The first run is its own screen flow. It only exists where a keypress can
2034
+ // be read; a pipe, a recorded session, and a dumb terminal keep the
2035
+ // numbered readline path below, which is still the only way to answer them.
2036
+ //
2037
+ // The readline interface is closed first. Left open it stays subscribed to
2038
+ // stdin's keypresses and echoes each one, which puts a stray line under
2039
+ // every menu the wizard draws and breaks the erase that replaces the menu
2040
+ // with its answer. The wizard opens its own where a browser sign-in needs
2041
+ // one.
2042
+ if (canRunOnboarding(streams)) {
2043
+ rl.close();
2044
+ return await runOnboardingWizard(streams);
2045
+ }
2046
+ const runtime = await pickRuntimeViaCategory(rl);
2047
+ if (!runtime) {
2048
+ printError("configuration cancelled");
2049
+ return 130;
2050
+ }
2051
+ return await runTargetSetupInteractive(rl, runtime, defaults);
2052
+ }
2053
+
2054
+ return await runConfigSectionsMenu(rl, streams);
2055
+ }
2056
+
2057
+ export async function runConfigureCommand(
2058
+ argv: ReadonlyArray<string>,
2059
+ inStream: NodeJS.ReadableStream = input,
2060
+ outStream: NodeJS.WritableStream = output,
2061
+ ): Promise<number> {
1514
2062
  let args: ParsedArgs;
1515
2063
  try {
1516
2064
  args = parseSetupArgs(argv);
@@ -1526,15 +2074,21 @@ export async function runConfigureCommand(argv: ReadonlyArray<string>): Promise<
1526
2074
  initializeClioHome();
1527
2075
  ensureRegistryPopulated();
1528
2076
 
2077
+ if (args.json) {
2078
+ const settings = readSettings();
2079
+ outStream.write(`${JSON.stringify(settings, null, 2)}\n`);
2080
+ return 0;
2081
+ }
2082
+
1529
2083
  if (args.list) {
1530
2084
  printRuntimeList(args.all);
1531
2085
  return 0;
1532
2086
  }
1533
2087
  if (args.interop) {
1534
- if (!input.isTTY) return runInteropReview(null);
2088
+ if (!input.isTTY) return runInteropReview({ rl: null });
1535
2089
  const rl = createInterface({ input, output });
1536
2090
  try {
1537
- return await runInteropReview(rl);
2091
+ return await runInteropReview({ rl, streams: { in: input, out: output } });
1538
2092
  } finally {
1539
2093
  rl.close();
1540
2094
  }
@@ -1595,9 +2149,9 @@ export async function runConfigureCommand(argv: ReadonlyArray<string>): Promise<
1595
2149
  return 2;
1596
2150
  }
1597
2151
 
1598
- const rl = createInterface({ input, output });
2152
+ const rl = createInterface({ input: inStream, output: outStream });
1599
2153
  try {
1600
- return await runInteractive(rl, runtime, args);
2154
+ return await runInteractive(rl, runtime, args, { in: inStream, out: outStream });
1601
2155
  } catch (err) {
1602
2156
  printError(err instanceof Error ? err.message : String(err));
1603
2157
  return 1;