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

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,14 @@
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.446",
6
+ "changes": [
7
+ "`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.",
8
+ "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.",
9
+ "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."
10
+ ]
11
+ },
4
12
  {
5
13
  "version": "0.1.0-alpha.445",
6
14
  "changes": [
@@ -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) {
@@ -4316,6 +4316,8 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
4316
4316
  await executeVaultUnlock({ kind: "vault.unlock", agent }, deps);
4317
4317
  },
4318
4318
  skipQueueSummary: true,
4319
+ isTTY: deps.isTTY ?? process.stdout.isTTY === true,
4320
+ stdoutColumns: deps.stdoutColumns ?? process.stdout.columns,
4319
4321
  });
4320
4322
  if (repairResult.repairsAttempted) {
4321
4323
  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
  }
@@ -133,213 +87,65 @@ function resolveProviderHealthCommand(providerHealth, status) {
133
87
  function isProblemStatus(status) {
134
88
  return status !== "ready" && status !== "attached";
135
89
  }
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);
157
- }
158
- function bodyLine(text, width) {
159
- const padded = padAnsi(text, Math.max(0, width - 4));
160
- return `${tty("│ ", TEAL)}${padded}${tty(" │", TEAL)}`;
161
- }
162
- function panel(title, body, width) {
163
- const lines = [sectionTitle(title, width)];
164
- for (const line of body) {
165
- lines.push(bodyLine(line, width));
166
- }
167
- lines.push(bottomRule(width));
168
- return lines;
169
- }
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);
90
+ function providerEntrySummary(entry) {
91
+ return entry.description ?? "Selected provider lanes for this machine.";
175
92
  }
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;
191
- }
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);
210
- }
211
- function renderCapabilityBody(entries, width) {
93
+ function providerEntryDetailLines(entry) {
212
94
  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
- }
95
+ if (entry.nextNote && !/^(Outward|Inner) lane: /.test(entry.nextNote)) {
96
+ lines.push(entry.nextNote);
223
97
  }
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;
98
+ if (entry.laneSummaries && entry.laneSummaries.length > 0) {
99
+ for (const lane of entry.laneSummaries) {
100
+ const laneLabel = lane.lane === "outward" ? "Outward lane" : "Inner lane";
101
+ lines.push(`${laneLabel}: ${lane.title} ${lane.detail}`);
237
102
  }
238
- const segments = wrapPlain(plain, width - 4);
239
- wrapped.push(...segments);
103
+ return lines;
240
104
  }
241
- return wrapped;
105
+ return [...lines, ...(entry.detailLines ?? [])];
242
106
  }
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,
107
+ function capabilityEntryDetailLines(entry) {
108
+ return [
109
+ ...(entry.detailLines ?? []),
110
+ ...(entry.nextNote ? [entry.nextNote] : []),
279
111
  ];
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
112
  }
297
- function renderNonTtyBay(entries, options) {
113
+ function entryToWizardItem(entry) {
114
+ return {
115
+ key: entry.option,
116
+ label: entry.name,
117
+ status: entry.status,
118
+ ...(entry.section === "Providers"
119
+ ? { summary: providerEntrySummary(entry) }
120
+ : entry.description
121
+ ? { summary: entry.description }
122
+ : {}),
123
+ detailLines: entry.section === "Providers"
124
+ ? providerEntryDetailLines(entry)
125
+ : capabilityEntryDetailLines(entry),
126
+ ...(entry.nextAction ? { command: entry.nextAction } : {}),
127
+ };
128
+ }
129
+ function nextStepFor(entries) {
298
130
  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
131
  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
- }
132
+ return {
133
+ label: "Everything here is already connected.",
134
+ detail: "Pick any capability if you want to review it, refresh it, or change its setup.",
135
+ };
337
136
  }
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");
137
+ return {
138
+ label: `Start with ${nextEntry.name}.`,
139
+ detail: nextEntry.nextNote ?? nextEntry.description ?? `Status: ${nextEntry.status}.`,
140
+ command: nextEntry.nextAction,
141
+ };
142
+ }
143
+ function sectionToWizard(entries, section, summary) {
144
+ return {
145
+ title: section,
146
+ summary,
147
+ items: entries.filter((entry) => entry.section === section).map((entry) => entryToWizardItem(entry)),
148
+ };
343
149
  }
344
150
  function summarizeProviderLane(agent, lane, providerHealth) {
345
151
  const providerHealthStatus = resolveProviderHealthStatus(providerHealth);
@@ -450,7 +256,25 @@ function renderConnectBay(entries, options) {
450
256
  columns: options.columns ?? null,
451
257
  },
452
258
  });
453
- if (!options.isTTY)
454
- return renderNonTtyBay(entries, options);
455
- return renderTtyBay(entries, options);
259
+ return (0, terminal_ui_1.renderTerminalWizard)({
260
+ isTTY: options.isTTY,
261
+ columns: options.columns,
262
+ masthead: {
263
+ subtitle: "Set up connections one step at a time.",
264
+ },
265
+ title: `Connect ${options.agent}`,
266
+ 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.",
267
+ nextStep: nextStepFor(entries),
268
+ sections: [
269
+ sectionToWizard(entries, "Providers", "Selected outward and inner lanes for this machine."),
270
+ sectionToWizard(entries, "Portable", "These travel with the agent bundle when their secrets are portable."),
271
+ sectionToWizard(entries, "This machine", "These depend on local attachments or machine-specific setup."),
272
+ ],
273
+ footerLines: [
274
+ "6. Not now",
275
+ "Choose a number, or type the capability name.",
276
+ ],
277
+ prompt: options.prompt,
278
+ suppressEvent: true,
279
+ });
456
280
  }
@@ -17,6 +17,34 @@ function renderScreenEvent(screen) {
17
17
  meta: { screen },
18
18
  });
19
19
  }
20
+ function homeActionSummary(action) {
21
+ switch (action.kind) {
22
+ case "chat":
23
+ return `Open chat with ${action.agent}.`;
24
+ case "up":
25
+ return "Start the local runtime and check what still needs attention.";
26
+ case "connect":
27
+ return "Set up providers, portable tools, and machine-specific attachments.";
28
+ case "repair":
29
+ return "Walk through repairs for anything blocking startup or chat.";
30
+ case "help":
31
+ return "Show the command guide.";
32
+ case "hatch":
33
+ return "Create a new agent on this machine.";
34
+ case "clone":
35
+ return "Bring an existing bundle onto this machine.";
36
+ case "exit":
37
+ return "Leave the prompt.";
38
+ }
39
+ }
40
+ function actionToWizardItem(action) {
41
+ return {
42
+ key: action.key,
43
+ label: action.label,
44
+ summary: homeActionSummary(action),
45
+ ...(action.key === "1" ? { recommended: true } : {}),
46
+ };
47
+ }
20
48
  function buildOuroHomeActions(agents) {
21
49
  if (agents.length === 0) {
22
50
  return [
@@ -57,21 +85,28 @@ function resolveOuroHomeAction(answer, actions) {
57
85
  function renderOuroHomeScreen(options) {
58
86
  renderScreenEvent("home");
59
87
  const actions = buildOuroHomeActions(options.agents);
60
- const sections = [
61
- {
62
- title: options.agents.length === 0 ? "Start here" : "Available agents",
63
- lines: options.agents.length === 0
64
- ? ["No agents are set up on this machine yet."]
65
- : options.agents.map((agent) => `${agent} is available.`),
66
- },
67
- ];
68
- const actionRows = actions.map((action, index) => ({
69
- label: action.label,
70
- actor: "agent-runnable",
71
- command: action.command,
72
- ...(index === 0 ? { recommended: true } : {}),
73
- }));
74
- return (0, terminal_ui_1.renderTerminalBoard)({
88
+ const chatActions = actions.filter((action) => action.kind === "chat");
89
+ const setupActions = actions.filter((action) => action.kind !== "chat");
90
+ const sections = options.agents.length === 0
91
+ ? [
92
+ {
93
+ title: "Start here",
94
+ summary: "There are no agents on this machine yet.",
95
+ items: setupActions.map((action) => actionToWizardItem(action)),
96
+ },
97
+ ]
98
+ : [
99
+ {
100
+ title: "Agents",
101
+ summary: "Jump straight into conversation or pick a setup path below.",
102
+ items: chatActions.map((action) => actionToWizardItem(action)),
103
+ },
104
+ {
105
+ title: "System",
106
+ items: setupActions.map((action) => actionToWizardItem(action)),
107
+ },
108
+ ];
109
+ return (0, terminal_ui_1.renderTerminalWizard)({
75
110
  isTTY: options.isTTY,
76
111
  columns: options.columns,
77
112
  masthead: {
@@ -84,13 +119,18 @@ function renderOuroHomeScreen(options) {
84
119
  ? "Create a new agent or clone an existing bundle to get started."
85
120
  : "Choose an agent or a setup task without memorizing commands.",
86
121
  sections,
87
- actions: actionRows,
122
+ nextStep: {
123
+ label: options.agents.length === 0 ? "Start by creating or cloning an agent." : `Start with ${actions[0].label}.`,
124
+ detail: options.agents.length === 0
125
+ ? "Once one agent bundle exists here, the rest of the command surface becomes interactive."
126
+ : "You can always type the number or the agent name instead of remembering a command.",
127
+ },
88
128
  prompt: `Choose [1-${actions.length}] or type a name: `,
89
129
  });
90
130
  }
91
131
  function renderAgentPickerScreen(options) {
92
132
  renderScreenEvent("agent-picker");
93
- return (0, terminal_ui_1.renderTerminalBoard)({
133
+ return (0, terminal_ui_1.renderTerminalWizard)({
94
134
  isTTY: options.isTTY,
95
135
  columns: options.columns,
96
136
  masthead: {
@@ -101,7 +141,12 @@ function renderAgentPickerScreen(options) {
101
141
  sections: [
102
142
  {
103
143
  title: "Agents",
104
- lines: options.agents.map((agent, index) => `${index + 1}. ${agent}`),
144
+ items: options.agents.map((agent, index) => ({
145
+ key: String(index + 1),
146
+ label: agent,
147
+ summary: "Available on this machine.",
148
+ ...(index === 0 ? { recommended: true } : {}),
149
+ })),
105
150
  },
106
151
  ],
107
152
  prompt: `Choose [1-${options.agents.length}] or type a name: `,
@@ -116,32 +161,65 @@ function resolveNamedAgentSelection(answer, agents) {
116
161
  return agents[numbered - 1];
117
162
  return agents.find((agent) => agent.toLowerCase() === normalized);
118
163
  }
119
- function statusLabel(status) {
120
- return status.replace(/-/g, " ");
121
- }
122
164
  function renderHumanReadinessBoard(options) {
123
165
  renderScreenEvent("readiness");
124
- const sections = options.snapshot.items.map((item) => ({
125
- title: item.title,
126
- lines: [
127
- `${statusLabel(item.status)} — ${item.summary}`,
128
- ...item.detailLines,
129
- ],
166
+ const issueItems = options.snapshot.items.map((item) => ({
167
+ label: item.title,
168
+ status: item.status,
169
+ summary: item.summary,
170
+ detailLines: item.detailLines,
130
171
  }));
131
- return renderHumanCommandBoard({
132
- title: options.title,
133
- subtitle: options.subtitle,
134
- summary: options.snapshot.summary,
172
+ const actionItems = options.snapshot.nextActions.map((action, index) => ({
173
+ key: String(index + 1),
174
+ label: action.label,
175
+ actor: action.actor,
176
+ command: action.command,
177
+ ...(action.recommended ? { recommended: true } : {}),
178
+ }));
179
+ if (options.prompt) {
180
+ actionItems.push({
181
+ key: String(actionItems.length + 1),
182
+ label: "Skip for now",
183
+ });
184
+ }
185
+ return (0, terminal_ui_1.renderTerminalWizard)({
135
186
  isTTY: options.isTTY,
136
187
  columns: options.columns,
137
- sections,
138
- actions: options.snapshot.nextActions,
188
+ masthead: {
189
+ subtitle: options.subtitle,
190
+ },
191
+ title: options.title,
192
+ summary: options.snapshot.summary,
193
+ nextStep: options.snapshot.primaryAction
194
+ ? {
195
+ label: options.snapshot.primaryAction.label,
196
+ detail: options.snapshot.summary,
197
+ command: options.snapshot.primaryAction.command,
198
+ }
199
+ : options.snapshot.status === "ready" || options.snapshot.status === "attached"
200
+ ? {
201
+ label: "Everything needed here is ready.",
202
+ detail: "You can keep going or leave this area alone.",
203
+ }
204
+ : undefined,
205
+ sections: [
206
+ {
207
+ title: "What needs attention",
208
+ items: issueItems,
209
+ },
210
+ ...(actionItems.length > 0
211
+ ? [{
212
+ title: "Ways forward",
213
+ items: actionItems,
214
+ }]
215
+ : []),
216
+ ],
139
217
  prompt: options.prompt,
140
218
  });
141
219
  }
142
220
  function renderHumanCommandBoard(options) {
143
221
  renderScreenEvent("command-board");
144
- return (0, terminal_ui_1.renderTerminalBoard)({
222
+ return (0, terminal_ui_1.renderTerminalGuide)({
145
223
  isTTY: options.isTTY,
146
224
  columns: options.columns,
147
225
  masthead: {
@@ -11,6 +11,7 @@ exports.isAffirmativeAnswer = isAffirmativeAnswer;
11
11
  exports.runInteractiveRepair = runInteractiveRepair;
12
12
  const runtime_1 = require("../../nerves/runtime");
13
13
  const identity_1 = require("../identity");
14
+ const terminal_ui_1 = require("./terminal-ui");
14
15
  function isCredentialIssue(degraded) {
15
16
  const reason = degraded.errorReason.toLowerCase();
16
17
  const hint = degraded.fixHint.toLowerCase();
@@ -90,6 +91,9 @@ function fallbackCommandsFor(degraded, primaryCommand) {
90
91
  function renderRepairChoices(prefix, commands) {
91
92
  return commands.map((command, index) => ` ${index === 0 ? prefix : "or"}: ${command}`);
92
93
  }
94
+ function repairStatusFor(action) {
95
+ return action.kind === "vault-unlock" ? "locked" : "needs credentials";
96
+ }
93
97
  function renderRepairQueueSummaryLines(degraded) {
94
98
  const repairable = degraded
95
99
  .map((entry) => ({ entry, action: runnableRepairActionFor(entry) }))
@@ -109,6 +113,34 @@ function renderRepairQueueSummaryLines(degraded) {
109
113
  });
110
114
  return lines;
111
115
  }
116
+ function renderRepairQueueSummary(degraded, deps) {
117
+ const repairable = degraded
118
+ .map((entry) => ({ entry, action: runnableRepairActionFor(entry) }))
119
+ .filter((item) => item.action !== undefined);
120
+ if (!deps.isTTY) {
121
+ return renderRepairQueueSummaryLines(degraded).join("\n");
122
+ }
123
+ return (0, terminal_ui_1.renderTerminalWizard)({
124
+ isTTY: true,
125
+ columns: deps.stdoutColumns,
126
+ masthead: {
127
+ subtitle: "Repair paths ready.",
128
+ },
129
+ title: "Needs attention before startup can finish",
130
+ summary: "Ouro found repair steps it can open right now. It will walk through them one at a time.",
131
+ sections: [{
132
+ title: "Repair queue",
133
+ items: repairable.map(({ entry, action }) => ({
134
+ label: entry.agent,
135
+ status: repairStatusFor(action),
136
+ summary: action.label,
137
+ detailLines: [entry.errorReason],
138
+ command: action.command,
139
+ })),
140
+ }],
141
+ suppressEvent: true,
142
+ }).trimEnd();
143
+ }
112
144
  function renderActionPromptLines(agent, action) {
113
145
  const lines = [
114
146
  `${agent}`,
@@ -120,11 +152,66 @@ function renderActionPromptLines(agent, action) {
120
152
  }
121
153
  return lines;
122
154
  }
123
- function renderDeferredRepair(agent, commands) {
124
- return [
125
- `Leaving ${agent} for later.`,
126
- ...renderRepairChoices("next", commands),
127
- ].join("\n");
155
+ function renderActionPrompt(entry, action, deps) {
156
+ if (!deps.isTTY)
157
+ return renderActionPromptLines(entry.agent, action).join("\n");
158
+ const footer = action.kind === "vault-unlock"
159
+ ? "Only say yes if you have the saved vault unlock secret."
160
+ : "Say yes if you want Ouro to open the auth flow now.";
161
+ return (0, terminal_ui_1.renderTerminalWizard)({
162
+ isTTY: true,
163
+ columns: deps.stdoutColumns,
164
+ masthead: {
165
+ subtitle: "One repair step at a time.",
166
+ },
167
+ title: `Repair ${entry.agent}`,
168
+ summary: "Ouro found one clear next move for this agent.",
169
+ nextStep: {
170
+ label: action.kind === "vault-unlock"
171
+ ? "Unlock the credential vault on this machine."
172
+ : "Refresh provider authentication.",
173
+ detail: action.kind === "vault-unlock"
174
+ ? "Use the saved unlock secret if you have it. If not, leave this for later and choose a replacement or recovery path."
175
+ : "This opens the provider login or refresh flow for the selected provider.",
176
+ command: action.command,
177
+ },
178
+ sections: [{
179
+ title: "Why startup paused",
180
+ items: [{
181
+ label: entry.agent,
182
+ status: repairStatusFor(action),
183
+ summary: entry.errorReason,
184
+ }],
185
+ }],
186
+ footerLines: [footer],
187
+ suppressEvent: true,
188
+ }).trimEnd();
189
+ }
190
+ function renderDeferredRepair(agent, commands, deps) {
191
+ if (!deps.isTTY) {
192
+ return [
193
+ `Leaving ${agent} for later.`,
194
+ ...renderRepairChoices("next", commands),
195
+ ].join("\n");
196
+ }
197
+ return (0, terminal_ui_1.renderTerminalWizard)({
198
+ isTTY: true,
199
+ columns: deps.stdoutColumns,
200
+ masthead: {
201
+ subtitle: "Nothing changed yet.",
202
+ },
203
+ title: `Leaving ${agent} for later`,
204
+ summary: "Ouro did not open this repair flow right now.",
205
+ sections: [{
206
+ title: "Next time",
207
+ items: commands.map((command, index) => ({
208
+ key: String(index + 1),
209
+ label: index === 0 ? "Start here" : "Or try this",
210
+ command,
211
+ })),
212
+ }],
213
+ suppressEvent: true,
214
+ }).trimEnd();
128
215
  }
129
216
  function renderManualRepairHint(agent, fixHint) {
130
217
  return [
@@ -140,7 +227,7 @@ function renderUnknownRepair(agent, errorReason) {
140
227
  ].join("\n");
141
228
  }
142
229
  function writeDeclinedRepair(degraded, command, deps) {
143
- deps.writeStdout(renderDeferredRepair(degraded.agent, fallbackCommandsFor(degraded, command)));
230
+ deps.writeStdout(renderDeferredRepair(degraded.agent, fallbackCommandsFor(degraded, command), deps));
144
231
  }
145
232
  function runnableRepairActionFor(degraded) {
146
233
  const typedAction = degraded.issue?.actions
@@ -180,10 +267,10 @@ function typedActionToRunnable(degraded, action) {
180
267
  function writeRepairQueueSummary(degraded, deps) {
181
268
  const lines = renderRepairQueueSummaryLines(degraded);
182
269
  if (lines.length > 0)
183
- deps.writeStdout(lines.join("\n"));
270
+ deps.writeStdout(renderRepairQueueSummary(degraded, deps));
184
271
  }
185
272
  async function attemptVaultUnlock(entry, action, deps) {
186
- deps.writeStdout(renderActionPromptLines(entry.agent, action).join("\n"));
273
+ deps.writeStdout(renderActionPrompt(entry, action, deps));
187
274
  const answer = await deps.promptInput(`Unlock ${entry.agent}'s vault now? [y/N] `);
188
275
  if (!isAffirmativeAnswer(answer)) {
189
276
  writeDeclinedRepair(entry, action.command, deps);
@@ -211,7 +298,7 @@ async function attemptVaultUnlock(entry, action, deps) {
211
298
  }
212
299
  }
213
300
  async function attemptProviderAuth(entry, action, deps) {
214
- deps.writeStdout(renderActionPromptLines(entry.agent, action).join("\n"));
301
+ deps.writeStdout(renderActionPrompt(entry, action, deps));
215
302
  const answer = await deps.promptInput(`Open the auth flow for ${entry.agent} now? [y/N] `);
216
303
  if (!isAffirmativeAnswer(answer)) {
217
304
  writeDeclinedRepair(entry, action.command, deps);
@@ -7,7 +7,9 @@ exports.wrapPlain = wrapPlain;
7
7
  exports.renderOverwriteFrame = renderOverwriteFrame;
8
8
  exports.renderOuroMasthead = renderOuroMasthead;
9
9
  exports.formatActionActorLabel = formatActionActorLabel;
10
+ exports.renderTerminalWizard = renderTerminalWizard;
10
11
  exports.renderTerminalBoard = renderTerminalBoard;
12
+ exports.renderTerminalGuide = renderTerminalGuide;
11
13
  exports.renderTerminalOperation = renderTerminalOperation;
12
14
  const runtime_1 = require("../../nerves/runtime");
13
15
  const RESET = "\x1b[0m";
@@ -145,6 +147,154 @@ function renderOuroMasthead(options) {
145
147
  function formatActionActorLabel(actor) {
146
148
  return actor.replace(/-/g, " ");
147
149
  }
150
+ function formatWizardStatusLabel(status) {
151
+ return status;
152
+ }
153
+ function isQuietWizardStatus(status) {
154
+ return status === "ready" || status === "attached" || status === "not attached";
155
+ }
156
+ function renderWizardStatusBadge(status, isTTY) {
157
+ const symbol = status === "ready" || status === "attached"
158
+ ? "●"
159
+ : status === "not attached"
160
+ ? "◌"
161
+ : "◆";
162
+ const label = `${symbol} ${formatWizardStatusLabel(status)}`;
163
+ if (!isTTY)
164
+ return label;
165
+ if (status === "ready" || status === "attached")
166
+ return color(label, GLOW, true);
167
+ if (status === "not attached")
168
+ return color(label, MIST);
169
+ return color(label, ALERT, true);
170
+ }
171
+ function renderWizardActorBadge(actor, isTTY) {
172
+ const label = `[${formatActionActorLabel(actor)}]`;
173
+ if (!isTTY)
174
+ return label;
175
+ if (actor === "human-required")
176
+ return color(label, ALERT, true);
177
+ if (actor === "human-choice")
178
+ return color(label, SCALE, true);
179
+ return color(label, MIST);
180
+ }
181
+ function renderWizardRecommendedBadge(isTTY) {
182
+ if (!isTTY)
183
+ return "[recommended]";
184
+ return color("[recommended]", GLOW, true);
185
+ }
186
+ function wrapWizardDetailLines(lines, width, isTTY, tone) {
187
+ const rendered = [];
188
+ for (const line of lines) {
189
+ const wrapped = wrapPlain(line, width);
190
+ for (const segment of wrapped) {
191
+ rendered.push(isTTY ? color(segment, tone) : segment);
192
+ }
193
+ }
194
+ return rendered;
195
+ }
196
+ function renderWizardItem(item, width, isTTY) {
197
+ const badges = [
198
+ ...(item.status ? [renderWizardStatusBadge(item.status, isTTY)] : []),
199
+ ...(item.actor ? [renderWizardActorBadge(item.actor, isTTY)] : []),
200
+ ...(item.recommended ? [renderWizardRecommendedBadge(isTTY)] : []),
201
+ ];
202
+ const keyPrefix = item.key ? `${item.key}. ` : "";
203
+ const header = `${keyPrefix}${item.label}${badges.length > 0 ? ` ${badges.join(" ")}` : ""}`;
204
+ const detailWidth = Math.max(18, width - 6);
205
+ const detailTone = item.status && !isQuietWizardStatus(item.status) ? BONE : MIST;
206
+ const lines = [isTTY ? color(header, BONE, true) : header];
207
+ if (item.summary) {
208
+ lines.push(...wrapWizardDetailLines([item.summary], detailWidth, isTTY, detailTone));
209
+ }
210
+ if (item.detailLines && item.detailLines.length > 0) {
211
+ lines.push(...wrapWizardDetailLines(item.detailLines, detailWidth, isTTY, detailTone));
212
+ }
213
+ if (item.command) {
214
+ lines.push(...wrapWizardDetailLines([`run: ${item.command}`], detailWidth, isTTY, MIST));
215
+ }
216
+ return lines.flatMap((line, index) => index === 0 ? [line] : [` ${line}`]);
217
+ }
218
+ function renderWizardSectionTTY(section, width) {
219
+ const lines = [];
220
+ if (section.summary) {
221
+ lines.push(...wrapWizardDetailLines([section.summary], Math.max(18, width - 4), true, MIST));
222
+ }
223
+ for (const [index, item] of section.items.entries()) {
224
+ if (index > 0)
225
+ lines.push("");
226
+ lines.push(...renderWizardItem(item, width, true));
227
+ }
228
+ return renderOperationSectionTTY(section.title, lines, width);
229
+ }
230
+ function renderWizardSectionPlain(section, width) {
231
+ const lines = [];
232
+ if (section.summary) {
233
+ lines.push(...wrapWizardDetailLines([section.summary], Math.max(18, width - 4), false, MIST));
234
+ }
235
+ for (const [index, item] of section.items.entries()) {
236
+ if (index > 0)
237
+ lines.push("");
238
+ lines.push(...renderWizardItem(item, width, false));
239
+ }
240
+ return renderOperationSectionPlain(section.title, lines);
241
+ }
242
+ function renderTerminalWizard(options) {
243
+ if (!options.suppressEvent) {
244
+ (0, runtime_1.emitNervesEvent)({
245
+ component: "daemon",
246
+ event: "daemon.terminal_wizard_rendered",
247
+ message: "rendered shared terminal wizard",
248
+ meta: {
249
+ title: options.title,
250
+ sections: options.sections?.length ?? 0,
251
+ items: options.sections?.reduce((count, section) => count + section.items.length, 0) ?? 0,
252
+ hasNextStep: !!options.nextStep,
253
+ tty: options.isTTY,
254
+ },
255
+ });
256
+ }
257
+ const width = boardWidth(options.columns);
258
+ const blocks = [];
259
+ blocks.push(renderOuroMasthead({
260
+ isTTY: options.isTTY,
261
+ columns: width,
262
+ subtitle: options.masthead?.subtitle,
263
+ }).trimEnd());
264
+ const introLines = [
265
+ options.isTTY ? color(options.title, BONE, true) : options.title,
266
+ ...(options.summary
267
+ ? wrapPlain(options.summary, Math.max(20, width - 2)).map((line) => options.isTTY ? color(line, MIST) : line)
268
+ : []),
269
+ ];
270
+ blocks.push(introLines.join("\n"));
271
+ if (options.nextStep) {
272
+ const nextStepLines = [
273
+ options.isTTY ? color(options.nextStep.label, BONE, true) : options.nextStep.label,
274
+ ...(options.nextStep.detail
275
+ ? wrapPlain(options.nextStep.detail, Math.max(18, width - 4)).map((line) => options.isTTY ? color(line, MIST) : line)
276
+ : []),
277
+ ...(options.nextStep.command
278
+ ? wrapPlain(`run: ${options.nextStep.command}`, Math.max(18, width - 4)).map((line) => options.isTTY ? color(line, MIST) : line)
279
+ : []),
280
+ ];
281
+ blocks.push((options.isTTY
282
+ ? renderOperationSectionTTY("Recommended next step", nextStepLines, width)
283
+ : renderOperationSectionPlain("Recommended next step", nextStepLines)).join("\n"));
284
+ }
285
+ for (const section of options.sections ?? []) {
286
+ blocks.push((options.isTTY
287
+ ? renderWizardSectionTTY(section, width)
288
+ : renderWizardSectionPlain(section, width)).join("\n"));
289
+ }
290
+ if (options.footerLines && options.footerLines.length > 0) {
291
+ blocks.push(options.footerLines.map((line) => options.isTTY ? color(line, MIST) : line).join("\n"));
292
+ }
293
+ if (options.prompt) {
294
+ blocks.push(options.isTTY ? color(options.prompt, BONE, true) : options.prompt);
295
+ }
296
+ return `${blocks.join("\n\n")}\n`;
297
+ }
148
298
  function renderActionLine(action) {
149
299
  const chips = [`[${formatActionActorLabel(action.actor)}]`];
150
300
  if (action.recommended)
@@ -195,6 +345,56 @@ function renderTerminalBoard(options) {
195
345
  }
196
346
  return `${blocks.join("\n\n")}\n`;
197
347
  }
348
+ function renderTerminalGuide(options) {
349
+ if (!options.suppressEvent) {
350
+ (0, runtime_1.emitNervesEvent)({
351
+ component: "daemon",
352
+ event: "daemon.terminal_guide_rendered",
353
+ message: "rendered shared terminal guide",
354
+ meta: {
355
+ title: options.title,
356
+ sections: options.sections?.length ?? 0,
357
+ actions: options.actions?.length ?? 0,
358
+ tty: options.isTTY,
359
+ },
360
+ });
361
+ }
362
+ const width = boardWidth(options.columns);
363
+ const blocks = [];
364
+ blocks.push(renderOuroMasthead({
365
+ isTTY: options.isTTY,
366
+ columns: width,
367
+ subtitle: options.masthead?.subtitle,
368
+ }).trimEnd());
369
+ const introLines = [
370
+ options.isTTY ? color(options.title, BONE, true) : options.title,
371
+ ...(options.summary
372
+ ? wrapPlain(options.summary, Math.max(20, width - 2)).map((line) => options.isTTY ? color(line, MIST) : line)
373
+ : []),
374
+ ];
375
+ blocks.push(introLines.join("\n"));
376
+ for (const section of options.sections ?? []) {
377
+ const lines = section.lines.map((line) => options.isTTY ? color(line, BONE) : line);
378
+ blocks.push((options.isTTY
379
+ ? renderOperationSectionTTY(section.title, lines, width)
380
+ : renderOperationSectionPlain(section.title, lines)).join("\n"));
381
+ }
382
+ const actionList = options.actions ?? [];
383
+ if (actionList.length > 0) {
384
+ const lines = [];
385
+ for (const [index, action] of actionList.entries()) {
386
+ lines.push(options.isTTY ? color(`${index + 1}. ${renderActionLine(action)}`, BONE, true) : `${index + 1}. ${renderActionLine(action)}`);
387
+ lines.push(options.isTTY ? color(`run: ${action.command}`, MIST) : `run: ${action.command}`);
388
+ }
389
+ blocks.push((options.isTTY
390
+ ? renderOperationSectionTTY("Next moves", lines, width)
391
+ : renderOperationSectionPlain("Next moves", lines)).join("\n"));
392
+ }
393
+ if (options.prompt) {
394
+ blocks.push(options.isTTY ? color(options.prompt, BONE, true) : options.prompt);
395
+ }
396
+ return `${blocks.join("\n\n")}\n`;
397
+ }
198
398
  function formatOperationStep(step) {
199
399
  const marker = step.status === "done"
200
400
  ? "✓"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.445",
3
+ "version": "0.1.0-alpha.446",
4
4
  "main": "dist/heart/daemon/ouro-entry.js",
5
5
  "bin": {
6
6
  "cli": "dist/heart/daemon/ouro-bot-entry.js",