@ouro.bot/cli 0.1.0-alpha.445 → 0.1.0-alpha.447

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.
package/README.md CHANGED
@@ -99,7 +99,7 @@ Task docs do not live in this repo anymore. Planning and doing docs live in the
99
99
  - Vault unlock material is local machine state. Prefer macOS Keychain, Windows DPAPI, or Linux Secret Service; plaintext fallback is allowed only by explicit human choice.
100
100
  - New vault unlock secrets are confirmed before use and rejected if they do not meet the minimum strength requirements.
101
101
  - Provider and runtime credentials are loaded into process memory at startup/auth/unlock/refresh and reused. The remote vault is not queried for every model or sense request.
102
- - Human TTY commands share one CLI surface family: bare `ouro` opens the home deck, `ouro up`/`ouro connect`/`ouro auth verify`/`ouro repair` reuse the same readiness truth, and `ouro help`/`ouro whoami`/`ouro versions`/`ouro hatch` render from the same Ouro-branded board layer.
102
+ - Human TTY commands share one CLI surface family: bare `ouro` opens the home deck, `ouro up` uses the boot checklist, `ouro connect`/`ouro auth verify`/`ouro repair` reuse the same readiness truth, and `ouro help`/`ouro whoami`/`ouro versions`/`ouro hatch` render through the same Ouro-branded wizard/guide language instead of raw transcript walls.
103
103
  - Human-facing CLI commands that can wait on browser auth, vault IO, daemon startup, daemon restart, provider checks, or connector setup use a shared progress checklist. If a cursor may blink for more than a few seconds, the command should print or animate the current step instead of going quiet.
104
104
  - CLI commands that mutate bundle config, such as vault setup or `ouro connect bluebubbles`, run bundle sync after the change when `sync.enabled` is true and report a compact `bundle sync:` line.
105
105
  - The daemon discovers bundles dynamically from `~/AgentBundles`.
package/changelog.json CHANGED
@@ -1,6 +1,22 @@
1
1
  {
2
2
  "_note": "This changelog is maintained as part of the PR/version-bump workflow. Agent-curated, not auto-generated. Agents read this file directly via read_file to understand what changed between versions.",
3
3
  "versions": [
4
+ {
5
+ "version": "0.1.0-alpha.447",
6
+ "changes": [
7
+ "`ouro connect` now checks only the providers selected for the current agent lanes during preflight, and those live reads avoid mutating the older provider snapshot cache. That keeps the command focused on what this machine actually needs instead of paying whole-vault latency for unrelated provider records.",
8
+ "Structured vault reads for `providers/*` and `runtime/config` now reuse one short-lived Bitwarden item listing per store instance and skip redundant `bw sync` calls while the local Bitwarden cache is fresh, cutting repeated vault startup work without adding any disk credential cache.",
9
+ "The connections screen now lets the fresh live provider check outrank stale local credential visibility, so a provider that just passed appears ready and a provider that just failed shows the real live-check failure instead of falling back to misleading `credentials missing` guidance. `@ouro.bot/cli` and the `ouro.bot` wrapper are version-synced for the connect preflight latency release."
10
+ ]
11
+ },
12
+ {
13
+ "version": "0.1.0-alpha.446",
14
+ "changes": [
15
+ "`ouro connect`, the home deck, readiness repair prompts, and the interactive repair queue now render through one shared wizard language instead of a mix of framed panels and transcript walls. Humans can choose by number or name, see one recommended next step, and keep the same visual footing across setup and repair.",
16
+ "Connector, auth, vault, hatch, and other info-heavy command flows now use a matching guide surface with the same Ouro masthead, ruled sections, and `Next moves` treatment, so the CLI stops jumping between unrelated visual grammars as soon as a command leaves the root menu.",
17
+ "The new surfaces stay truthful to the underlying work: connect still runs the shared live provider verification path before rendering, repair prompts surface the real next command without dead air, and new renderer plus command-layer coverage lock the wizard/guide family into the shipped CLI."
18
+ ]
19
+ },
4
20
  {
5
21
  "version": "0.1.0-alpha.445",
6
22
  "changes": [
@@ -330,12 +330,20 @@ function selectedProviderPlan(agentName, state) {
330
330
  ...["outward", "inner"].map((lane) => `- ${laneAudienceLabel(lane)}: ${bindingLabel(state.lanes[lane])}`),
331
331
  ].join("\n");
332
332
  }
333
+ function selectedProvidersForState(state) {
334
+ return [...new Set(["outward", "inner"].map((lane) => state.lanes[lane].provider))];
335
+ }
333
336
  function mapVaultRefreshProgress(agentName, onProgress) {
334
337
  return (message) => {
335
338
  if (message.startsWith("reading vault items for ")) {
336
339
  onProgress(`${agentName}: opening saved provider credentials in the vault`);
337
340
  return;
338
341
  }
342
+ const providerRead = message.match(/^reading ([a-z0-9-]+) credentials\.\.\.$/i);
343
+ if (providerRead) {
344
+ onProgress(`${agentName}: reading saved ${providerRead[1]} credentials`);
345
+ return;
346
+ }
339
347
  if (message === "parsing provider credentials...") {
340
348
  onProgress(`${agentName}: organizing saved provider credentials`);
341
349
  }
@@ -381,7 +389,12 @@ async function checkAgentConfigWithProviderHealth(agentName, bundlesRoot, deps =
381
389
  return { ok: true };
382
390
  deps.onProgress?.(selectedProviderPlan(agentName, stateResult.state));
383
391
  const ping = deps.pingProvider ?? (await Promise.resolve().then(() => __importStar(require("../provider-ping")))).pingProvider;
384
- const poolResult = await (0, provider_credentials_1.refreshProviderCredentialPool)(agentName, deps.onProgress ? { onProgress: mapVaultRefreshProgress(agentName, deps.onProgress) } : undefined);
392
+ const providers = selectedProvidersForState(stateResult.state);
393
+ const poolResult = await (0, provider_credentials_1.refreshProviderCredentialPool)(agentName, {
394
+ ...(deps.onProgress ? { onProgress: mapVaultRefreshProgress(agentName, deps.onProgress) } : {}),
395
+ providers,
396
+ skipCache: true,
397
+ });
385
398
  const pingGroups = new Map();
386
399
  const lanes = ["outward", "inner"];
387
400
  for (const lane of lanes) {
@@ -52,6 +52,8 @@ function makeInteractiveRepairDeps(deps) {
52
52
  runAuthFlow: deps.runAuthFlow ?? (async () => undefined),
53
53
  runVaultUnlock: deps.runVaultUnlock,
54
54
  skipQueueSummary: deps.skipQueueSummary,
55
+ isTTY: deps.isTTY,
56
+ stdoutColumns: deps.stdoutColumns,
55
57
  };
56
58
  }
57
59
  async function runDeterministicRepair(degraded, deps) {
@@ -2060,11 +2060,33 @@ async function buildConnectMenu(agent, deps, onProgress) {
2060
2060
  onProgress?.("loading this machine's settings");
2061
2061
  const machineRuntime = await (0, runtime_credentials_1.refreshMachineRuntimeCredentialConfig)(agent, currentMachineId(deps), { preserveCachedOnFailure: true });
2062
2062
  const { teamsEnabled, blueBubblesEnabled } = readConnectBaySenseFlags(agent, deps);
2063
- let perplexityStatus;
2064
- let perplexityDetailLines;
2065
2063
  const perplexityApiKey = runtimeConfig.ok
2066
2064
  ? readRuntimeConfigString(runtimeConfig.config, "integrations.perplexityApiKey")
2067
2065
  : null;
2066
+ const embeddingsApiKey = runtimeConfig.ok
2067
+ ? readRuntimeConfigString(runtimeConfig.config, "integrations.openaiEmbeddingsApiKey")
2068
+ : null;
2069
+ const shouldVerifyPerplexity = runtimeConfig.ok && !!perplexityApiKey;
2070
+ const shouldVerifyEmbeddings = runtimeConfig.ok && !!embeddingsApiKey;
2071
+ let perplexityVerification;
2072
+ let embeddingsVerification;
2073
+ if (shouldVerifyPerplexity && shouldVerifyEmbeddings) {
2074
+ onProgress?.("verifying Perplexity search and memory embeddings");
2075
+ [perplexityVerification, embeddingsVerification] = await Promise.all([
2076
+ (0, runtime_capability_check_1.verifyPerplexityCapability)(perplexityApiKey),
2077
+ (0, runtime_capability_check_1.verifyEmbeddingsCapability)(embeddingsApiKey),
2078
+ ]);
2079
+ }
2080
+ else if (shouldVerifyPerplexity) {
2081
+ onProgress?.("verifying Perplexity search");
2082
+ perplexityVerification = await (0, runtime_capability_check_1.verifyPerplexityCapability)(perplexityApiKey);
2083
+ }
2084
+ else if (shouldVerifyEmbeddings) {
2085
+ onProgress?.("verifying memory embeddings");
2086
+ embeddingsVerification = await (0, runtime_capability_check_1.verifyEmbeddingsCapability)(embeddingsApiKey);
2087
+ }
2088
+ let perplexityStatus;
2089
+ let perplexityDetailLines;
2068
2090
  if (!runtimeConfig.ok) {
2069
2091
  perplexityStatus = runtimeConfigReadStatus(runtimeConfig);
2070
2092
  perplexityDetailLines = [];
@@ -2074,16 +2096,15 @@ async function buildConnectMenu(agent, deps, onProgress) {
2074
2096
  perplexityDetailLines = ["no API key saved yet"];
2075
2097
  }
2076
2098
  else {
2077
- onProgress?.("verifying Perplexity search");
2078
- const verification = await (0, runtime_capability_check_1.verifyPerplexityCapability)(perplexityApiKey);
2079
- perplexityStatus = verification.ok ? "ready" : "needs attention";
2080
- perplexityDetailLines = [verification.ok ? "verified live just now" : `live check failed: ${verification.summary}`];
2099
+ perplexityStatus = perplexityVerification?.ok ? "ready" : "needs attention";
2100
+ perplexityDetailLines = [
2101
+ perplexityVerification?.ok
2102
+ ? "verified live just now"
2103
+ : `live check failed: ${perplexityVerification.summary}`,
2104
+ ];
2081
2105
  }
2082
2106
  let embeddingsStatus;
2083
2107
  let embeddingsDetailLines;
2084
- const embeddingsApiKey = runtimeConfig.ok
2085
- ? readRuntimeConfigString(runtimeConfig.config, "integrations.openaiEmbeddingsApiKey")
2086
- : null;
2087
2108
  if (!runtimeConfig.ok) {
2088
2109
  embeddingsStatus = runtimeConfigReadStatus(runtimeConfig);
2089
2110
  embeddingsDetailLines = [];
@@ -2093,10 +2114,12 @@ async function buildConnectMenu(agent, deps, onProgress) {
2093
2114
  embeddingsDetailLines = ["no API key saved yet"];
2094
2115
  }
2095
2116
  else {
2096
- onProgress?.("verifying memory embeddings");
2097
- const verification = await (0, runtime_capability_check_1.verifyEmbeddingsCapability)(embeddingsApiKey);
2098
- embeddingsStatus = verification.ok ? "ready" : "needs attention";
2099
- embeddingsDetailLines = [verification.ok ? "verified live just now" : `live check failed: ${verification.summary}`];
2117
+ embeddingsStatus = embeddingsVerification?.ok ? "ready" : "needs attention";
2118
+ embeddingsDetailLines = [
2119
+ embeddingsVerification?.ok
2120
+ ? "verified live just now"
2121
+ : `live check failed: ${embeddingsVerification.summary}`,
2122
+ ];
2100
2123
  }
2101
2124
  const teamsStatus = runtimeConfig.ok
2102
2125
  ? hasRuntimeConfigValue(runtimeConfig.config, "teams.clientId")
@@ -4316,6 +4339,8 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
4316
4339
  await executeVaultUnlock({ kind: "vault.unlock", agent }, deps);
4317
4340
  },
4318
4341
  skipQueueSummary: true,
4342
+ isTTY: deps.isTTY ?? process.stdout.isTTY === true,
4343
+ stdoutColumns: deps.stdoutColumns ?? process.stdout.columns,
4319
4344
  });
4320
4345
  if (repairResult.repairsAttempted) {
4321
4346
  repairsAttempted = true;
@@ -17,52 +17,6 @@ const CONNECT_STATUS_PRIORITY = {
17
17
  ready: 6,
18
18
  attached: 6,
19
19
  };
20
- const RESET = "\x1b[0m";
21
- const BOLD = "\x1b[1m";
22
- const TEAL = "\x1b[38;2;78;201;176m";
23
- const GREEN = "\x1b[38;2;46;204;64m";
24
- const GOLD = "\x1b[38;2;230;190;50m";
25
- const BONE = "\x1b[38;2;238;242;234m";
26
- const MIST = "\x1b[38;2;165;184;168m";
27
- const ANSI_RE = /\x1b\[[0-9;]*m/g;
28
- function stripAnsi(text) {
29
- return text.replace(ANSI_RE, "");
30
- }
31
- function visibleLength(text) {
32
- return stripAnsi(text).length;
33
- }
34
- function padAnsi(text, width) {
35
- const missing = Math.max(0, width - visibleLength(text));
36
- return `${text}${" ".repeat(missing)}`;
37
- }
38
- function wrapPlain(text, width) {
39
- const normalized = text.trim();
40
- if (!normalized)
41
- return [""];
42
- const words = normalized.split(/\s+/);
43
- const lines = [];
44
- let current = "";
45
- for (const word of words) {
46
- if (!current) {
47
- current = word;
48
- continue;
49
- }
50
- const candidate = `${current} ${word}`;
51
- if (candidate.length <= width) {
52
- current = candidate;
53
- continue;
54
- }
55
- lines.push(current);
56
- current = word;
57
- }
58
- lines.push(current);
59
- return lines;
60
- }
61
- function tty(text, color, bold = false) {
62
- if (bold)
63
- return `${color}${BOLD}${text}${RESET}`;
64
- return `${color}${text}${RESET}`;
65
- }
66
20
  function escapeRegExp(value) {
67
21
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
68
22
  }
@@ -130,216 +84,102 @@ function resolveProviderHealthCommand(providerHealth, status) {
130
84
  }
131
85
  return undefined;
132
86
  }
133
- function isProblemStatus(status) {
134
- return status !== "ready" && status !== "attached";
135
- }
136
- function statusChip(status) {
137
- const symbol = status === "ready" || status === "attached"
138
- ? "●"
139
- : status === "not attached"
140
- ? "◌"
141
- : "◆";
142
- const label = `${symbol} ${status}`;
143
- if (status === "ready" || status === "attached")
144
- return tty(label, GREEN, true);
145
- if (status === "not attached")
146
- return tty(label, MIST);
147
- return tty(label, GOLD, true);
148
- }
149
- function sectionTitle(title, width) {
150
- const plain = `╭─ ${title} `;
151
- const rule = "─".repeat(Math.max(0, width - plain.length - 1));
152
- return `${tty("╭─ ", TEAL)}${tty(title, BONE, true)}${tty(` ${rule}╮`, TEAL)}`;
153
- }
154
- function bottomRule(width) {
155
- const line = `╰${"─".repeat(Math.max(0, width - 2))}╯`;
156
- return tty(line, TEAL);
87
+ function providerHealthTargetLane(providerHealth) {
88
+ const issue = providerHealth?.issue;
89
+ const actionLane = issue?.actions
90
+ .map((action) => "lane" in action && action.lane ? action.lane : undefined)
91
+ .find((lane) => lane === "outward" || lane === "inner");
92
+ if (actionLane)
93
+ return actionLane;
94
+ const text = [issue?.summary, issue?.detail, providerHealth?.error]
95
+ .filter(Boolean)
96
+ .join(" ")
97
+ .toLowerCase();
98
+ if (/\boutward provider\b/.test(text) || /\boutward lane\b/.test(text))
99
+ return "outward";
100
+ if (/\binner provider\b/.test(text) || /\binner lane\b/.test(text))
101
+ return "inner";
102
+ return undefined;
157
103
  }
158
- function bodyLine(text, width) {
159
- const padded = padAnsi(text, Math.max(0, width - 4));
160
- return `${tty("│ ", TEAL)}${padded}${tty(" │", TEAL)}`;
104
+ function providerHealthAppliesToLane(providerHealth, lane) {
105
+ const targetLane = providerHealthTargetLane(providerHealth);
106
+ return !targetLane || targetLane === lane.lane;
161
107
  }
162
- function panel(title, body, width) {
163
- const lines = [sectionTitle(title, width)];
164
- for (const line of body) {
165
- lines.push(bodyLine(line, width));
108
+ function providerHealthDetail(providerHealth, status) {
109
+ if (status === "locked")
110
+ return "vault locked on this machine";
111
+ if (status === "needs credentials")
112
+ return "credentials missing";
113
+ if (status === "needs setup") {
114
+ return providerHealth?.issue?.detail ?? providerHealth?.error ?? "needs setup";
166
115
  }
167
- lines.push(bottomRule(width));
168
- return lines;
116
+ const detail = providerHealth?.issue?.detail ?? providerHealth?.error;
117
+ if (!detail)
118
+ return "live check needs attention";
119
+ return /failed live check/i.test(detail) ? detail : `failed live check: ${detail}`;
169
120
  }
170
- function renderHeader(agent, width) {
171
- return panel(`${agent} connections`, [
172
- tty("Set up or review one capability at a time.", BONE, true),
173
- tty("Everything on this screen was checked live just now.", MIST),
174
- ], width);
175
- }
176
- function nextMoveBody(entry) {
177
- if (!entry) {
178
- return [
179
- tty("Everything here is ready.", BONE, true),
180
- tty("Pick what you want to review or refresh.", MIST),
181
- ];
182
- }
183
- const lines = [
184
- `${entry.name} ${statusChip(entry.status)}`,
185
- ];
186
- if (entry.nextNote)
187
- lines.push(entry.nextNote);
188
- if (entry.nextAction)
189
- lines.push(tty(entry.nextAction, MIST));
190
- return lines;
121
+ function isProblemStatus(status) {
122
+ return status !== "ready" && status !== "attached";
191
123
  }
192
- function renderProviderBody(entry, width) {
193
- const lines = [
194
- `${entry.option} ${entry.name} ${statusChip(entry.status)}`,
195
- ];
196
- const lanes = entry.laneSummaries ?? [];
197
- for (const [index, lane] of lanes.entries()) {
198
- if (index > 0)
199
- lines.push("");
200
- const laneLabel = lane.lane === "outward" ? "Outward lane" : "Inner lane";
201
- lines.push(tty(laneLabel, BONE, true));
202
- lines.push(lane.title);
203
- lines.push(isProblemStatus(lane.status) ? lane.detail : tty(lane.detail, MIST));
204
- }
205
- if (lanes.length === 0) {
206
- for (const detail of entry.detailLines ?? [])
207
- lines.push(detail);
208
- }
209
- return normalizeWrappedBody(lines, width);
124
+ function providerEntrySummary(entry) {
125
+ return entry.description ?? "Selected provider lanes for this machine.";
210
126
  }
211
- function renderCapabilityBody(entries, width) {
127
+ function providerEntryDetailLines(entry) {
212
128
  const lines = [];
213
- for (const [index, entry] of entries.entries()) {
214
- if (index > 0)
215
- lines.push("");
216
- lines.push(`${entry.option} ${entry.name} ${statusChip(entry.status)}`);
217
- if (entry.description) {
218
- lines.push(isProblemStatus(entry.status) ? entry.description : tty(entry.description, MIST));
219
- }
220
- for (const detail of entry.detailLines ?? []) {
221
- lines.push(detail);
222
- }
129
+ if (entry.nextNote && !/^(Outward|Inner) lane: /.test(entry.nextNote)) {
130
+ lines.push(entry.nextNote);
223
131
  }
224
- return normalizeWrappedBody(lines, width);
225
- }
226
- function normalizeWrappedBody(lines, width) {
227
- const wrapped = [];
228
- for (const line of lines) {
229
- if (!line) {
230
- wrapped.push("");
231
- continue;
232
- }
233
- const plain = stripAnsi(line);
234
- if (plain.length <= width - 4) {
235
- wrapped.push(line);
236
- continue;
132
+ if (entry.laneSummaries && entry.laneSummaries.length > 0) {
133
+ for (const lane of entry.laneSummaries) {
134
+ const laneLabel = lane.lane === "outward" ? "Outward lane" : "Inner lane";
135
+ lines.push(`${laneLabel}: ${lane.title} ${lane.detail}`);
237
136
  }
238
- const segments = wrapPlain(plain, width - 4);
239
- wrapped.push(...segments);
137
+ return lines;
240
138
  }
241
- return wrapped;
139
+ return [...lines, ...(entry.detailLines ?? [])];
242
140
  }
243
- function stackPanels(panels) {
244
- const lines = [];
245
- for (const [index, panelLines] of panels.entries()) {
246
- if (index > 0)
247
- lines.push("");
248
- lines.push(...panelLines);
249
- }
250
- return lines;
251
- }
252
- function combineColumns(left, right, leftWidth, rightWidth, gap = 2) {
253
- const total = Math.max(left.length, right.length);
254
- const lines = [];
255
- for (let index = 0; index < total; index += 1) {
256
- const leftLine = left[index] ?? " ".repeat(leftWidth);
257
- const rightLine = right[index] ?? " ".repeat(rightWidth);
258
- lines.push(`${padAnsi(leftLine, leftWidth)}${" ".repeat(gap)}${padAnsi(rightLine, rightWidth)}`);
259
- }
260
- return lines;
261
- }
262
- function renderTtyBay(entries, options) {
263
- const columns = Math.max(options.columns ?? 108, 72);
264
- const fullWidth = Math.max(56, columns - 2);
265
- const masthead = (0, terminal_ui_1.renderOuroMasthead)({
266
- isTTY: true,
267
- columns,
268
- subtitle: "Set up connections one step at a time.",
269
- }).trimEnd();
270
- const header = renderHeader(options.agent, fullWidth);
271
- const nextEntry = entries.find((entry) => isProblemStatus(entry.status));
272
- const providerEntry = entries.find((entry) => entry.section === "Providers");
273
- const portableEntries = entries.filter((entry) => entry.section === "Portable");
274
- const machineEntries = entries.filter((entry) => entry.section === "This machine");
275
- const wide = columns >= 118;
276
- const footer = [
277
- tty("Choose a number, or type the capability name.", MIST),
278
- options.prompt,
141
+ function capabilityEntryDetailLines(entry) {
142
+ return [
143
+ ...(entry.detailLines ?? []),
144
+ ...(entry.nextNote ? [entry.nextNote] : []),
279
145
  ];
280
- if (!wide) {
281
- const panels = [
282
- header,
283
- panel("Recommended next step", nextMoveBody(nextEntry), fullWidth),
284
- panel("Providers", renderProviderBody(providerEntry, fullWidth), fullWidth),
285
- panel("Portable", renderCapabilityBody(portableEntries, fullWidth), fullWidth),
286
- panel("This machine", renderCapabilityBody(machineEntries, fullWidth), fullWidth),
287
- ];
288
- return [masthead, "", ...stackPanels(panels), "", ...footer].join("\n");
289
- }
290
- const gap = 2;
291
- const leftWidth = Math.max(52, Math.floor((fullWidth - gap) / 2));
292
- const rightWidth = Math.max(40, fullWidth - gap - leftWidth);
293
- const topRow = combineColumns(panel("Recommended next step", nextMoveBody(nextEntry), leftWidth), panel("This machine", renderCapabilityBody(machineEntries, rightWidth), rightWidth), leftWidth, rightWidth, gap);
294
- const bottomRow = combineColumns(panel("Providers", renderProviderBody(providerEntry, leftWidth), leftWidth), panel("Portable", renderCapabilityBody(portableEntries, rightWidth), rightWidth), leftWidth, rightWidth, gap);
295
- return [masthead, "", ...header, "", ...topRow, "", ...bottomRow, "", ...footer].join("\n");
296
146
  }
297
- function renderNonTtyBay(entries, options) {
147
+ function entryToWizardItem(entry) {
148
+ return {
149
+ key: entry.option,
150
+ label: entry.name,
151
+ status: entry.status,
152
+ ...(entry.section === "Providers"
153
+ ? { summary: providerEntrySummary(entry) }
154
+ : entry.description
155
+ ? { summary: entry.description }
156
+ : {}),
157
+ detailLines: entry.section === "Providers"
158
+ ? providerEntryDetailLines(entry)
159
+ : capabilityEntryDetailLines(entry),
160
+ ...(entry.nextAction ? { command: entry.nextAction } : {}),
161
+ };
162
+ }
163
+ function nextStepFor(entries) {
298
164
  const nextEntry = entries.find((entry) => isProblemStatus(entry.status));
299
- const lines = [
300
- `${options.agent} connections`,
301
- "Set up or review one capability at a time. Provider status was checked live just now.",
302
- "",
303
- "Recommended next step",
304
- "---------------------",
305
- ];
306
165
  if (!nextEntry) {
307
- lines.push("Everything here is ready. Pick what you want to review or refresh.");
308
- }
309
- else {
310
- lines.push(`${nextEntry.name} - ${nextEntry.status}`);
311
- if (nextEntry.nextNote)
312
- lines.push(nextEntry.nextNote);
313
- if (nextEntry.nextAction)
314
- lines.push(`run: ${nextEntry.nextAction}`);
315
- }
316
- lines.push("");
317
- for (const section of ["Providers", "Portable", "This machine"]) {
318
- lines.push(section);
319
- lines.push("-".repeat(Math.max(6, section.length + 4)));
320
- for (const entry of entries.filter((candidate) => candidate.section === section)) {
321
- lines.push(`${entry.option}. ${entry.name} [${entry.status}]`);
322
- if (entry.laneSummaries && entry.laneSummaries.length > 0) {
323
- for (const lane of entry.laneSummaries) {
324
- const laneLabel = lane.lane === "outward" ? "Outward lane" : "Inner lane";
325
- lines.push(` ${laneLabel}: ${lane.title}`);
326
- lines.push(` ${lane.detail}`);
327
- }
328
- }
329
- else {
330
- for (const detail of entry.detailLines ?? [])
331
- lines.push(` ${detail}`);
332
- }
333
- if (entry.description)
334
- lines.push(` ${entry.description}`);
335
- lines.push("");
336
- }
166
+ return {
167
+ label: "Everything here is already connected.",
168
+ detail: "Pick any capability if you want to review it, refresh it, or change its setup.",
169
+ };
337
170
  }
338
- lines.push("6. Not now");
339
- lines.push("");
340
- lines.push("Choose a number, or type the capability name.");
341
- lines.push(options.prompt);
342
- return lines.join("\n");
171
+ return {
172
+ label: `Start with ${nextEntry.name}.`,
173
+ detail: nextEntry.nextNote ?? nextEntry.description ?? `Status: ${nextEntry.status}.`,
174
+ command: nextEntry.nextAction,
175
+ };
176
+ }
177
+ function sectionToWizard(entries, section, summary) {
178
+ return {
179
+ title: section,
180
+ summary,
181
+ items: entries.filter((entry) => entry.section === section).map((entry) => entryToWizardItem(entry)),
182
+ };
343
183
  }
344
184
  function summarizeProviderLane(agent, lane, providerHealth) {
345
185
  const providerHealthStatus = resolveProviderHealthStatus(providerHealth);
@@ -354,6 +194,23 @@ function summarizeProviderLane(agent, lane, providerHealth) {
354
194
  };
355
195
  }
356
196
  const fallbackAction = providerHealthCommand ?? lane.credential.repairCommand;
197
+ if (providerHealth?.ok) {
198
+ return {
199
+ lane: lane.lane,
200
+ status: "ready",
201
+ title: `${lane.provider} / ${lane.model}`,
202
+ detail: "ready",
203
+ };
204
+ }
205
+ if (providerHealthStatus && providerHealthAppliesToLane(providerHealth, lane)) {
206
+ return {
207
+ lane: lane.lane,
208
+ status: providerHealthStatus,
209
+ title: `${lane.provider} / ${lane.model}`,
210
+ detail: providerHealthDetail(providerHealth, providerHealthStatus),
211
+ action: fallbackAction,
212
+ };
213
+ }
357
214
  if (lane.credential.status === "missing") {
358
215
  return {
359
216
  lane: lane.lane,
@@ -372,14 +229,6 @@ function summarizeProviderLane(agent, lane, providerHealth) {
372
229
  action: fallbackAction,
373
230
  };
374
231
  }
375
- if (providerHealth?.ok) {
376
- return {
377
- lane: lane.lane,
378
- status: "ready",
379
- title: `${lane.provider} / ${lane.model}`,
380
- detail: "ready",
381
- };
382
- }
383
232
  if (lane.readiness.status === "failed") {
384
233
  return {
385
234
  lane: lane.lane,
@@ -450,7 +299,25 @@ function renderConnectBay(entries, options) {
450
299
  columns: options.columns ?? null,
451
300
  },
452
301
  });
453
- if (!options.isTTY)
454
- return renderNonTtyBay(entries, options);
455
- return renderTtyBay(entries, options);
302
+ return (0, terminal_ui_1.renderTerminalWizard)({
303
+ isTTY: options.isTTY,
304
+ columns: options.columns,
305
+ masthead: {
306
+ subtitle: "Set up connections one step at a time.",
307
+ },
308
+ title: `Connect ${options.agent}`,
309
+ summary: "Choose one capability to bring online. Each row tells you whether Ouro checked it live just now or is showing saved setup on this machine.",
310
+ nextStep: nextStepFor(entries),
311
+ sections: [
312
+ sectionToWizard(entries, "Providers", "Selected outward and inner lanes for this machine."),
313
+ sectionToWizard(entries, "Portable", "These travel with the agent bundle when their secrets are portable."),
314
+ sectionToWizard(entries, "This machine", "These depend on local attachments or machine-specific setup."),
315
+ ],
316
+ footerLines: [
317
+ "6. Not now",
318
+ "Choose a number, or type the capability name.",
319
+ ],
320
+ prompt: options.prompt,
321
+ suppressEvent: true,
322
+ });
456
323
  }