@iowarp/clio-coder 0.4.5 → 0.4.6

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 (188) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/CONTRIBUTING.md +14 -9
  3. package/NOTICE +16 -0
  4. package/README.md +312 -685
  5. package/dist/{acp-H3CU2HBQ.js → acp-UVEVWSDL.js} +6 -6
  6. package/dist/{agents-IXQQZ7BD.js → agents-VWY37DTK.js} +31 -30
  7. package/dist/assets/codewiki.json +1 -1
  8. package/dist/{auth-R7KYF7N3.js → auth-PCV2XQDV.js} +6 -6
  9. package/dist/{builtins-CRFDQUVP.js → builtins-PD652MTK.js} +3 -3
  10. package/dist/{chunk-SDWNDICN.js → chunk-24T2YZ7A.js} +58 -56
  11. package/dist/{chunk-FSTIBAGM.js → chunk-2AKYFREM.js} +7 -7
  12. package/dist/{chunk-SE4ZKILO.js → chunk-3BJGMT2D.js} +30 -9
  13. package/dist/{chunk-GVYWZNRK.js → chunk-3HQVBIKD.js} +2 -2
  14. package/dist/{chunk-EWD5P3GK.js → chunk-3JLFK5MW.js} +4 -4
  15. package/dist/{chunk-O2RQ7LCK.js → chunk-3O2ZAUGQ.js} +2 -2
  16. package/dist/{chunk-GXCCHKN6.js → chunk-3QKXHCGS.js} +4 -4
  17. package/dist/{chunk-YEJIG7LN.js → chunk-3WBALRQH.js} +7 -7
  18. package/dist/{chunk-I7VLWRK2.js → chunk-4DU2TVI4.js} +5 -5
  19. package/dist/{chunk-TEG3RYVZ.js → chunk-4U4TKZFR.js} +3 -3
  20. package/dist/{chunk-EHCPSZJF.js → chunk-4VMV6OS6.js} +2 -2
  21. package/dist/{chunk-T7FDXNRU.js → chunk-536HXRFE.js} +4 -4
  22. package/dist/{chunk-ZMZQEBKI.js → chunk-5QRX7SLS.js} +8 -8
  23. package/dist/{chunk-RXIOCVBZ.js → chunk-6HAO6X5B.js} +4 -4
  24. package/dist/{chunk-HGSLRG33.js → chunk-6OII3LOK.js} +2 -2
  25. package/dist/{chunk-U2JOLB7O.js → chunk-6RYDAVQT.js} +32 -32
  26. package/dist/{chunk-T53XI6I7.js → chunk-6VRRJT7L.js} +2 -2
  27. package/dist/{chunk-RLTT5GVY.js → chunk-7JSSKXG2.js} +4 -4
  28. package/dist/{chunk-633EXGZA.js → chunk-ACCKVSHU.js} +3 -3
  29. package/dist/{chunk-6SYEGZI3.js → chunk-B3MOMLJ4.js} +2 -2
  30. package/dist/{chunk-UXAC2FX4.js → chunk-BLTEAUCB.js} +3 -3
  31. package/dist/{chunk-DRD2A54W.js → chunk-BT7HGDOX.js} +15 -15
  32. package/dist/{chunk-F34AR6O3.js → chunk-DXLUVUD4.js} +5 -5
  33. package/dist/{chunk-HR3HPQ76.js → chunk-E7RF4KBO.js} +2 -2
  34. package/dist/chunk-EM2WNQS2.js +93 -0
  35. package/dist/{chunk-KYKNBD4Y.js → chunk-F5Z7EWMN.js} +953 -233
  36. package/dist/{chunk-VLPTKCKT.js → chunk-GLYBJ4RQ.js} +3 -3
  37. package/dist/{chunk-KDWLXVMN.js → chunk-HPTJF6DG.js} +16 -39
  38. package/dist/{chunk-6CDNRHWH.js → chunk-HZ2T2YED.js} +2 -2
  39. package/dist/{chunk-UTUNT5QL.js → chunk-HZFBVU5X.js} +7 -7
  40. package/dist/{chunk-KF6GFW6O.js → chunk-I2K2NCZQ.js} +7 -7
  41. package/dist/{chunk-5DDOTTG4.js → chunk-IGLVVNEG.js} +2 -2
  42. package/dist/{chunk-4SVIVOMC.js → chunk-IKRWLEDA.js} +5 -5
  43. package/dist/{chunk-R6MAYTMS.js → chunk-JMPL7NUB.js} +3 -3
  44. package/dist/{chunk-5ZMXGW7P.js → chunk-KAFMW7WM.js} +4 -85
  45. package/dist/{chunk-R6MICQYU.js → chunk-LIN2K3EL.js} +4 -4
  46. package/dist/{chunk-QEQISF75.js → chunk-LK2FN5JY.js} +1 -1
  47. package/dist/{chunk-YYB3MC65.js → chunk-N2OKBREN.js} +2 -2
  48. package/dist/{chunk-XXBQKW32.js → chunk-NNWJYUNO.js} +4 -4
  49. package/dist/{chunk-TZ7MFRG4.js → chunk-P5JMBPDJ.js} +22 -20
  50. package/dist/{chunk-WFKDPU7U.js → chunk-P6M7RKMI.js} +2 -2
  51. package/dist/{chunk-MDU3C27C.js → chunk-PZN2LQY6.js} +5 -5
  52. package/dist/{chunk-S32UQ5GL.js → chunk-Q7NNUHQV.js} +15 -15
  53. package/dist/{chunk-U7VQ4NI4.js → chunk-QCOV5DFY.js} +5 -5
  54. package/dist/{chunk-MC3JSVR6.js → chunk-QJMRYTCX.js} +7 -7
  55. package/dist/{chunk-DIMRLNIC.js → chunk-QNWRHOMF.js} +3 -3
  56. package/dist/{chunk-ILJ7DWGE.js → chunk-QXFBABFJ.js} +4 -4
  57. package/dist/chunk-R2CT5YDO.js +66 -0
  58. package/dist/{chunk-E3BQTQUP.js → chunk-RGZNQ5CT.js} +3 -3
  59. package/dist/{chunk-SBZX6ARC.js → chunk-SXQKNKRA.js} +7 -7
  60. package/dist/{chunk-WLMSOT73.js → chunk-TJFDL26W.js} +3 -3
  61. package/dist/{chunk-EQEJJ53P.js → chunk-VAHLPXPY.js} +2 -2
  62. package/dist/{chunk-7TKWWPD7.js → chunk-VANN3GDP.js} +3 -23
  63. package/dist/{chunk-GNQRWTMY.js → chunk-VHMX7SXF.js} +2 -2
  64. package/dist/{chunk-UL36QJX6.js → chunk-WKUYQBCG.js} +6 -6
  65. package/dist/{chunk-FKIOX6DS.js → chunk-WNGF5AJX.js} +9 -4
  66. package/dist/{chunk-ICO4TQ2F.js → chunk-WV6TDAPN.js} +36 -20
  67. package/dist/{chunk-K6RDB7IA.js → chunk-X3BI7HTV.js} +9 -9
  68. package/dist/{chunk-NY6IWDVU.js → chunk-XWMSPJYN.js} +2 -2
  69. package/dist/{chunk-7HBHHVCK.js → chunk-YPRSNUTC.js} +2 -2
  70. package/dist/{chunk-ONRTXOZY.js → chunk-ZS43Y2FT.js} +2 -2
  71. package/dist/cli/index.js +27 -27
  72. package/dist/{clio-5I2IBFYL.js → clio-QQCMTUZE.js} +6 -6
  73. package/dist/{code-nav-YXIAEZVS.js → code-nav-GUPM6766.js} +8 -8
  74. package/dist/{config-Y2JDJ3JJ.js → config-IIM7YOD3.js} +42 -41
  75. package/dist/{configure-SML6UBZ5.js → configure-4ZM5HWJX.js} +28 -10
  76. package/dist/{context-S2EEC3EJ.js → context-44BZLJ3J.js} +12 -12
  77. package/dist/{context-P5YSEO55.js → context-AIDVQKMW.js} +34 -33
  78. package/dist/{context-QQ53HSGJ.js → context-OL5P3HEP.js} +25 -24
  79. package/dist/{context-clear-TMEQCOTH.js → context-clear-OQV76MYP.js} +34 -33
  80. package/dist/{context-working-set-CNLSQN5K.js → context-working-set-ZUW3EGLS.js} +10 -9
  81. package/dist/{detail-5572VLBO.js → detail-UXPWRANG.js} +35 -34
  82. package/dist/{dispatch-runner-FRHD6P2O.js → dispatch-runner-P3ZW6G77.js} +38 -37
  83. package/dist/{doctor-N3LCM2LG.js → doctor-JJVGPZWT.js} +18 -17
  84. package/dist/{eval-BUU3ZOUZ.js → eval-V2RTN3UH.js} +26 -25
  85. package/dist/{evidence-DTGEN456.js → evidence-35PYWGSO.js} +34 -33
  86. package/dist/{evidence-SQ5DNFEN.js → evidence-CIGGKI6J.js} +36 -35
  87. package/dist/{evolve-IJNUFZDS.js → evolve-33SM3NJP.js} +34 -33
  88. package/dist/{fleet-ZNNVSYFO.js → fleet-LFO7SIPC.js} +62 -61
  89. package/dist/{fleet-commands-5YE7NPJL.js → fleet-commands-RLK72GU3.js} +9 -9
  90. package/dist/fleet-decisions-3SBSCTCW.js +1 -1
  91. package/dist/{fleet-graph-6HHPWF5T.js → fleet-graph-JZIBCGMW.js} +11 -11
  92. package/dist/{fleet-inspect-JEYMGCGC.js → fleet-inspect-2L5YGGZJ.js} +35 -34
  93. package/dist/{fleet-validate-4PM4GDDL.js → fleet-validate-LIAVKDUU.js} +12 -12
  94. package/dist/{fleet-verify-NJDTHTO3.js → fleet-verify-JIK3OQZR.js} +34 -33
  95. package/dist/{fleet-view-UP2BWQQY.js → fleet-view-IWHYGQKK.js} +35 -34
  96. package/dist/{init-S3GZQ5ZF.js → init-NJEDOTQW.js} +46 -45
  97. package/dist/{interop-YOTGXFJM.js → interop-RVCAYECG.js} +4 -4
  98. package/dist/{inventory-K2OZ36YJ.js → inventory-74UL7UHU.js} +35 -34
  99. package/dist/{library-GJI6W6OK.js → library-CTKZA7K3.js} +10 -10
  100. package/dist/{memory-OQOXQGYD.js → memory-I76I5VUH.js} +34 -33
  101. package/dist/{models-3IK2Y5WE.js → models-D4YDIQZQ.js} +19 -18
  102. package/dist/{monitor-FTJLTKCQ.js → monitor-NKVD55HX.js} +40 -39
  103. package/dist/{orchestrator-N47O7SFD.js → orchestrator-I6OATVPK.js} +658 -1092
  104. package/dist/{preload-65U34NG6.js → preload-EJAWJEIO.js} +34 -33
  105. package/dist/{reset-MUVQDLNL.js → reset-4XMQKDNZ.js} +5 -3
  106. package/dist/{resources-LM76COAO.js → resources-TVYVXK3Q.js} +10 -10
  107. package/dist/{run-3VFZQ33N.js → run-OWQNNSJU.js} +61 -60
  108. package/dist/{share-4JUB4BL7.js → share-NPZZMUXG.js} +11 -11
  109. package/dist/{skills-S36AAJHU.js → skills-OXFEAWMT.js} +12 -12
  110. package/dist/{skills-eval-G6T57L7M.js → skills-eval-J5TCKSJA.js} +38 -37
  111. package/dist/{skills-inventory-WLM46PJH.js → skills-inventory-7II4DM57.js} +10 -10
  112. package/dist/{slash-commands-GBARG3Y7.js → slash-commands-MDSDSWYF.js} +26 -25
  113. package/dist/{targets-N5REUETU.js → targets-KILZEOCE.js} +36 -20
  114. package/dist/{tasks-6BVY5YZF.js → tasks-R6P6OV22.js} +7 -7
  115. package/dist/{terminal-lease-57UEOJKS.js → terminal-lease-LZCG2KN3.js} +2 -2
  116. package/dist/{upgrade-4HD5HAIV.js → upgrade-D2OZH6IQ.js} +9 -9
  117. package/dist/{usage-4GWM2WWP.js → usage-PIASNUS4.js} +40 -39
  118. package/dist/{verifiers-GSKWKWVG.js → verifiers-LTHUY2PX.js} +9 -9
  119. package/dist/{verify-3AS3ULCY.js → verify-HYF7KBVE.js} +7 -7
  120. package/dist/{wiki-generate-7BV7XWIY.js → wiki-generate-RK7JQ5NP.js} +51 -50
  121. package/dist/worker/entry.js +41 -39
  122. package/docs/architecture/context-engine.md +1 -1
  123. package/docs/architecture/tui-design.md +45 -30
  124. package/docs/guide/commands-and-modes.md +26 -25
  125. package/docs/guide/configuration-and-targets.md +122 -27
  126. package/docs/guide/configuration-reference.md +9 -4
  127. package/docs/guide/fleet-dispatch.md +1 -6
  128. package/docs/guide/glossary.md +1 -1
  129. package/docs/guide/installation-and-lifecycle.md +44 -11
  130. package/docs/process/development-pipeline.md +1 -1
  131. package/docs/process/documentation-coverage.md +1 -1
  132. package/docs/process/release-cut-checklist.md +18 -6
  133. package/package.json +1 -1
  134. package/src/cli/ask.ts +10 -10
  135. package/src/cli/configure-editor.ts +51 -0
  136. package/src/cli/configure-interop.ts +1 -1
  137. package/src/cli/configure-oauth.ts +1 -1
  138. package/src/cli/configure-onboarding.ts +232 -96
  139. package/src/cli/configure-prompts.ts +76 -0
  140. package/src/cli/configure-quick.ts +274 -0
  141. package/src/cli/configure-target.ts +14 -2
  142. package/src/cli/configure.ts +413 -174
  143. package/src/cli/oauth-manual-input.ts +1 -1
  144. package/src/cli/reset.ts +2 -0
  145. package/src/cli/select.ts +35 -4
  146. package/src/cli/upgrade.ts +2 -2
  147. package/src/core/config.ts +6 -1
  148. package/src/core/defaults.ts +33 -11
  149. package/src/{interactive → core}/external-editor.ts +4 -4
  150. package/src/domains/config/classify.ts +1 -1
  151. package/src/domains/config/keybindings.ts +3 -31
  152. package/src/domains/providers/probe/fingerprint.ts +13 -6
  153. package/src/domains/providers/probe/reasoning.ts +2 -0
  154. package/src/domains/providers/runtimes/common/probe-helpers.ts +23 -7
  155. package/src/domains/providers/runtimes/protocol/anthropic-compat.ts +7 -2
  156. package/src/domains/providers/runtimes/protocol/openai-compat.ts +10 -4
  157. package/src/interactive/application-controller.ts +1 -31
  158. package/src/interactive/chat-panel.ts +163 -637
  159. package/src/interactive/chat-renderer.ts +13 -30
  160. package/src/interactive/dispatch-board.ts +1 -3
  161. package/src/interactive/editor-submit.ts +8 -31
  162. package/src/interactive/footer/dashboard.ts +8 -3
  163. package/src/interactive/footer/widgets.ts +50 -91
  164. package/src/interactive/interactive-application.ts +16 -42
  165. package/src/interactive/interactive-event-projection.ts +2 -44
  166. package/src/interactive/interactive-input-runtime.ts +3 -28
  167. package/src/interactive/interactive-presentation.ts +30 -9
  168. package/src/interactive/interactive-slash-runtime.ts +3 -40
  169. package/src/interactive/layout.ts +29 -4
  170. package/src/interactive/overlay-general-openers.ts +2 -0
  171. package/src/interactive/overlay-lifecycle.ts +1 -0
  172. package/src/interactive/overlay-session-lifecycle.ts +1 -1
  173. package/src/interactive/overlays/settings.ts +9 -8
  174. package/src/interactive/renderers/preview.ts +16 -0
  175. package/src/interactive/renderers/tool-execution.ts +67 -160
  176. package/src/interactive/renderers/worker-entry.ts +57 -41
  177. package/src/interactive/slash-autocomplete.ts +1 -6
  178. package/src/interactive/slash-commands.ts +7 -39
  179. package/src/interactive/status/controller.ts +0 -5
  180. package/src/interactive/status/state-machine.ts +44 -26
  181. package/src/interactive/status/types.ts +3 -0
  182. package/src/interactive/status/verbs.ts +13 -12
  183. package/src/interactive/transcript-detail.ts +50 -112
  184. package/src/interactive/view/artifacts.ts +4 -0
  185. package/src/interactive/view/view-overlay.ts +44 -34
  186. package/src/tools/presentation.ts +1 -1
  187. package/src/tools/registry.ts +1 -1
  188. package/src/tools/verify/numeric.ts +4 -4
@@ -1,17 +1,17 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import { stdin as input, stdout as output } from "node:process";
3
- import { createInterface } from "node:readline/promises";
4
3
  import {
5
4
  bindAgentProfileInSettings,
6
5
  type ClioSettings,
7
6
  readSettings,
8
7
  removeTargetFromSettings,
9
8
  settingsPath,
9
+ updateSavedSettingsDocument,
10
10
  updateSettings,
11
11
  } from "../core/config.js";
12
12
  import {
13
13
  type AutonomyLevel,
14
- type OutputVerbosity,
14
+ type OutputStyle,
15
15
  type PanesSettings,
16
16
  type SmoothStreaming,
17
17
  THINKING_LEVELS,
@@ -32,6 +32,7 @@ import {
32
32
  listProviderSupportEntries,
33
33
  type ProviderSupportEntry,
34
34
  recordTargetModelSnapshot,
35
+ resolveAuthTarget,
35
36
  resolveRuntimeAuthTarget,
36
37
  supportGroupLabel,
37
38
  } from "../domains/providers/index.js";
@@ -41,8 +42,10 @@ import { registerBuiltinRuntimes } from "../domains/providers/runtimes/builtins.
41
42
  import { greetLmStudio } from "../domains/providers/runtimes/common/lmstudio-http.js";
42
43
  import type { ProbeResult, RuntimeDescriptor } from "../domains/providers/types/runtime-descriptor.js";
43
44
  import type { TargetDescriptor } from "../domains/providers/types/target-descriptor.js";
45
+ import { AUTONOMY_LEVELS } from "../domains/safety/index.js";
44
46
  import { registerClioOAuthProviders } from "../engine/oauth.js";
45
47
  import { ask, askYesNo } from "./ask.js";
48
+ import { editSettings } from "./configure-editor.js";
46
49
  import { runInteropReview } from "./configure-interop.js";
47
50
  import {
48
51
  type ConfigureCategory,
@@ -54,6 +57,8 @@ import {
54
57
  } from "./configure-layout.js";
55
58
  import { loginOAuthRuntime } from "./configure-oauth.js";
56
59
  import { canRunOnboarding, runOnboardingWizard } from "./configure-onboarding.js";
60
+ import { ConfigureNavigation, ConfigurePrompts } from "./configure-prompts.js";
61
+ import { runQuickConnect } from "./configure-quick.js";
57
62
  import {
58
63
  applyTarget,
59
64
  assertOrchestratorReplacementEligible,
@@ -72,6 +77,7 @@ import {
72
77
  setOrchestratorPointer,
73
78
  setWorkerDefaultPointer,
74
79
  setWorkerProfilePointer,
80
+ targetApiKeyRef,
75
81
  validateContextWindowOverride,
76
82
  validateResolvedModel,
77
83
  type WireModelInventory,
@@ -87,13 +93,17 @@ const HELP = `clio-coder configure
87
93
  Configure model targets and runtime settings for chat and fleet dispatch.
88
94
 
89
95
  Usage:
90
- clio-coder configure interactive configuration wizard
96
+ clio-coder configure Quick Connect, Settings, or Diagnostics
97
+ clio-coder configure --quick connect an endpoint with recommended defaults
98
+ clio-coder configure --settings open the complete settings menu
91
99
  clio-coder configure --section <name> open one section directly:
92
100
  targets, models, chat, fleet,
93
- permissions, panes, skills, diagnostics
101
+ permissions, panes, skills, diagnostics, advanced
94
102
  Without a terminal this prints the
95
103
  section's values and exits.
96
104
  clio-coder configure --json emit the effective settings as JSON
105
+ clio-coder configure --edit edit all user settings in VISUAL/EDITOR;
106
+ validate and review before saving
97
107
  clio-coder configure --interop review detected coding agents as delegation peers
98
108
  clio-coder configure --list list target runtimes (user-facing only)
99
109
  clio-coder configure --list --all list every registered runtime including aliases
@@ -138,6 +148,9 @@ interface ParsedArgs {
138
148
  all: boolean;
139
149
  interop: boolean;
140
150
  json: boolean;
151
+ edit: boolean;
152
+ quick: boolean;
153
+ settings: boolean;
141
154
  section?: string;
142
155
  remove?: string;
143
156
  renameOld?: string;
@@ -173,6 +186,9 @@ function parseSetupArgs(argv: ReadonlyArray<string>): ParsedArgs {
173
186
  all: false,
174
187
  interop: false,
175
188
  json: false,
189
+ edit: false,
190
+ quick: false,
191
+ settings: false,
176
192
  force: false,
177
193
  gateway: false,
178
194
  setOrchestrator: false,
@@ -195,6 +211,15 @@ function parseSetupArgs(argv: ReadonlyArray<string>): ParsedArgs {
195
211
  case "--json":
196
212
  out.json = true;
197
213
  break;
214
+ case "--edit":
215
+ out.edit = true;
216
+ break;
217
+ case "--quick":
218
+ out.quick = true;
219
+ break;
220
+ case "--settings":
221
+ out.settings = true;
222
+ break;
198
223
  case "--section":
199
224
  out.section = need();
200
225
  break;
@@ -316,7 +341,9 @@ function printRuntimeList(includeHidden: boolean): void {
316
341
  const runtime = getRuntimeRegistry().get(entry.runtimeId);
317
342
  const status =
318
343
  runtime && entry.connectable
319
- ? auth.statusForTarget(resolveRuntimeAuthTarget(runtime), { includeFallback: false })
344
+ ? auth.statusForTarget(resolveRuntimeAuthTarget(runtime), {
345
+ includeFallback: false,
346
+ })
320
347
  : null;
321
348
  const authLabel =
322
349
  runtime?.auth === "oauth"
@@ -386,7 +413,7 @@ function resolveModelChoice(
386
413
  }
387
414
 
388
415
  async function askModelChoice(
389
- rl: ReturnType<typeof createInterface>,
416
+ rl: ConfigurePrompts,
390
417
  label: string,
391
418
  wireModels: ReadonlyArray<string>,
392
419
  defaultValue: string | undefined,
@@ -433,6 +460,7 @@ async function runNonInteractive(runtime: RuntimeDescriptor, args: ParsedArgs):
433
460
  );
434
461
  return 2;
435
462
  }
463
+ initializeClioHome();
436
464
  const settings = readSettings();
437
465
  const auth = openAuthStorage();
438
466
  const support = buildProviderSupportEntry(runtime);
@@ -463,38 +491,48 @@ async function runNonInteractive(runtime: RuntimeDescriptor, args: ParsedArgs):
463
491
  );
464
492
  }
465
493
  }
466
- const authStatus = auth.statusForTarget(resolveRuntimeAuthTarget(runtime), { includeFallback: false });
494
+ const authStatus = auth.statusForTarget(
495
+ existing ? resolveAuthTarget(existing, runtime) : resolveRuntimeAuthTarget(runtime),
496
+ { includeFallback: false },
497
+ );
467
498
  const apiKeyEnv = args.apiKeyEnv ?? existing?.auth?.apiKeyEnvVar;
468
499
  const apiKeyRef =
469
500
  runtime.auth === "api-key" && (args.apiKey || existing?.auth?.apiKeyRef || authStatus.source === "stored-api-key")
470
- ? runtime.id
501
+ ? args.apiKey
502
+ ? targetApiKeyRef(args.id, settings.targets)
503
+ : (existing?.auth?.apiKeyRef ?? runtime.id)
471
504
  : undefined;
472
505
  const oauthProfile =
473
506
  runtime.auth === "oauth" ? (existing?.auth?.oauthProfile ?? runtime.oauthProviderId ?? runtime.id) : undefined;
474
- const seed = buildDescriptor(runtime, args.id, {
475
- ...(url !== undefined ? { url } : {}),
476
- ...(args.model !== undefined
477
- ? { model: args.model }
478
- : existing?.defaultModel
479
- ? { model: existing.defaultModel }
480
- : support.defaultModel
481
- ? { model: support.defaultModel }
507
+ const seed: TargetDescriptor = {
508
+ ...existing,
509
+ ...buildDescriptor(runtime, args.id, {
510
+ ...(url !== undefined ? { url } : {}),
511
+ ...(args.model !== undefined
512
+ ? { model: args.model }
513
+ : existing?.defaultModel
514
+ ? { model: existing.defaultModel }
515
+ : support.defaultModel
516
+ ? { model: support.defaultModel }
517
+ : {}),
518
+ ...(apiKeyEnv !== undefined ? { apiKeyEnv } : {}),
519
+ ...(apiKeyRef !== undefined ? { apiKeyRef } : {}),
520
+ ...(oauthProfile !== undefined ? { oauthProfile } : {}),
521
+ gateway: args.gateway || existing?.gateway === true,
522
+ ...(args.lifecycle !== undefined
523
+ ? { lifecycle: args.lifecycle }
524
+ : existing?.lifecycle !== undefined
525
+ ? { lifecycle: existing.lifecycle }
482
526
  : {}),
483
- ...(apiKeyEnv !== undefined ? { apiKeyEnv } : {}),
484
- ...(apiKeyRef !== undefined ? { apiKeyRef } : {}),
485
- ...(oauthProfile !== undefined ? { oauthProfile } : {}),
486
- gateway: args.gateway || existing?.gateway === true,
487
- ...(args.lifecycle !== undefined
488
- ? { lifecycle: args.lifecycle }
489
- : existing?.lifecycle !== undefined
490
- ? { lifecycle: existing.lifecycle }
491
- : {}),
492
- ...(args.contextWindow !== undefined ? { contextWindow: args.contextWindow } : {}),
493
- ...(args.maxTokens !== undefined ? { maxTokens: args.maxTokens } : {}),
494
- ...(args.reasoning !== undefined ? { reasoning: args.reasoning } : {}),
495
- ...(existing?.lmstudio ? { lmstudio: existing.lmstudio } : {}),
496
- ...(existing?.litellm ? { litellm: existing.litellm } : {}),
497
- });
527
+ ...(args.contextWindow !== undefined ? { contextWindow: args.contextWindow } : {}),
528
+ ...(args.maxTokens !== undefined ? { maxTokens: args.maxTokens } : {}),
529
+ ...(args.reasoning !== undefined ? { reasoning: args.reasoning } : {}),
530
+ ...(existing?.lmstudio ? { lmstudio: existing.lmstudio } : {}),
531
+ ...(existing?.litellm ? { litellm: existing.litellm } : {}),
532
+ }),
533
+ };
534
+ seed.capabilities = { ...existing?.capabilities, ...seed.capabilities };
535
+ if (seed.auth) seed.auth = { ...existing?.auth, ...seed.auth };
498
536
  const inventory = await resolveSupportedWireModels(runtime, seed, existing, args.apiKey);
499
537
  const wireModels = inventory.models;
500
538
  const model =
@@ -508,24 +546,29 @@ async function runNonInteractive(runtime: RuntimeDescriptor, args: ParsedArgs):
508
546
  }
509
547
  if (!validateResolvedModel(runtime, seed, model, args.force, inventory)) return 2;
510
548
  if (!validateContextWindowOverride(runtime, model, args.contextWindow, args.force)) return 2;
511
- const descriptor = buildDescriptor(runtime, args.id, {
512
- ...(url !== undefined ? { url } : {}),
513
- ...(model ? { model } : {}),
514
- ...(apiKeyEnv !== undefined ? { apiKeyEnv } : {}),
515
- ...(apiKeyRef !== undefined ? { apiKeyRef } : {}),
516
- ...(oauthProfile !== undefined ? { oauthProfile } : {}),
517
- gateway: args.gateway || existing?.gateway === true,
518
- ...(args.lifecycle !== undefined
519
- ? { lifecycle: args.lifecycle }
520
- : existing?.lifecycle !== undefined
521
- ? { lifecycle: existing.lifecycle }
522
- : {}),
523
- ...(args.contextWindow !== undefined ? { contextWindow: args.contextWindow } : {}),
524
- ...(args.maxTokens !== undefined ? { maxTokens: args.maxTokens } : {}),
525
- ...(args.reasoning !== undefined ? { reasoning: args.reasoning } : {}),
526
- ...(existing?.lmstudio ? { lmstudio: existing.lmstudio } : {}),
527
- ...(existing?.litellm ? { litellm: existing.litellm } : {}),
528
- });
549
+ const descriptor: TargetDescriptor = {
550
+ ...existing,
551
+ ...buildDescriptor(runtime, args.id, {
552
+ ...(url !== undefined ? { url } : {}),
553
+ ...(model ? { model } : {}),
554
+ ...(apiKeyEnv !== undefined ? { apiKeyEnv } : {}),
555
+ ...(apiKeyRef !== undefined ? { apiKeyRef } : {}),
556
+ ...(oauthProfile !== undefined ? { oauthProfile } : {}),
557
+ gateway: args.gateway || existing?.gateway === true,
558
+ ...(args.lifecycle !== undefined
559
+ ? { lifecycle: args.lifecycle }
560
+ : existing?.lifecycle !== undefined
561
+ ? { lifecycle: existing.lifecycle }
562
+ : {}),
563
+ ...(args.contextWindow !== undefined ? { contextWindow: args.contextWindow } : {}),
564
+ ...(args.maxTokens !== undefined ? { maxTokens: args.maxTokens } : {}),
565
+ ...(args.reasoning !== undefined ? { reasoning: args.reasoning } : {}),
566
+ ...(existing?.lmstudio ? { lmstudio: existing.lmstudio } : {}),
567
+ ...(existing?.litellm ? { litellm: existing.litellm } : {}),
568
+ }),
569
+ };
570
+ descriptor.capabilities = { ...existing?.capabilities, ...descriptor.capabilities };
571
+ if (descriptor.auth) descriptor.auth = { ...existing?.auth, ...descriptor.auth };
529
572
  const setOrchestrator = args.setOrchestrator || args.orchestratorModel !== undefined;
530
573
  const setBackground = args.setBackground || args.backgroundModel !== undefined;
531
574
  if ((setOrchestrator || setBackground) && !isOrchestratorEligibleRuntime(runtime)) {
@@ -540,8 +583,8 @@ async function runNonInteractive(runtime: RuntimeDescriptor, args: ParsedArgs):
540
583
  printError(error instanceof Error ? error.message : String(error));
541
584
  return 1;
542
585
  }
543
- if (args.apiKey) {
544
- auth.setApiKey(runtime.id, args.apiKey);
586
+ if (args.apiKey && apiKeyRef) {
587
+ auth.setApiKey(apiKeyRef, args.apiKey);
545
588
  // Bailing here leaves nothing half-done: settings are not touched until
546
589
  // applyConfiguration below, so a refused credential write means the whole
547
590
  // command changed nothing, which is what the non-zero exit now claims.
@@ -572,14 +615,16 @@ async function runNonInteractive(runtime: RuntimeDescriptor, args: ParsedArgs):
572
615
  // concurrent session's field-level write-through is never lost.
573
616
  applyConfiguration(settings);
574
617
  updateSettings(applyConfiguration);
575
- const probe = await runtimeProbe(runtime, descriptor);
618
+ const probe = await runtimeProbe(runtime, descriptor, args.apiKey);
576
619
  if (probe?.ok && probe.models) {
577
620
  recordTargetModelSnapshot(descriptor, probe.models, probe.modelLabels ? { modelLabels: probe.modelLabels } : {});
578
621
  }
579
622
  printSummary(settings, descriptor, probe);
580
623
  if (
581
624
  runtime.auth === "oauth" &&
582
- !auth.statusForTarget(resolveRuntimeAuthTarget(runtime), { includeFallback: false }).available
625
+ !auth.statusForTarget(existing ? resolveAuthTarget(existing, runtime) : resolveRuntimeAuthTarget(runtime), {
626
+ includeFallback: false,
627
+ }).available
583
628
  ) {
584
629
  process.stdout.write(
585
630
  `note: authenticate ${runtime.id} with \`clio-coder auth login ${runtime.id}\` before using this target\n`,
@@ -589,14 +634,14 @@ async function runNonInteractive(runtime: RuntimeDescriptor, args: ParsedArgs):
589
634
  return 0;
590
635
  }
591
636
 
592
- async function pickRuntime(rl: ReturnType<typeof createInterface>): Promise<RuntimeDescriptor | null> {
637
+ async function pickRuntime(rl: ConfigurePrompts): Promise<RuntimeDescriptor | null> {
593
638
  const registry = getRuntimeRegistry();
594
639
  const entries = listProviderSupportEntries(registry.list());
595
640
  return pickRuntimeFromEntries(rl, entries, "\nSupported runtimes:");
596
641
  }
597
642
 
598
643
  async function pickRuntimeFromEntries(
599
- rl: ReturnType<typeof createInterface>,
644
+ rl: ConfigurePrompts,
600
645
  entries: ReadonlyArray<ProviderSupportEntry>,
601
646
  heading: string,
602
647
  ): Promise<RuntimeDescriptor | null> {
@@ -646,7 +691,7 @@ async function pickRuntimeFromEntries(
646
691
  }
647
692
  }
648
693
 
649
- async function pickCategory(rl: ReturnType<typeof createInterface>): Promise<ConfigureCategory | null> {
694
+ async function pickCategory(rl: ConfigurePrompts): Promise<ConfigureCategory | null> {
650
695
  process.stdout.write("\nHow will you connect Clio to a model?\n\n");
651
696
  for (const line of formatCategoryMenu(terminalColumns())) process.stdout.write(`${line}\n`);
652
697
  for (;;) {
@@ -659,7 +704,7 @@ async function pickCategory(rl: ReturnType<typeof createInterface>): Promise<Con
659
704
  }
660
705
  }
661
706
 
662
- async function pickRuntimeViaCategory(rl: ReturnType<typeof createInterface>): Promise<RuntimeDescriptor | null> {
707
+ async function pickRuntimeViaCategory(rl: ConfigurePrompts): Promise<RuntimeDescriptor | null> {
663
708
  const registry = getRuntimeRegistry();
664
709
  const allEntries = listProviderSupportEntries(registry.list());
665
710
  const category = await pickCategory(rl);
@@ -697,7 +742,7 @@ async function pickRuntimeViaCategory(rl: ReturnType<typeof createInterface>): P
697
742
  }
698
743
 
699
744
  async function maybeSteerToNativeRuntime(
700
- rl: ReturnType<typeof createInterface>,
745
+ rl: ConfigurePrompts,
701
746
  currentRuntime: RuntimeDescriptor,
702
747
  url: string,
703
748
  ): Promise<RuntimeDescriptor> {
@@ -714,7 +759,7 @@ async function maybeSteerToNativeRuntime(
714
759
  }
715
760
 
716
761
  async function runTargetSetupInteractive(
717
- rl: ReturnType<typeof createInterface>,
762
+ rl: ConfigurePrompts,
718
763
  runtime: RuntimeDescriptor,
719
764
  defaults: ParsedArgs,
720
765
  ): Promise<number> {
@@ -723,12 +768,9 @@ async function runTargetSetupInteractive(
723
768
  let support = buildProviderSupportEntry(runtime);
724
769
  const initialRuntimeId = runtime.id;
725
770
  const existingForRuntime = configuredTargetsForRuntime(settings, initialRuntimeId);
726
- const existing =
727
- (defaults.id
728
- ? settings.targets.find((entry) => entry.id === defaults.id && entry.runtime === initialRuntimeId)
729
- : null) ??
730
- existingForRuntime[0] ??
731
- null;
771
+ const existing = defaults.id
772
+ ? (settings.targets.find((entry) => entry.id === defaults.id && entry.runtime === initialRuntimeId) ?? null)
773
+ : null;
732
774
  if (existingForRuntime.length > 0) {
733
775
  process.stdout.write(`\nExisting targets for ${runtime.id}:\n`);
734
776
  for (const target of existingForRuntime) {
@@ -736,7 +778,7 @@ async function runTargetSetupInteractive(
736
778
  }
737
779
  }
738
780
  process.stdout.write(`\nSelected runtime: ${runtime.id} (${support.summary})\n`);
739
- process.stdout.write(`Credentials: ${describeAuthStatus(runtime)}\n`);
781
+ process.stdout.write(`Credentials: ${describeAuthStatus(runtime, existing ?? undefined)}\n`);
740
782
  if (support.modelHints.length > 0) {
741
783
  process.stdout.write(`Known models: ${describeRuntimeModels(support, 4)}\n`);
742
784
  }
@@ -785,7 +827,10 @@ async function runTargetSetupInteractive(
785
827
  let apiKeyRef: string | undefined = existing?.auth?.apiKeyRef;
786
828
  let oauthProfile: string | undefined =
787
829
  runtime.auth === "oauth" ? (existing?.auth?.oauthProfile ?? runtime.oauthProviderId ?? runtime.id) : undefined;
788
- const authStatus = auth.statusForTarget(resolveRuntimeAuthTarget(runtime), { includeFallback: false });
830
+ const authStatus = auth.statusForTarget(
831
+ existing ? resolveAuthTarget(existing, runtime) : resolveRuntimeAuthTarget(runtime),
832
+ { includeFallback: false },
833
+ );
789
834
  if (runtime.auth === "api-key") {
790
835
  // Default to the env-var path, which keeps the key off disk. A target that
791
836
  // needs no key at all defaults to skip instead: a local llama.cpp server
@@ -793,7 +838,7 @@ async function runTargetSetupInteractive(
793
838
  // `Env var name:` with nothing correct to type into it.
794
839
  const needsCredential = targetRequiresAuth({ id: targetId, runtime: runtime.id }, runtime);
795
840
  const defaultSource =
796
- authStatus.source === "stored-api-key"
841
+ existing?.auth?.apiKeyRef || authStatus.source === "stored-api-key"
797
842
  ? "keep"
798
843
  : authStatus.source === "environment" || existing?.auth?.apiKeyEnvVar || needsCredential
799
844
  ? "env"
@@ -806,10 +851,10 @@ async function runTargetSetupInteractive(
806
851
  if (choice === null) return 0;
807
852
  const normalized = choice.trim().toLowerCase();
808
853
  if (normalized === "stored") {
809
- const literal = await ask(rl, "API key literal (stored in credentials.yaml, mode 0600)");
854
+ const literal = await rl.text("API key literal (stored in credentials.yaml, mode 0600)", "", true);
810
855
  if (literal !== null && literal.length > 0) {
811
856
  apiKeyLiteral = literal;
812
- apiKeyRef = runtime.id;
857
+ apiKeyRef = targetApiKeyRef(targetId, settings.targets);
813
858
  }
814
859
  } else if (normalized === "env") {
815
860
  const envDefault = defaults.apiKeyEnv ?? existing?.auth?.apiKeyEnvVar ?? runtime.credentialsEnvVar ?? "";
@@ -834,7 +879,7 @@ async function runTargetSetupInteractive(
834
879
  const connected = await loginOAuthRuntime(rl, runtime);
835
880
  if (!connected) return 1;
836
881
  }
837
- oauthProfile = runtime.oauthProviderId ?? runtime.id;
882
+ oauthProfile = existing?.auth?.oauthProfile ?? runtime.oauthProviderId ?? runtime.id;
838
883
  }
839
884
 
840
885
  let model: string | undefined = defaults.model;
@@ -857,8 +902,9 @@ async function runTargetSetupInteractive(
857
902
  ...(existing?.litellm ? { litellm: existing.litellm } : {}),
858
903
  });
859
904
 
905
+ if (existing?.auth?.headers) tentative.auth = { ...tentative.auth, headers: existing.auth.headers };
860
906
  if (runtime.kind === "http" || runtime.externalAgentLoop?.modelCatalog === "live-authoritative") {
861
- inventory = await resolveSupportedWireModels(runtime, tentative, existing ?? undefined);
907
+ inventory = await resolveSupportedWireModels(runtime, tentative, existing ?? undefined, apiKeyLiteral);
862
908
  }
863
909
  const wireModels = inventory.models;
864
910
  // The wizard shows the whole list, so a catalog-ordered runtime does not need
@@ -934,7 +980,7 @@ async function runTargetSetupInteractive(
934
980
  const gatewayDefault = defaults.gateway || existing?.gateway === true;
935
981
  const gatewayAnswer = gatewayDefault ? true : await askYesNo(rl, "Mark as gateway?", false);
936
982
 
937
- const descriptor = buildDescriptor(runtime, targetId, {
983
+ const configured = buildDescriptor(runtime, targetId, {
938
984
  ...(url !== undefined ? { url } : {}),
939
985
  ...(model !== undefined ? { model } : {}),
940
986
  ...(apiKeyEnv !== undefined ? { apiKeyEnv } : {}),
@@ -952,22 +998,20 @@ async function runTargetSetupInteractive(
952
998
  ...(existing?.lmstudio ? { lmstudio: existing.lmstudio } : {}),
953
999
  ...(existing?.litellm ? { litellm: existing.litellm } : {}),
954
1000
  });
1001
+ const descriptor: TargetDescriptor = {
1002
+ ...existing,
1003
+ ...configured,
1004
+ capabilities: { ...existing?.capabilities, ...configured.capabilities },
1005
+ };
1006
+ if (existing?.auth?.headers) descriptor.auth = { ...configured.auth, headers: existing.auth.headers };
1007
+ else if (!configured.auth) delete descriptor.auth;
955
1008
  try {
956
1009
  assertOrchestratorReplacementEligible(settings, descriptor);
957
1010
  } catch (error) {
958
1011
  printError(error instanceof Error ? error.message : String(error));
959
1012
  return 1;
960
1013
  }
961
- if (apiKeyLiteral) {
962
- auth.setApiKey(runtime.id, apiKeyLiteral);
963
- // As in the non-interactive path: the settings write is still ahead of us,
964
- // so refusing here leaves the whole wizard run without an effect.
965
- if (credentialWriteFailed(auth, `credential for ${runtime.id} was not stored; target '${descriptor.id}' not saved`))
966
- return 1;
967
- printPlaintextCredentialWarning();
968
- }
969
-
970
- const probe = await runtimeProbe(runtime, descriptor);
1014
+ const probe = await runtimeProbe(runtime, descriptor, apiKeyLiteral);
971
1015
  if (probe?.ok && probe.models) recordTargetModelSnapshot(descriptor, probe.models);
972
1016
  if (probe) {
973
1017
  process.stdout.write("\n");
@@ -1025,6 +1069,15 @@ async function runTargetSetupInteractive(
1025
1069
  : undefined;
1026
1070
  if (backgroundModel === null) return 0;
1027
1071
 
1072
+ if (apiKeyLiteral && apiKeyRef) {
1073
+ auth.setApiKey(apiKeyRef, apiKeyLiteral);
1074
+ // As in the non-interactive path: the settings write is still ahead of us,
1075
+ // so refusing here leaves the whole wizard run without an effect.
1076
+ if (credentialWriteFailed(auth, `credential for ${runtime.id} was not stored; target '${descriptor.id}' not saved`))
1077
+ return 1;
1078
+ printPlaintextCredentialWarning();
1079
+ }
1080
+
1028
1081
  const applyWizardChoice = (target: ClioSettings): void => {
1029
1082
  applyTarget(target, descriptor);
1030
1083
  if (setOrchestrator) setOrchestratorPointer(target, descriptor, orchestratorModel);
@@ -1076,11 +1129,16 @@ export function runTargetRename(oldId: string, newId: string): number {
1076
1129
  for (const profile of Object.values(settings.fleet.profiles)) {
1077
1130
  if (profile.target === oldId) profile.target = newId;
1078
1131
  }
1079
- settings.chat.modelPicker.cycleSet = settings.chat.modelPicker.cycleSet.map((entry) => {
1132
+ for (const roster of Object.values(settings.fleet.rosters)) {
1133
+ for (const member of roster.members) if (member.target === oldId) member.target = newId;
1134
+ }
1135
+ const renameRef = (entry: string): string => {
1080
1136
  const [head, ...rest] = entry.split("/");
1081
1137
  if (head !== oldId) return entry;
1082
1138
  return rest.length === 0 ? newId : `${newId}/${rest.join("/")}`;
1083
- });
1139
+ };
1140
+ settings.chat.modelPicker.cycleSet = settings.chat.modelPicker.cycleSet.map(renameRef);
1141
+ settings.chat.modelPicker.favorites = settings.chat.modelPicker.favorites.map(renameRef);
1084
1142
  });
1085
1143
  printOk(`renamed ${oldId} to ${newId}`);
1086
1144
  return 0;
@@ -1099,12 +1157,13 @@ export function runTargetRename(oldId: string, newId: string): number {
1099
1157
  */
1100
1158
 
1101
1159
  interface SectionIo {
1102
- rl: ReturnType<typeof createInterface>;
1160
+ rl: ConfigurePrompts;
1103
1161
  out: NodeJS.WritableStream;
1104
1162
  /** Both ends of this run, for an action that opens a picker of its own. */
1105
1163
  streams: ConfigureStreams;
1106
1164
  /** Confirm one change on the transcript, in the presenter's voice. */
1107
1165
  ok: (text: string) => void;
1166
+ warn: (text: string) => void;
1108
1167
  }
1109
1168
 
1110
1169
  interface SectionAction {
@@ -1141,11 +1200,11 @@ async function askInteger(
1141
1200
  const answer = await ask(io.rl, label, String(current));
1142
1201
  if (answer === null) return;
1143
1202
  const parsed = Number(answer);
1144
- if (!Number.isFinite(parsed) || parsed < min) {
1145
- io.out.write(` ${label} must be a number of at least ${min}; left at ${current}\n`);
1203
+ if (!Number.isSafeInteger(parsed) || parsed < min) {
1204
+ io.warn(`${label} must be a whole number of at least ${min}; left at ${current}\n`);
1146
1205
  return;
1147
1206
  }
1148
- const value = Math.floor(parsed);
1207
+ const value = parsed;
1149
1208
  apply(value);
1150
1209
  io.ok(`${label} set to ${value}`);
1151
1210
  }
@@ -1158,17 +1217,34 @@ async function askChoice(
1158
1217
  current: string,
1159
1218
  apply: (value: string) => void,
1160
1219
  ): Promise<void> {
1161
- const answer = await ask(io.rl, `${label} [${allowed.join("|")}]`, current);
1220
+ const answer = await io.rl.choose(label, allowed, current);
1162
1221
  if (answer === null) return;
1163
1222
  const value = answer.trim().toLowerCase();
1164
1223
  if (!allowed.includes(value)) {
1165
- io.out.write(` ${label} must be one of ${allowed.join(", ")}; left at ${current}\n`);
1224
+ io.warn(`${label} must be one of ${allowed.join(", ")}; left at ${current}\n`);
1166
1225
  return;
1167
1226
  }
1168
1227
  apply(value);
1169
1228
  io.ok(`${label} set to ${value}`);
1170
1229
  }
1171
1230
 
1231
+ async function runTargetWizard(io: SectionIo, options: Parameters<typeof runOnboardingWizard>[1]): Promise<void> {
1232
+ const before = readSettings().targets;
1233
+ io.rl.clearScreen();
1234
+ const code = await runOnboardingWizard(io.streams, options);
1235
+ if (code === 130) throw new ConfigureNavigation("quit");
1236
+ if (code !== 0) {
1237
+ // Keep the actual save error visible until the user has read it.
1238
+ await io.rl.text("Press Enter to return");
1239
+ io.warn("Target settings were not saved.");
1240
+ return;
1241
+ }
1242
+ const changed = readSettings().targets.find(
1243
+ (target) => JSON.stringify(target) !== JSON.stringify(before.find((entry) => entry.id === target.id)),
1244
+ );
1245
+ if (changed) io.ok(`Target ${changed.id} saved`);
1246
+ }
1247
+
1172
1248
  const SECTIONS: ReadonlyArray<SectionSpec> = [
1173
1249
  {
1174
1250
  id: "targets",
@@ -1201,6 +1277,10 @@ const SECTIONS: ReadonlyArray<SectionSpec> = [
1201
1277
  label: "Add a target",
1202
1278
  hint: "register a provider endpoint",
1203
1279
  run: async (io) => {
1280
+ if (canRunOnboarding(io.streams)) {
1281
+ await runTargetWizard(io, { mode: readSettings().targets.length === 0 ? "first" : "add" });
1282
+ return;
1283
+ }
1204
1284
  const runtime = await pickRuntimeViaCategory(io.rl);
1205
1285
  if (runtime) await runTargetSetupInteractive(io.rl, runtime, emptyArgs());
1206
1286
  },
@@ -1225,17 +1305,59 @@ const SECTIONS: ReadonlyArray<SectionSpec> = [
1225
1305
  const settings = readSettings();
1226
1306
  const ids = settings.targets.map((target) => target.id);
1227
1307
  if (ids.length === 0) {
1228
- io.out.write(" No targets to remove.\n");
1308
+ io.warn("No targets to remove.");
1229
1309
  return;
1230
1310
  }
1231
1311
  io.out.write(` Registered: ${ids.join(", ")}\n`);
1232
- const chosen = await ask(io.rl, "Target id to remove");
1312
+ const chosen = await io.rl.choose("Target to remove", ids, ids[0] ?? "");
1233
1313
  if (chosen === null || !ids.includes(chosen)) return;
1234
1314
  if (await askYesNo(io.rl, `Remove target '${chosen}'?`, false)) {
1235
- runTargetRemove(chosen);
1315
+ if (runTargetRemove(chosen) === 0) io.ok(`Removed target ${chosen}`);
1316
+ else io.warn(`Could not remove target ${chosen}`);
1317
+ }
1318
+ },
1319
+ },
1320
+ {
1321
+ label: "Edit a target",
1322
+ hint: "endpoint, credentials, model, capabilities",
1323
+ run: async (io) => {
1324
+ const targets = readSettings().targets;
1325
+ if (targets.length === 0) return;
1326
+ const id = await io.rl.choose(
1327
+ "Target to edit",
1328
+ targets.map((target) => target.id),
1329
+ targets[0]?.id ?? "",
1330
+ );
1331
+ const target = targets.find((entry) => entry.id === id);
1332
+ if (!target) return;
1333
+ if (canRunOnboarding(io.streams)) {
1334
+ await runTargetWizard(io, { mode: "edit", target });
1335
+ } else {
1336
+ const runtime = getRuntimeRegistry().get(target.runtime);
1337
+ if (runtime) await runTargetSetupInteractive(io.rl, runtime, { ...emptyArgs(), id });
1338
+ }
1339
+ },
1340
+ },
1341
+ {
1342
+ label: "Rename a target",
1343
+ run: async (io) => {
1344
+ const ids = readSettings().targets.map((target) => target.id);
1345
+ if (ids.length === 0) return;
1346
+ const id = await io.rl.choose("Target to rename", ids, ids[0] ?? "");
1347
+ const name = await ask(io.rl, "New target id", id);
1348
+ if (!name || name === id) return;
1349
+ if (ids.includes(name)) {
1350
+ io.warn(`Target id already exists: ${name}`);
1351
+ return;
1236
1352
  }
1353
+ if (runTargetRename(id, name) === 0) io.ok(`Renamed ${id} to ${name}`);
1354
+ else io.warn(`Could not rename target ${id}`);
1237
1355
  },
1238
1356
  },
1357
+ {
1358
+ label: "Set the background memory target",
1359
+ run: async (io) => assignTarget(io, "memory"),
1360
+ },
1239
1361
  ],
1240
1362
  },
1241
1363
  {
@@ -1292,8 +1414,12 @@ const SECTIONS: ReadonlyArray<SectionSpec> = [
1292
1414
  readSettings().chat.model ?? "",
1293
1415
  );
1294
1416
  if (model === null) return;
1295
- updateSettings((draft) => {
1296
- draft.chat.model = model.length > 0 ? model : null;
1417
+ // Keep null in the saved document. The effective reader resolves it
1418
+ // to the target model, so writing that normalized result would pin it again.
1419
+ updateSavedSettingsDocument((saved) => {
1420
+ const document = saved as { chat?: Record<string, unknown> };
1421
+ document.chat = { ...document.chat, model: model.length > 0 ? model : null };
1422
+ return document;
1297
1423
  });
1298
1424
  io.ok(model.length > 0 ? `Chat default model set to ${model}` : "Chat default model cleared");
1299
1425
  },
@@ -1451,7 +1577,7 @@ const SECTIONS: ReadonlyArray<SectionSpec> = [
1451
1577
  }
1452
1578
  const parsed = Number(answer);
1453
1579
  if (!Number.isFinite(parsed) || parsed < 1) {
1454
- io.out.write(` Concurrency limit must be auto or at least 1; left at ${current}\n`);
1580
+ io.warn(`Concurrency limit must be auto or at least 1; left at ${current}\n`);
1455
1581
  return;
1456
1582
  }
1457
1583
  updateSettings((draft) => {
@@ -1495,7 +1621,7 @@ const SECTIONS: ReadonlyArray<SectionSpec> = [
1495
1621
  if (answer === null) return;
1496
1622
  const parsed = Number(answer);
1497
1623
  if (!Number.isFinite(parsed) || parsed < 1) {
1498
- io.out.write(` Run timeout must be at least 1 second; left at ${current}s\n`);
1624
+ io.warn(`Run timeout must be at least 1 second; left at ${current}s\n`);
1499
1625
  return;
1500
1626
  }
1501
1627
  updateSettings((draft) => {
@@ -1524,22 +1650,16 @@ const SECTIONS: ReadonlyArray<SectionSpec> = [
1524
1650
  actions: [
1525
1651
  {
1526
1652
  label: "Autonomy level",
1527
- hint: "interactive | assisted | auto-edit | full",
1653
+ hint: AUTONOMY_LEVELS.join(" | "),
1528
1654
  run: async (io) => {
1529
1655
  io.out.write(
1530
- " interactive prompts for everything, assisted auto-reads and confirms writes,\n auto-edit confirms only bash, full runs unattended.\n",
1531
- );
1532
- await askChoice(
1533
- io,
1534
- "Autonomy level",
1535
- ["interactive", "assisted", "auto-edit", "full"],
1536
- readSettings().safety.autonomy,
1537
- (value) => {
1538
- updateSettings((draft) => {
1539
- draft.safety.autonomy = value as AutonomyLevel;
1540
- });
1541
- },
1656
+ " read-only inspects; suggest asks before changes; auto-edit permits workspace\n edits; full-auto skips autonomy prompts. Safety rules still apply at every level.\n",
1542
1657
  );
1658
+ await askChoice(io, "Autonomy level", AUTONOMY_LEVELS, readSettings().safety.autonomy, (value) => {
1659
+ updateSettings((draft) => {
1660
+ draft.safety.autonomy = value as AutonomyLevel;
1661
+ });
1662
+ });
1543
1663
  },
1544
1664
  },
1545
1665
  {
@@ -1568,7 +1688,7 @@ const SECTIONS: ReadonlyArray<SectionSpec> = [
1568
1688
  if (answer === null) return;
1569
1689
  const parsed = Number(answer);
1570
1690
  if (!Number.isFinite(parsed) || parsed <= 0) {
1571
- io.out.write(` Session cost limit must be greater than 0; left at $${current}\n`);
1691
+ io.warn(`Session cost limit must be greater than 0; left at $${current}\n`);
1572
1692
  return;
1573
1693
  }
1574
1694
  updateSettings((draft) => {
@@ -1618,7 +1738,7 @@ const SECTIONS: ReadonlyArray<SectionSpec> = [
1618
1738
  ["Panes capability", settings.interface.panes.enabled],
1619
1739
  ["Startup layout", settings.interface.panes.layout],
1620
1740
  ["TUI mode", settings.interface.mode],
1621
- ["Output detail", settings.interface.outputDetail],
1741
+ ["Output style", settings.interface.outputDetail],
1622
1742
  ["Desktop notifications", onOff(settings.interface.desktopNotifications)],
1623
1743
  ["Git commit attribution", onOff(settings.integrations.git.commitAttribution)],
1624
1744
  ];
@@ -1670,17 +1790,17 @@ const SECTIONS: ReadonlyArray<SectionSpec> = [
1670
1790
  },
1671
1791
  },
1672
1792
  {
1673
- label: "Output detail",
1674
- hint: "minimal | default | verbose",
1793
+ label: "Output style",
1794
+ hint: "compact | standard | detailed",
1675
1795
  run: async (io) => {
1676
1796
  await askChoice(
1677
1797
  io,
1678
- "Output detail",
1679
- ["minimal", "default", "verbose"],
1798
+ "Output style",
1799
+ ["compact", "standard", "detailed"],
1680
1800
  readSettings().interface.outputDetail,
1681
1801
  (value) => {
1682
1802
  updateSettings((draft) => {
1683
- draft.interface.outputDetail = value as OutputVerbosity;
1803
+ draft.interface.outputDetail = value as OutputStyle;
1684
1804
  });
1685
1805
  },
1686
1806
  );
@@ -1789,9 +1909,10 @@ const SECTIONS: ReadonlyArray<SectionSpec> = [
1789
1909
  {
1790
1910
  label: "Run doctor",
1791
1911
  hint: "read-only health check",
1792
- run: async () => {
1912
+ run: async (io) => {
1793
1913
  const { runDoctorCommand } = await import("./doctor.js");
1794
1914
  await runDoctorCommand([]);
1915
+ await io.rl.text("Press Enter to return");
1795
1916
  },
1796
1917
  },
1797
1918
  {
@@ -1803,6 +1924,26 @@ const SECTIONS: ReadonlyArray<SectionSpec> = [
1803
1924
  return;
1804
1925
  }
1805
1926
  io.out.write(`\n--- ${shortenPath(file)} ---\n${readFileSync(file, "utf8")}--- end ---\n`);
1927
+ await io.rl.text("Press Enter to return");
1928
+ },
1929
+ },
1930
+ ],
1931
+ },
1932
+ {
1933
+ id: "advanced",
1934
+ title: "All Settings",
1935
+ summary: "validated editor for every setting",
1936
+ aliases: ["all", "settings"],
1937
+ fields: () => [
1938
+ ["Settings file", shortenPath(settingsPath())],
1939
+ ["Reference", "clio-coder docs configuration-reference"],
1940
+ ],
1941
+ actions: [
1942
+ {
1943
+ label: "Edit all settings",
1944
+ hint: "VISUAL / EDITOR; validate before saving",
1945
+ run: async (io) => {
1946
+ if (await editSettings(io.rl)) io.ok("Settings saved; previous file kept at settings.yaml.bak");
1806
1947
  },
1807
1948
  },
1808
1949
  ],
@@ -1818,6 +1959,9 @@ function emptyArgs(): ParsedArgs {
1818
1959
  all: false,
1819
1960
  interop: false,
1820
1961
  json: false,
1962
+ edit: false,
1963
+ quick: false,
1964
+ settings: false,
1821
1965
  force: false,
1822
1966
  gateway: false,
1823
1967
  setOrchestrator: false,
@@ -1842,34 +1986,48 @@ async function askList(
1842
1986
  io.ok(values.length === 0 ? `${label} cleared` : `${label} set to ${values.join(", ")}`);
1843
1987
  }
1844
1988
 
1845
- async function assignTarget(io: SectionIo, role: "chat" | "fleet"): Promise<void> {
1989
+ async function assignTarget(io: SectionIo, role: "chat" | "fleet" | "memory"): Promise<void> {
1846
1990
  const settings = readSettings();
1847
- const ids = settings.targets.map((target) => target.id);
1991
+ const ids = settings.targets
1992
+ .filter((target) => {
1993
+ const runtime = getRuntimeRegistry().get(target.runtime);
1994
+ return role === "fleet" || (runtime !== null && isOrchestratorEligibleRuntime(runtime));
1995
+ })
1996
+ .map((target) => target.id);
1848
1997
  if (ids.length === 0) {
1849
- io.out.write(" No targets registered yet. Add one first.\n");
1998
+ io.warn("No eligible targets registered yet. Add one first.");
1850
1999
  return;
1851
2000
  }
1852
2001
  io.out.write(` Registered: ${ids.join(", ")}\n`);
1853
- const currentTarget = role === "chat" ? settings.chat.target : settings.fleet.default.target;
1854
- const chosen = await ask(io.rl, `${role === "chat" ? "Chat" : "Fleet"} target id`, currentTarget ?? ids[0] ?? "");
2002
+ const currentTarget =
2003
+ role === "chat"
2004
+ ? settings.chat.target
2005
+ : role === "fleet"
2006
+ ? settings.fleet.default.target
2007
+ : settings.context.memory.target;
2008
+ const label = role === "chat" ? "Chat" : role === "fleet" ? "Fleet" : "Background memory";
2009
+ const chosen = await io.rl.choose(`${label} target`, ids, currentTarget ?? ids[0] ?? "");
1855
2010
  if (chosen === null) return;
1856
2011
  if (!ids.includes(chosen)) {
1857
- io.out.write(` ${chosen} is not a registered target; nothing changed.\n`);
2012
+ io.warn(`${chosen} is not a registered target; nothing changed.`);
1858
2013
  return;
1859
2014
  }
1860
2015
  const target = settings.targets.find((entry) => entry.id === chosen);
1861
- const model = await ask(io.rl, "Model (blank for the target default)", target?.defaultModel ?? "");
2016
+ const model = await ask(io.rl, `Model override (blank uses ${target?.defaultModel ?? "the target default"})`, "");
1862
2017
  if (model === null) return;
1863
2018
  updateSettings((draft) => {
1864
2019
  if (role === "chat") {
1865
2020
  draft.chat.target = chosen;
1866
2021
  draft.chat.model = model.length > 0 ? model : null;
1867
- } else {
2022
+ } else if (role === "fleet") {
1868
2023
  draft.fleet.default.target = chosen;
1869
2024
  draft.fleet.default.model = model.length > 0 ? model : null;
2025
+ } else {
2026
+ draft.context.memory.target = chosen;
2027
+ draft.context.memory.model = model.length > 0 ? model : null;
1870
2028
  }
1871
2029
  });
1872
- io.ok(`${role === "chat" ? "Chat" : "Fleet"} target set to ${chosen}`);
2030
+ io.ok(`${label} target set to ${chosen}`);
1873
2031
  }
1874
2032
 
1875
2033
  interface MenuEntry {
@@ -1886,15 +2044,18 @@ type MenuChoice = { kind: "index"; index: number } | { kind: "back" } | { kind:
1886
2044
  * of this CLI uses.
1887
2045
  */
1888
2046
  async function pickEntry(
1889
- rl: ReturnType<typeof createInterface>,
2047
+ rl: ConfigurePrompts,
1890
2048
  io: ConfigureStreams,
1891
2049
  entries: ReadonlyArray<MenuEntry>,
1892
2050
  backLabel: string,
1893
2051
  plain: boolean,
2052
+ initialIndex = 0,
1894
2053
  ): Promise<MenuChoice> {
1895
2054
  const out = io.out;
1896
2055
  if (canSelect(io.in as NodeJS.ReadStream, out as NodeJS.WriteStream)) {
1897
2056
  const result = await promptSelect({
2057
+ initialIndex,
2058
+ clearOnExit: true,
1898
2059
  choices: entries.map((entry, index) => ({
1899
2060
  value: index,
1900
2061
  label: entry.label,
@@ -1936,55 +2097,110 @@ function renderSection(spec: SectionSpec, out: NodeJS.WritableStream): Lifecycle
1936
2097
 
1937
2098
  /** Show one screen and stay on it until the operator leaves or quits. */
1938
2099
  async function runSection(
1939
- rl: ReturnType<typeof createInterface>,
2100
+ rl: ConfigurePrompts,
1940
2101
  streams: ConfigureStreams,
1941
2102
  spec: SectionSpec,
1942
2103
  backLabel: string,
1943
2104
  ): Promise<"back" | "quit"> {
2105
+ let selected = 0;
2106
+ let notice: { text: string; warning: boolean } | undefined;
1944
2107
  for (;;) {
2108
+ rl.clearScreen();
1945
2109
  const presenter = renderSection(spec, streams.out);
2110
+ if (notice) {
2111
+ if (notice.warning) presenter.warn(notice.text);
2112
+ else presenter.completedStep(notice.text);
2113
+ notice = undefined;
2114
+ }
1946
2115
  const io: SectionIo = {
1947
2116
  rl,
1948
2117
  out: streams.out,
1949
2118
  streams,
1950
2119
  ok: (text) => {
1951
- presenter.completedStep(text);
2120
+ notice = { text, warning: false };
2121
+ },
2122
+ warn: (text) => {
2123
+ notice = { text: text.trim(), warning: true };
1952
2124
  },
1953
2125
  };
1954
2126
  presenter.blank();
1955
- const choice = await pickEntry(rl, streams, spec.actions, backLabel, presenter.isPlain());
2127
+ const choice = await pickEntry(rl, streams, spec.actions, backLabel, presenter.isPlain(), selected);
1956
2128
  if (choice.kind !== "index") return choice.kind;
1957
- const action = spec.actions[choice.index];
1958
- if (action !== undefined) await action.run(io);
2129
+ selected = choice.index;
2130
+ const action = spec.actions[selected];
2131
+ try {
2132
+ if (action !== undefined) await action.run(io);
2133
+ } catch (error) {
2134
+ if (error instanceof ConfigureNavigation) {
2135
+ if (error.kind === "quit") return "quit";
2136
+ } else
2137
+ notice = { text: `Change not saved: ${error instanceof Error ? error.message : String(error)}`, warning: true };
2138
+ }
1959
2139
  }
1960
2140
  }
1961
2141
 
1962
- /** The top-level screen. Leaving it is an ordinary exit, not a cancellation. */
2142
+ /** Full settings, opened directly or one level below the launcher. */
1963
2143
  async function runConfigSectionsMenu(
1964
- rl: ReturnType<typeof createInterface>,
2144
+ rl: ConfigurePrompts,
1965
2145
  streams: ConfigureStreams,
1966
- ): Promise<number> {
2146
+ backLabel: "back" | "quit" = "quit",
2147
+ ): Promise<"back" | "quit"> {
2148
+ let selected = 0;
1967
2149
  for (;;) {
2150
+ rl.clearScreen();
1968
2151
  const presenter = createLifecyclePresenter({ stream: streams.out });
1969
- presenter.header("Clio Coder Configuration", "configure");
2152
+ presenter.header("Clio Coder Settings", "configure");
1970
2153
  presenter.note(`Source: ${shortenPath(settingsPath())}`);
1971
2154
  presenter.blank();
1972
2155
  const choice = await pickEntry(
1973
2156
  rl,
1974
2157
  streams,
1975
2158
  SECTIONS.map((section) => ({ label: section.title, hint: section.summary })),
1976
- "quit",
2159
+ backLabel,
1977
2160
  presenter.isPlain(),
2161
+ selected,
1978
2162
  );
1979
- // Escape and q both leave the top screen, and leaving a settings menu you
1980
- // only looked at is not a failure: this used to exit 130 and print
1981
- // "error: configuration cancelled" over a run that did exactly what was
1982
- // asked. The first-run wizard still exits 130, because there a cancel
1983
- // really does leave Clio unconfigured.
1984
- if (choice.kind !== "index") return 0;
1985
- const spec = SECTIONS[choice.index];
2163
+ if (choice.kind !== "index") return choice.kind;
2164
+ selected = choice.index;
2165
+ const spec = SECTIONS[selected];
1986
2166
  if (spec === undefined) continue;
1987
- if ((await runSection(rl, streams, spec, "back")) === "quit") return 0;
2167
+ if ((await runSection(rl, streams, spec, "back")) === "quit") return "quit";
2168
+ }
2169
+ }
2170
+
2171
+ async function runConfigLauncher(rl: ConfigurePrompts, streams: ConfigureStreams): Promise<number> {
2172
+ let selected = 0;
2173
+ const exitCode = () => (readSettings().chat.target ? 0 : 130);
2174
+ for (;;) {
2175
+ rl.clearScreen();
2176
+ const presenter = createLifecyclePresenter({ stream: streams.out });
2177
+ presenter.header("Clio Coder", "configure");
2178
+ presenter.note("Connect a model and start coding. Everything else is optional.");
2179
+ presenter.blank();
2180
+ const choice = await pickEntry(
2181
+ rl,
2182
+ streams,
2183
+ [
2184
+ { label: "Quick Connect", hint: "endpoint → model → ready" },
2185
+ { label: "Settings", hint: "all configuration options" },
2186
+ { label: "Diagnostics", hint: "check an existing installation" },
2187
+ ],
2188
+ "quit",
2189
+ presenter.isPlain(),
2190
+ selected,
2191
+ );
2192
+ if (choice.kind !== "index") return exitCode();
2193
+ selected = choice.index;
2194
+ if (selected === 0) {
2195
+ const result = await runQuickConnect(rl);
2196
+ if (result === "connected") return 0;
2197
+ if (result === "quit") return exitCode();
2198
+ } else if (selected === 1) {
2199
+ if ((await runConfigSectionsMenu(rl, streams, "back")) === "quit") return exitCode();
2200
+ } else {
2201
+ const section = findSection("diagnostics");
2202
+ if (section && (await runSection(rl, streams, section, "back")) === "quit") return exitCode();
2203
+ }
1988
2204
  }
1989
2205
  }
1990
2206
 
@@ -2003,12 +2219,28 @@ interface ConfigureStreams {
2003
2219
  }
2004
2220
 
2005
2221
  async function runInteractive(
2006
- rl: ReturnType<typeof createInterface>,
2222
+ rl: ConfigurePrompts,
2007
2223
  preselectedRuntime: RuntimeDescriptor | null,
2008
2224
  defaults: ParsedArgs,
2009
2225
  streams: ConfigureStreams,
2010
2226
  ): Promise<number> {
2227
+ if (defaults.quick) {
2228
+ if (!canRunOnboarding(streams)) {
2229
+ printError("--quick needs a terminal; use --id and --runtime for unattended setup");
2230
+ return 2;
2231
+ }
2232
+ return (await runQuickConnect(rl)) === "connected" ? 0 : 130;
2233
+ }
2234
+ if (defaults.settings) {
2235
+ await runConfigSectionsMenu(rl, streams);
2236
+ return 0;
2237
+ }
2011
2238
  if (preselectedRuntime) {
2239
+ if (canRunOnboarding(streams))
2240
+ return runOnboardingWizard(streams, {
2241
+ mode: readSettings().targets.length === 0 ? "first" : "add",
2242
+ runtime: preselectedRuntime,
2243
+ });
2012
2244
  return await runTargetSetupInteractive(rl, preselectedRuntime, defaults);
2013
2245
  }
2014
2246
 
@@ -2029,21 +2261,10 @@ async function runInteractive(
2029
2261
  await runSection(rl, streams, spec, "quit");
2030
2262
  return 0;
2031
2263
  }
2264
+ if (canRunOnboarding(streams)) return runConfigLauncher(rl, streams);
2032
2265
 
2033
2266
  if (readSettings().targets.length === 0) {
2034
- // The first run is its own screen flow. It only exists where a keypress can
2035
- // be read; a pipe, a recorded session, and a dumb terminal keep the
2036
- // numbered readline path below, which is still the only way to answer them.
2037
- //
2038
- // The readline interface is closed first. Left open it stays subscribed to
2039
- // stdin's keypresses and echoes each one, which puts a stray line under
2040
- // every menu the wizard draws and breaks the erase that replaces the menu
2041
- // with its answer. The wizard opens its own where a browser sign-in needs
2042
- // one.
2043
- if (canRunOnboarding(streams)) {
2044
- rl.close();
2045
- return await runOnboardingWizard(streams);
2046
- }
2267
+ // Dumb terminals retain the numbered setup path.
2047
2268
  const runtime = await pickRuntimeViaCategory(rl);
2048
2269
  if (!runtime) {
2049
2270
  printError("configuration cancelled");
@@ -2052,7 +2273,8 @@ async function runInteractive(
2052
2273
  return await runTargetSetupInteractive(rl, runtime, defaults);
2053
2274
  }
2054
2275
 
2055
- return await runConfigSectionsMenu(rl, streams);
2276
+ await runConfigSectionsMenu(rl, streams);
2277
+ return 0;
2056
2278
  }
2057
2279
 
2058
2280
  export async function runConfigureCommand(
@@ -2072,8 +2294,21 @@ export async function runConfigureCommand(
2072
2294
  process.stdout.write(HELP);
2073
2295
  return 0;
2074
2296
  }
2075
- initializeClioHome();
2076
2297
  ensureRegistryPopulated();
2298
+ if (args.edit) {
2299
+ if (!(inStream as { isTTY?: boolean }).isTTY || !(outStream as { isTTY?: boolean }).isTTY) {
2300
+ printError("--edit needs a terminal; use configure --json to inspect settings without one");
2301
+ return 2;
2302
+ }
2303
+ try {
2304
+ await editSettings(new ConfigurePrompts(inStream, outStream));
2305
+ return 0;
2306
+ } catch (error) {
2307
+ if (error instanceof ConfigureNavigation) return 0;
2308
+ printError(error instanceof Error ? error.message : String(error));
2309
+ return 1;
2310
+ }
2311
+ }
2077
2312
 
2078
2313
  if (args.json) {
2079
2314
  const settings = readSettings();
@@ -2086,13 +2321,9 @@ export async function runConfigureCommand(
2086
2321
  return 0;
2087
2322
  }
2088
2323
  if (args.interop) {
2089
- if (!input.isTTY) return runInteropReview({ rl: null });
2090
- const rl = createInterface({ input, output });
2091
- try {
2092
- return await runInteropReview({ rl, streams: { in: input, out: output } });
2093
- } finally {
2094
- rl.close();
2095
- }
2324
+ if (!(inStream as { isTTY?: boolean }).isTTY) return runInteropReview({ rl: null });
2325
+ const rl = new ConfigurePrompts(inStream, outStream);
2326
+ return runInteropReview({ rl, streams: { in: inStream, out: outStream } });
2096
2327
  }
2097
2328
  if (args.remove) return runTargetRemove(args.remove);
2098
2329
  if (args.renameOld && args.renameNew) return runTargetRename(args.renameOld, args.renameNew);
@@ -2128,6 +2359,11 @@ export async function runConfigureCommand(
2128
2359
  args.gateway ||
2129
2360
  args.lifecycle !== undefined ||
2130
2361
  args.setOrchestrator ||
2362
+ args.setBackground ||
2363
+ args.backgroundModel !== undefined ||
2364
+ args.orchestratorModel !== undefined ||
2365
+ args.workerModel !== undefined ||
2366
+ args.bindAgent !== undefined ||
2131
2367
  args.setWorkerDefault ||
2132
2368
  args.contextWindow !== undefined ||
2133
2369
  args.maxTokens !== undefined ||
@@ -2141,7 +2377,7 @@ export async function runConfigureCommand(
2141
2377
  // non-interactive stdin the wizard below would read EOF, take the default
2142
2378
  // answers, write the initial settings template, and exit 0 without
2143
2379
  // configuring the requested target. Reject with the missing half instead.
2144
- if ((runtimeId !== undefined || hasTargetSetupFlag) && !input.isTTY) {
2380
+ if ((runtimeId !== undefined || hasTargetSetupFlag) && !(inStream as { isTTY?: boolean }).isTTY) {
2145
2381
  printError(
2146
2382
  runtime === null
2147
2383
  ? "--runtime is required when configuring a target non-interactively"
@@ -2150,13 +2386,16 @@ export async function runConfigureCommand(
2150
2386
  return 2;
2151
2387
  }
2152
2388
 
2153
- const rl = createInterface({ input: inStream, output: outStream });
2389
+ const rl = new ConfigurePrompts(inStream, outStream);
2154
2390
  try {
2155
2391
  return await runInteractive(rl, runtime, args, { in: inStream, out: outStream });
2156
2392
  } catch (err) {
2393
+ if (err instanceof ConfigureNavigation) {
2394
+ if (readSettings().targets.length > 0) return 0;
2395
+ printError("configuration cancelled");
2396
+ return 130;
2397
+ }
2157
2398
  printError(err instanceof Error ? err.message : String(err));
2158
2399
  return 1;
2159
- } finally {
2160
- rl.close();
2161
2400
  }
2162
2401
  }