@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
@@ -0,0 +1,274 @@
1
+ import { readSettings, SettingsValidationError, updateSettings, validateSettings } from "../core/config.js";
2
+ import { initializeClioHome } from "../core/init.js";
3
+ import { openAuthStorage } from "../domains/providers/auth/index.js";
4
+ import { isOrchestratorEligibleRuntime, resolveAuthTarget } from "../domains/providers/index.js";
5
+ import { fingerprintNativeRuntime } from "../domains/providers/probe/fingerprint.js";
6
+ import { probeJson } from "../domains/providers/probe/http.js";
7
+ import { getRuntimeRegistry } from "../domains/providers/registry.js";
8
+ import { targetRootUrl } from "../domains/providers/runtimes/common/local-synth.js";
9
+ import type { ProbeContext, ProbeResult, RuntimeDescriptor } from "../domains/providers/types/runtime-descriptor.js";
10
+ import type { TargetDescriptor } from "../domains/providers/types/target-descriptor.js";
11
+ import { ConfigureNavigation, type ConfigurePrompts } from "./configure-prompts.js";
12
+ import {
13
+ applyTarget,
14
+ deriveTargetId,
15
+ setOrchestratorPointer,
16
+ setWorkerDefaultPointer,
17
+ targetApiKeyRef,
18
+ } from "./configure-target.js";
19
+ import { createLifecyclePresenter } from "./lifecycle-presenter.js";
20
+ import { credentialWriteFailed, printPlaintextCredentialWarning } from "./shared.js";
21
+
22
+ interface Connection {
23
+ runtime: RuntimeDescriptor;
24
+ target: TargetDescriptor;
25
+ existing: TargetDescriptor | undefined;
26
+ probe: ProbeResult;
27
+ models: string[];
28
+ }
29
+
30
+ function endpointUrl(value: string): string {
31
+ const url = new URL(value.includes("://") ? value : `http://${value}`);
32
+ if (!["http:", "https:"].includes(url.protocol) || !url.hostname)
33
+ throw new Error("Use an http:// or https:// endpoint URL.");
34
+ if (url.username || url.password)
35
+ throw new Error("Enter the endpoint without credentials; Clio asks for a key when needed.");
36
+ return url.toString().replace(/\/$/u, "");
37
+ }
38
+
39
+ /** Probe only the endpoint the user entered; new endpoints never inherit another provider's key. */
40
+ async function connect(url: string, key: string | undefined): Promise<Connection> {
41
+ const settings = readSettings();
42
+ const registry = getRuntimeRegistry();
43
+ const matches = settings.targets.filter((target) => {
44
+ const runtime = registry.get(target.runtime);
45
+ if (!target.url || !runtime || !isOrchestratorEligibleRuntime(runtime)) return false;
46
+ try {
47
+ return endpointUrl(target.url) === url;
48
+ } catch {
49
+ return false;
50
+ }
51
+ });
52
+ const existing = matches.find((target) => target.id === settings.chat.target) ?? matches[0];
53
+ let runtime = existing ? registry.get(existing.runtime) : null;
54
+ let token = key;
55
+ if (existing && runtime && token === undefined) {
56
+ const auth = await openAuthStorage().resolveForTarget(resolveAuthTarget(existing, runtime), {
57
+ includeFallback: false,
58
+ });
59
+ token = auth.apiKey;
60
+ }
61
+ const ctx: ProbeContext = { credentialsPresent: new Set(), httpTimeoutMs: 5000, authToken: token ?? "" };
62
+ if (!runtime) {
63
+ const root = targetRootUrl({ id: "quick", runtime: "openai-compat", url }) ?? url;
64
+ const headers: Record<string, string> = token ? { Authorization: `Bearer ${token}` } : {};
65
+ const native = await fingerprintNativeRuntime(root, headers);
66
+ let id = native?.runtimeId ?? "openai-compat";
67
+ if (!native) {
68
+ const info = await probeJson<{ data?: Array<{ model_name?: unknown; model_info?: unknown }> }>({
69
+ url: `${root}/v1/model/info`,
70
+ headers,
71
+ timeoutMs: 1000,
72
+ });
73
+ if (info.ok && info.data?.data?.some((row) => typeof row.model_name === "string" && row.model_info !== undefined))
74
+ id = "litellm";
75
+ }
76
+ runtime = registry.get(id);
77
+ }
78
+ if (!runtime) throw new Error("No compatible runtime is available. Open Settings → Targets & Auth.");
79
+ const target: TargetDescriptor = existing
80
+ ? structuredClone(existing)
81
+ : {
82
+ id: deriveTargetId(runtime.id, settings.targets),
83
+ runtime: runtime.id,
84
+ url,
85
+ };
86
+ let probe = (await runtime.probe?.(target, ctx)) ?? {
87
+ ok: false,
88
+ error: "This connection does not support endpoint checks.",
89
+ };
90
+ let models = probe.ok ? (probe.models ?? (await runtime.probeModels?.(target, ctx)) ?? []) : [];
91
+ // An Anthropic-only server can expose the same /v1/models path but require x-api-key.
92
+ if (!existing && runtime.id === "openai-compat" && !probe.ok && /HTTP (401|403|404)/u.test(probe.error ?? "")) {
93
+ const anthropic = registry.get("anthropic-compat");
94
+ if (anthropic) {
95
+ const candidate = { ...target, runtime: anthropic.id };
96
+ const catalog = (await anthropic.probeModels?.(candidate, ctx)) ?? [];
97
+ if (catalog.length > 0) {
98
+ runtime = anthropic;
99
+ target.runtime = anthropic.id;
100
+ target.id = deriveTargetId(anthropic.id, settings.targets);
101
+ models = catalog;
102
+ probe = { ok: true, models };
103
+ }
104
+ }
105
+ }
106
+ models = [...new Set(models)].filter((id) => probe.modelCapabilities?.[id]?.chat !== false);
107
+ return { runtime, target, existing, probe, models };
108
+ }
109
+
110
+ /** URL → key only if required → model only if several → Connect. No settings questionnaire. */
111
+ export async function runQuickConnect(prompts: ConfigurePrompts): Promise<"connected" | "back" | "quit"> {
112
+ const initial = readSettings();
113
+ const first = initial.targets.length === 0;
114
+ const current = initial.targets.find((target) => target.id === initial.chat.target);
115
+ let url = current?.url ?? "";
116
+ let key: string | undefined;
117
+ let connection: Connection | undefined;
118
+ let model = "";
119
+ let step: "url" | "key" | "model" | "save" = "url";
120
+ let askedKey = false;
121
+ let optionalKey = false;
122
+ let notice = "";
123
+ const presenter = createLifecyclePresenter({ stream: prompts.output });
124
+ for (;;) {
125
+ prompts.clearScreen();
126
+ presenter.header("Quick Connect", "configure");
127
+ presenter.note("Connect one model to power Clio. Change other settings whenever you need them.");
128
+ if (notice) {
129
+ presenter.warn(notice);
130
+ notice = "";
131
+ }
132
+ try {
133
+ if (step === "url") {
134
+ presenter.note("LM Studio: localhost:1234 · Ollama: localhost:11434");
135
+ const answer = await prompts.text("Endpoint URL", url);
136
+ if (!answer) {
137
+ notice = "Enter the endpoint URL shown by your model server or API provider.";
138
+ continue;
139
+ }
140
+ try {
141
+ url = endpointUrl(answer);
142
+ } catch (error) {
143
+ notice = error instanceof Error ? error.message : String(error);
144
+ continue;
145
+ }
146
+ key = undefined;
147
+ askedKey = false;
148
+ connection = undefined;
149
+ } else if (step === "key") {
150
+ presenter.note(`Endpoint: ${url}`);
151
+ if (optionalKey)
152
+ presenter.note("Some APIs publish models publicly but require a key for chat. Leave blank for a keyless server.");
153
+ key = (await prompts.text("API key (saved locally when you choose Connect)", "", true)) || undefined;
154
+ if (!key && !optionalKey) {
155
+ notice = "Enter a key, or press Escape to change the endpoint.";
156
+ continue;
157
+ }
158
+ askedKey = true;
159
+ }
160
+ if (step === "url" || step === "key") {
161
+ presenter.note("Checking the endpoint and reading its model list…");
162
+ connection = await connect(url, key);
163
+ if (!connection.probe.ok) {
164
+ if (connection.probe.authFailed || /HTTP (401|403)/u.test(connection.probe.error ?? "")) {
165
+ optionalKey = false;
166
+ notice = key
167
+ ? "The endpoint rejected that key. Try again, or Escape to go back."
168
+ : "This endpoint needs an API key.";
169
+ step = "key";
170
+ } else {
171
+ notice = `Could not connect: ${connection.probe.error ?? "no reply"}. Check the URL and that the server is running.`;
172
+ step = "url";
173
+ }
174
+ continue;
175
+ }
176
+ if (
177
+ !connection.existing &&
178
+ !askedKey &&
179
+ connection.runtime.auth === "api-key" &&
180
+ connection.runtime.tier !== "local-native"
181
+ ) {
182
+ optionalKey = true;
183
+ step = "key";
184
+ continue;
185
+ }
186
+ if (connection.models.length === 0) {
187
+ notice =
188
+ "The endpoint offered no chat models. Load a model and retry. Servers without model discovery can use Settings → Targets & Auth.";
189
+ step = "url";
190
+ continue;
191
+ }
192
+ const preferred =
193
+ connection.existing?.id === initial.chat.target ? initial.chat.model : connection.target.defaultModel;
194
+ model = preferred && connection.models.includes(preferred) ? preferred : (connection.models[0] ?? "");
195
+ step = connection.models.length === 1 ? "save" : "model";
196
+ continue;
197
+ }
198
+ if (!connection) {
199
+ step = "url";
200
+ continue;
201
+ }
202
+ if (step === "model") {
203
+ presenter.note(`Connected: ${url} (${connection.runtime.displayName})`);
204
+ model = await prompts.choose("Choose the model for Clio", connection.models, model, true);
205
+ if (!connection.models.includes(model)) {
206
+ notice = "Choose a model from the endpoint's live list.";
207
+ continue;
208
+ }
209
+ step = "save";
210
+ continue;
211
+ }
212
+ presenter.fields([
213
+ ["Endpoint", url],
214
+ ["Model", model],
215
+ ["Connection", "live model discovery passed"],
216
+ ]);
217
+ presenter.note(
218
+ first
219
+ ? "Ready with recommended defaults: workspace edits, approval for unrecognized commands, one worker, $5 tracked session budget."
220
+ : "Use this model for chat. Your other settings and role assignments stay in place.",
221
+ );
222
+ const choice = await prompts.choose("Ready to connect", ["Connect", "Back"], "Connect");
223
+ if (choice !== "Connect") throw new ConfigureNavigation("back");
224
+ const descriptor = structuredClone(connection.target);
225
+ if (!connection.existing) descriptor.defaultModel = model;
226
+ if (key) {
227
+ descriptor.auth = { ...descriptor.auth, apiKeyRef: targetApiKeyRef(descriptor.id, readSettings().targets) };
228
+ delete descriptor.auth.apiKeyEnvVar;
229
+ }
230
+ const apply = (settings: ReturnType<typeof readSettings>) => {
231
+ const target = settings.targets.find((entry) => entry.id === descriptor.id);
232
+ if (JSON.stringify(target) !== JSON.stringify(connection?.existing))
233
+ throw new Error("This target changed during setup. Reopen Quick Connect to keep those changes.");
234
+ applyTarget(settings, descriptor);
235
+ setOrchestratorPointer(settings, descriptor, model);
236
+ if (first && !settings.fleet.default.target) setWorkerDefaultPointer(settings, descriptor, model);
237
+ };
238
+ const preview = readSettings();
239
+ apply(preview);
240
+ const validated = validateSettings(preview);
241
+ if (validated.issues.length) throw new SettingsValidationError(validated.issues);
242
+ initializeClioHome();
243
+ if (key && descriptor.auth?.apiKeyRef) {
244
+ const auth = openAuthStorage();
245
+ auth.setApiKey(descriptor.auth.apiKeyRef, key);
246
+ if (credentialWriteFailed(auth, "The key was not saved; connection settings were not changed."))
247
+ throw new Error("The key could not be saved. Check permissions on the credentials file and retry.");
248
+ printPlaintextCredentialWarning();
249
+ }
250
+ updateSettings(apply);
251
+ presenter.done(`Connected: ${descriptor.id} / ${model}`);
252
+ presenter.commandAdvice("Start Clio:", "clio-coder");
253
+ return "connected";
254
+ } catch (error) {
255
+ if (!(error instanceof ConfigureNavigation)) {
256
+ notice = error instanceof Error ? error.message : String(error);
257
+ step = "url";
258
+ continue;
259
+ }
260
+ if (error.kind === "quit" || step === "url") {
261
+ presenter.done("Quick Connect cancelled; connection settings not saved.");
262
+ return error.kind;
263
+ }
264
+ step =
265
+ step === "save" && (connection?.models.length ?? 0) > 1
266
+ ? "model"
267
+ : step === "model" || step === "save"
268
+ ? askedKey
269
+ ? "key"
270
+ : "url"
271
+ : "url";
272
+ }
273
+ }
274
+ }
@@ -89,6 +89,15 @@ export function deriveTargetId(runtimeId: string, existing: ReadonlyArray<Target
89
89
  return `${base}-${Date.now()}`;
90
90
  }
91
91
 
92
+ /** A renamed target can still own the credential under its previous name. */
93
+ export function targetApiKeyRef(targetId: string, targets: ReadonlyArray<TargetDescriptor>): string {
94
+ const taken = new Set(targets.filter((target) => target.id !== targetId).map((target) => target.auth?.apiKeyRef));
95
+ const base = `target:${targetId}`;
96
+ let ref = base;
97
+ for (let suffix = 2; taken.has(ref); suffix += 1) ref = `${base}-${suffix}`;
98
+ return ref;
99
+ }
100
+
92
101
  async function buildProbeContext(runtime?: RuntimeDescriptor, target?: TargetDescriptor): Promise<ProbeContext> {
93
102
  const probeCtx: ProbeContext = {
94
103
  credentialsPresent: credentialsPresent(),
@@ -434,8 +443,11 @@ export function buildDescriptor(runtime: RuntimeDescriptor, id: string, parts: D
434
443
  return descriptor;
435
444
  }
436
445
 
437
- export function describeAuthStatus(runtime: RuntimeDescriptor): string {
438
- const status = openAuthStorage().statusForTarget(resolveRuntimeAuthTarget(runtime), { includeFallback: false });
446
+ export function describeAuthStatus(runtime: RuntimeDescriptor, target?: TargetDescriptor): string {
447
+ const status = openAuthStorage().statusForTarget(
448
+ target ? resolveAuthTarget(target, runtime) : resolveRuntimeAuthTarget(runtime),
449
+ { includeFallback: false },
450
+ );
439
451
  // This describes a credential, not a network state. It is printed before the
440
452
  // wizard has asked for a URL, so `not connected` read as a connection that
441
453
  // had already been tried and failed.