@ouro.bot/cli 0.1.0-alpha.431 → 0.1.0-alpha.433

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.
@@ -76,6 +76,7 @@ const provider_state_1 = require("../provider-state");
76
76
  const machine_identity_1 = require("../machine-identity");
77
77
  const provider_models_1 = require("../provider-models");
78
78
  const ouro_version_manager_1 = require("../versioning/ouro-version-manager");
79
+ const update_checker_1 = require("../versioning/update-checker");
79
80
  const sync_1 = require("../sync");
80
81
  const cli_parse_1 = require("./cli-parse");
81
82
  const cli_parse_2 = require("./cli-parse");
@@ -104,6 +105,55 @@ const DEFAULT_DAEMON_STARTUP_POLL_INTERVAL_MS = 500;
104
105
  const DEFAULT_DAEMON_STARTUP_STABILITY_WINDOW_MS = 1_500;
105
106
  const DEFAULT_DAEMON_STARTUP_RETRY_LIMIT = 1;
106
107
  const DEFAULT_DAEMON_STARTUP_LOG_LINES = 10;
108
+ function summarizeCliUpdateCheckStatus(error, timedOut = false) {
109
+ const normalized = error.trim().toLowerCase();
110
+ if (timedOut || normalized.includes("timed out") || normalized.includes("abort")) {
111
+ return "skipped; registry did not answer";
112
+ }
113
+ if (normalized.includes("registry unavailable")) {
114
+ return "skipped; registry unavailable";
115
+ }
116
+ if (normalized.includes("network") ||
117
+ normalized.includes("fetch failed") ||
118
+ normalized.includes("enotfound") ||
119
+ normalized.includes("eai_again") ||
120
+ normalized.includes("econnreset")) {
121
+ return "skipped; registry unavailable";
122
+ }
123
+ return "skipped; update check unavailable";
124
+ }
125
+ async function runCliUpdateCheckWithTimeout(checkForCliUpdate, timeoutMs = update_checker_1.CLI_UPDATE_CHECK_TIMEOUT_MS) {
126
+ return await new Promise((resolve, reject) => {
127
+ let settled = false;
128
+ const timeoutId = setTimeout(() => {
129
+ settled = true;
130
+ resolve({
131
+ timedOut: true,
132
+ result: {
133
+ available: false,
134
+ error: `update check timed out after ${Math.max(1, Math.round(timeoutMs / 1000))}s`,
135
+ },
136
+ });
137
+ }, timeoutMs);
138
+ /* v8 ignore start -- Promise-settlement wiring is exercised by command tests; v8 misses these callback lines @preserve */
139
+ void checkForCliUpdate()
140
+ .then((result) => {
141
+ if (settled)
142
+ return;
143
+ settled = true;
144
+ clearTimeout(timeoutId);
145
+ resolve({ result, timedOut: false });
146
+ })
147
+ .catch((error) => {
148
+ if (settled)
149
+ return;
150
+ settled = true;
151
+ clearTimeout(timeoutId);
152
+ reject(error);
153
+ });
154
+ /* v8 ignore stop */
155
+ });
156
+ }
107
157
  async function checkAgentProviders(deps, agentsOverride, onProgress) {
108
158
  const agents = agentsOverride ?? await listCliAgents(deps);
109
159
  const bundlesRoot = deps.bundlesRoot ?? (0, identity_1.getAgentBundlesRoot)();
@@ -376,15 +426,14 @@ function ttyBoardEnabled(deps) {
376
426
  return deps.isTTY ?? process.stdout.isTTY === true;
377
427
  }
378
428
  function renderCommandBoard(deps, options) {
379
- return (0, terminal_ui_1.renderTerminalBoard)({
380
- isTTY: true,
381
- columns: deps.stdoutColumns ?? process.stdout.columns,
382
- masthead: {
383
- subtitle: options.subtitle,
384
- },
429
+ return (0, human_command_screens_1.renderHumanCommandBoard)({
385
430
  title: options.title,
431
+ subtitle: options.subtitle,
386
432
  summary: options.summary,
433
+ isTTY: true,
434
+ columns: deps.stdoutColumns ?? process.stdout.columns,
387
435
  sections: options.sections,
436
+ actions: options.actions,
388
437
  }).trimEnd();
389
438
  }
390
439
  function writeConnectorIntro(deps, options) {
@@ -398,6 +447,57 @@ function writeConnectorIntro(deps, options) {
398
447
  : options.fallbackLines.join("\n");
399
448
  deps.writeStdout(text);
400
449
  }
450
+ function formatLocalUnlockStoreLine(store) {
451
+ return `local unlock store: ${store.kind}${store.secure ? "" : " (explicit plaintext fallback)"}`;
452
+ }
453
+ function writeCommandOutcome(deps, options) {
454
+ const message = ttyBoardEnabled(deps)
455
+ ? renderCommandBoard(deps, {
456
+ title: options.title,
457
+ subtitle: options.subtitle,
458
+ summary: options.summary,
459
+ sections: options.sections,
460
+ })
461
+ : options.fallbackLines.join("\n");
462
+ deps.writeStdout(message);
463
+ return message;
464
+ }
465
+ function writeCapabilityOutcome(deps, options) {
466
+ return writeCommandOutcome(deps, {
467
+ title: "Capability connected",
468
+ subtitle: options.subtitle,
469
+ summary: options.summary,
470
+ sections: [
471
+ {
472
+ title: "What changed",
473
+ lines: options.whatChanged,
474
+ },
475
+ {
476
+ title: "Next moves",
477
+ lines: options.nextMoves,
478
+ },
479
+ ],
480
+ fallbackLines: options.fallbackLines,
481
+ });
482
+ }
483
+ function writeCapabilityAttention(deps, options) {
484
+ return writeCommandOutcome(deps, {
485
+ title: "Capability needs attention",
486
+ subtitle: options.subtitle,
487
+ summary: options.summary,
488
+ sections: [
489
+ {
490
+ title: "What changed",
491
+ lines: options.whatChanged,
492
+ },
493
+ {
494
+ title: "Next moves",
495
+ lines: options.nextMoves,
496
+ },
497
+ ],
498
+ fallbackLines: options.fallbackLines,
499
+ });
500
+ }
401
501
  async function promptForNamedAgent(title, subtitle, agents, deps) {
402
502
  if (!deps.promptInput)
403
503
  throw new Error("agent selection requires interactive input");
@@ -906,10 +1006,6 @@ function pushAgentBundleAfterCliMutation(agent, deps) {
906
1006
  }
907
1007
  return `bundle sync: could not push bundle changes (${result.error})`;
908
1008
  }
909
- function appendBundleSyncSummary(message, agent, deps) {
910
- const syncSummary = pushAgentBundleAfterCliMutation(agent, deps);
911
- return syncSummary ? `${message}\n${syncSummary}` : message;
912
- }
913
1009
  function writeAgentVaultConfig(agentName, configPath, config, vault) {
914
1010
  const nextConfig = {
915
1011
  ...config,
@@ -1094,13 +1190,34 @@ async function executeVaultUnlock(command, deps) {
1094
1190
  finally {
1095
1191
  progress.end();
1096
1192
  }
1097
- const message = [
1193
+ const fallbackLines = [
1098
1194
  `vault unlocked for ${command.agent} on this machine`,
1099
1195
  `vault: ${vault.email} at ${vault.serverUrl}`,
1100
- `local unlock store: ${store.kind}${store.secure ? "" : " (explicit plaintext fallback)"}`,
1101
- ].join("\n");
1102
- deps.writeStdout(message);
1103
- return message;
1196
+ formatLocalUnlockStoreLine(store),
1197
+ ];
1198
+ return writeCommandOutcome(deps, {
1199
+ title: "Credential vault",
1200
+ subtitle: `${command.agent}'s vault is open on this machine again.`,
1201
+ summary: `This machine can now open ${command.agent}'s vault without re-entering the unlock secret every request.`,
1202
+ sections: [
1203
+ {
1204
+ title: "What changed",
1205
+ lines: [
1206
+ `Vault: ${vault.email} at ${vault.serverUrl}`,
1207
+ formatLocalUnlockStoreLine(store),
1208
+ "secret was not printed",
1209
+ ],
1210
+ },
1211
+ {
1212
+ title: "Next moves",
1213
+ lines: [
1214
+ `Run ouro up to let the house retry ${command.agent}.`,
1215
+ `If credentials are still missing, run ouro auth --agent ${command.agent} --provider <provider>.`,
1216
+ ],
1217
+ },
1218
+ ],
1219
+ fallbackLines,
1220
+ });
1104
1221
  }
1105
1222
  async function executeVaultCreate(command, deps) {
1106
1223
  if (command.agent === "SerpentGuide") {
@@ -1153,15 +1270,40 @@ async function executeVaultCreate(command, deps) {
1153
1270
  /* v8 ignore next -- defensive: success path assigns store before continuing @preserve */
1154
1271
  if (!store)
1155
1272
  throw new Error(`vault create failed for ${command.agent}: local unlock material was not saved`);
1156
- const message = appendBundleSyncSummary([
1273
+ const syncSummary = pushAgentBundleAfterCliMutation(command.agent, deps);
1274
+ const fallbackLines = [
1157
1275
  `vault created for ${command.agent}`,
1158
1276
  `vault: ${email} at ${serverUrl}`,
1159
- `local unlock store: ${store.kind}${store.secure ? "" : " (explicit plaintext fallback)"}`,
1277
+ formatLocalUnlockStoreLine(store),
1160
1278
  "All raw credentials for this agent will be stored in this Ouro credential vault.",
1161
1279
  "Keep the vault unlock secret saved outside Ouro. Another machine will need it once.",
1162
- ].join("\n"), command.agent, deps);
1163
- deps.writeStdout(message);
1164
- return message;
1280
+ ...(syncSummary ? [syncSummary] : []),
1281
+ ];
1282
+ return writeCommandOutcome(deps, {
1283
+ title: "Credential vault",
1284
+ subtitle: `${command.agent} has a vault home now.`,
1285
+ summary: `All raw credentials for ${command.agent} now live in this vault.`,
1286
+ sections: [
1287
+ {
1288
+ title: "What changed",
1289
+ lines: [
1290
+ `Vault: ${email} at ${serverUrl}`,
1291
+ formatLocalUnlockStoreLine(store),
1292
+ "This vault will hold provider and runtime credentials for the agent.",
1293
+ "secret was not printed",
1294
+ ...(syncSummary ? [syncSummary] : []),
1295
+ ],
1296
+ },
1297
+ {
1298
+ title: "Next moves",
1299
+ lines: [
1300
+ `Authenticate the providers ${command.agent} should use with ouro auth --agent ${command.agent} --provider <provider>.`,
1301
+ `Then run ouro up so the house can bring ${command.agent} online.`,
1302
+ ],
1303
+ },
1304
+ ],
1305
+ fallbackLines,
1306
+ });
1165
1307
  }
1166
1308
  async function executeVaultReplace(command, deps) {
1167
1309
  if (command.agent === "SerpentGuide") {
@@ -1204,16 +1346,42 @@ async function executeVaultReplace(command, deps) {
1204
1346
  throw new Error(`vault replace failed for ${command.agent}: no vault repair result`);
1205
1347
  if (!repair.ok)
1206
1348
  return repair.message;
1207
- const message = appendBundleSyncSummary([
1349
+ const syncSummary = pushAgentBundleAfterCliMutation(command.agent, deps);
1350
+ const fallbackLines = [
1208
1351
  `vault replaced for ${command.agent}`,
1209
1352
  `vault: ${email} at ${serverUrl}`,
1210
- `local unlock store: ${repair.store.kind}${repair.store.secure ? "" : " (explicit plaintext fallback)"}`,
1353
+ formatLocalUnlockStoreLine(repair.store),
1211
1354
  "imported: none",
1212
1355
  `next: ouro repair --agent ${command.agent}`,
1213
1356
  "Keep the vault unlock secret saved outside Ouro. Another machine will need it once.",
1214
- ].join("\n"), command.agent, deps);
1215
- deps.writeStdout(message);
1216
- return message;
1357
+ ...(syncSummary ? [syncSummary] : []),
1358
+ ];
1359
+ return writeCommandOutcome(deps, {
1360
+ title: "Credential vault",
1361
+ subtitle: `${command.agent} has a fresh vault.`,
1362
+ summary: `${command.agent} now has a fresh empty vault.`,
1363
+ sections: [
1364
+ {
1365
+ title: "What changed",
1366
+ lines: [
1367
+ `Vault: ${email} at ${serverUrl}`,
1368
+ formatLocalUnlockStoreLine(repair.store),
1369
+ "Imported: none",
1370
+ "secret was not printed",
1371
+ ...(syncSummary ? [syncSummary] : []),
1372
+ ],
1373
+ },
1374
+ {
1375
+ title: "Next moves",
1376
+ lines: [
1377
+ `Re-enter provider credentials with ouro auth --agent ${command.agent} --provider <provider>.`,
1378
+ `Re-enter runtime secrets with ouro connect --agent ${command.agent} or ouro vault config set --agent ${command.agent} --key <field>.`,
1379
+ `Then run ouro repair --agent ${command.agent} or ouro up.`,
1380
+ ],
1381
+ },
1382
+ ],
1383
+ fallbackLines,
1384
+ });
1217
1385
  }
1218
1386
  async function executeVaultRecover(command, deps) {
1219
1387
  if (command.agent === "SerpentGuide") {
@@ -1290,19 +1458,47 @@ async function executeVaultRecover(command, deps) {
1290
1458
  progress.end();
1291
1459
  }
1292
1460
  const providerList = [...importedProviders].sort();
1293
- const message = appendBundleSyncSummary([
1461
+ const syncSummary = pushAgentBundleAfterCliMutation(command.agent, deps);
1462
+ const fallbackLines = [
1294
1463
  `vault recovered for ${command.agent}`,
1295
1464
  `vault: ${email} at ${serverUrl}`,
1296
- `local unlock store: ${repair.store.kind}${repair.store.secure ? "" : " (explicit plaintext fallback)"}`,
1465
+ formatLocalUnlockStoreLine(repair.store),
1297
1466
  `sources imported: ${sourceImports.length}`,
1298
1467
  `provider credentials imported: ${providerList.length === 0 ? "none" : providerList.join(", ")}`,
1299
1468
  `runtime credentials imported: ${runtimeFields.length === 0 ? "none" : runtimeFields.join(", ")}`,
1300
1469
  `machine runtime credentials imported: ${machineRuntimeFields.length === 0 ? "none" : machineRuntimeFields.join(", ")}`,
1301
1470
  "credential values were not printed",
1302
1471
  "Keep the vault unlock secret saved outside Ouro. Another machine will need it once.",
1303
- ].join("\n"), command.agent, deps);
1304
- deps.writeStdout(message);
1305
- return message;
1472
+ ...(syncSummary ? [syncSummary] : []),
1473
+ ];
1474
+ return writeCommandOutcome(deps, {
1475
+ title: "Credential vault",
1476
+ subtitle: `${command.agent}'s vault is back in service.`,
1477
+ summary: `Recovered credentials have been moved back into ${command.agent}'s vault.`,
1478
+ sections: [
1479
+ {
1480
+ title: "What changed",
1481
+ lines: [
1482
+ `Vault: ${email} at ${serverUrl}`,
1483
+ formatLocalUnlockStoreLine(repair.store),
1484
+ `Sources imported: ${sourceImports.length}`,
1485
+ `Provider credentials: ${providerList.length === 0 ? "none" : providerList.join(", ")}`,
1486
+ `Runtime credentials: ${runtimeFields.length === 0 ? "none" : runtimeFields.join(", ")}`,
1487
+ `Machine runtime credentials: ${machineRuntimeFields.length === 0 ? "none" : machineRuntimeFields.join(", ")}`,
1488
+ "secret was not printed",
1489
+ ...(syncSummary ? [syncSummary] : []),
1490
+ ],
1491
+ },
1492
+ {
1493
+ title: "Next moves",
1494
+ lines: [
1495
+ `Run ouro auth verify --agent ${command.agent} to re-check the stored providers.`,
1496
+ `Then run ouro up so the house can retry ${command.agent}.`,
1497
+ ],
1498
+ },
1499
+ ],
1500
+ fallbackLines,
1501
+ });
1306
1502
  }
1307
1503
  async function executeVaultStatus(command, deps) {
1308
1504
  if (command.agent === "SerpentGuide") {
@@ -1931,16 +2127,27 @@ async function executeConnectPerplexity(agent, deps) {
1931
2127
  if (!verification.ok) {
1932
2128
  progress.failPhase("verifying Perplexity search", verification.summary);
1933
2129
  progress.end();
1934
- const message = [
1935
- `Perplexity key was saved for ${agent}, but the live check failed.`,
1936
- `stored: ${stored.itemPath}`,
1937
- `live check: ${verification.summary}`,
1938
- "secret was not printed",
1939
- "",
1940
- `Next: rerun \`ouro connect perplexity --agent ${agent}\` with a working key.`,
1941
- ].join("\n");
1942
- deps.writeStdout(message);
1943
- return message;
2130
+ return writeCapabilityAttention(deps, {
2131
+ subtitle: `${agent}'s Perplexity key needs another pass.`,
2132
+ summary: "Perplexity search was saved, but the live check failed.",
2133
+ whatChanged: [
2134
+ `Stored: ${stored.itemPath}`,
2135
+ `Live check: ${verification.summary}`,
2136
+ "secret was not printed",
2137
+ ],
2138
+ nextMoves: [
2139
+ `Rerun ouro connect perplexity --agent ${agent} with a working key.`,
2140
+ `Use ouro connect --agent ${agent} to review the rest of the capability bay.`,
2141
+ ],
2142
+ fallbackLines: [
2143
+ `Perplexity key was saved for ${agent}, but the live check failed.`,
2144
+ `stored: ${stored.itemPath}`,
2145
+ `live check: ${verification.summary}`,
2146
+ "secret was not printed",
2147
+ "",
2148
+ `Next: rerun \`ouro connect perplexity --agent ${agent}\` with a working key.`,
2149
+ ],
2150
+ });
1944
2151
  }
1945
2152
  progress.completePhase("verifying Perplexity search", verification.summary);
1946
2153
  reload = await runCommandProgressPhase(progress, `applying change to running ${agent}`, () => applyRuntimeChangeToRunningAgent(agent, deps, (message) => progress.updateDetail(message)), (result) => result);
@@ -1948,17 +2155,29 @@ async function executeConnectPerplexity(agent, deps) {
1948
2155
  finally {
1949
2156
  progress.end();
1950
2157
  }
1951
- const message = [
1952
- `Perplexity connected for ${agent}`,
1953
- "capability: Perplexity search",
1954
- `stored: ${stored.itemPath}`,
1955
- `running agent: ${reload}`,
1956
- "secret was not printed",
1957
- "",
1958
- "Next: ask the agent to search.",
1959
- ].join("\n");
1960
- deps.writeStdout(message);
1961
- return message;
2158
+ return writeCapabilityOutcome(deps, {
2159
+ subtitle: `${agent}'s search compass is online.`,
2160
+ summary: `Perplexity search is ready to travel with ${agent}.`,
2161
+ whatChanged: [
2162
+ `Capability: Perplexity search`,
2163
+ `Stored: ${stored.itemPath}`,
2164
+ `Running agent: ${reload}`,
2165
+ "secret was not printed",
2166
+ ],
2167
+ nextMoves: [
2168
+ "Ask the agent to search.",
2169
+ `Reopen the connect bay with ouro connect --agent ${agent} whenever you want to review capabilities.`,
2170
+ ],
2171
+ fallbackLines: [
2172
+ `Perplexity connected for ${agent}`,
2173
+ "capability: Perplexity search",
2174
+ `stored: ${stored.itemPath}`,
2175
+ `running agent: ${reload}`,
2176
+ "secret was not printed",
2177
+ "",
2178
+ "Next: ask the agent to search.",
2179
+ ],
2180
+ });
1962
2181
  }
1963
2182
  async function executeConnectEmbeddings(agent, deps) {
1964
2183
  if (agent === "SerpentGuide") {
@@ -2018,16 +2237,27 @@ async function executeConnectEmbeddings(agent, deps) {
2018
2237
  if (!verification.ok) {
2019
2238
  progress.failPhase("verifying memory embeddings", verification.summary);
2020
2239
  progress.end();
2021
- const message = [
2022
- `Embeddings key was saved for ${agent}, but the live check failed.`,
2023
- `stored: ${stored.itemPath}`,
2024
- `live check: ${verification.summary}`,
2025
- "secret was not printed",
2026
- "",
2027
- `Next: rerun \`ouro connect embeddings --agent ${agent}\` with a working key.`,
2028
- ].join("\n");
2029
- deps.writeStdout(message);
2030
- return message;
2240
+ return writeCapabilityAttention(deps, {
2241
+ subtitle: `${agent}'s memory key needs another pass.`,
2242
+ summary: "Memory embeddings were saved, but the live check failed.",
2243
+ whatChanged: [
2244
+ `Stored: ${stored.itemPath}`,
2245
+ `Live check: ${verification.summary}`,
2246
+ "secret was not printed",
2247
+ ],
2248
+ nextMoves: [
2249
+ `Rerun ouro connect embeddings --agent ${agent} with a working key.`,
2250
+ `Use ouro connect --agent ${agent} to review the rest of the capability bay.`,
2251
+ ],
2252
+ fallbackLines: [
2253
+ `Embeddings key was saved for ${agent}, but the live check failed.`,
2254
+ `stored: ${stored.itemPath}`,
2255
+ `live check: ${verification.summary}`,
2256
+ "secret was not printed",
2257
+ "",
2258
+ `Next: rerun \`ouro connect embeddings --agent ${agent}\` with a working key.`,
2259
+ ],
2260
+ });
2031
2261
  }
2032
2262
  progress.completePhase("verifying memory embeddings", verification.summary);
2033
2263
  reload = await runCommandProgressPhase(progress, `applying change to running ${agent}`, () => applyRuntimeChangeToRunningAgent(agent, deps, (message) => progress.updateDetail(message)), (result) => result);
@@ -2035,17 +2265,29 @@ async function executeConnectEmbeddings(agent, deps) {
2035
2265
  finally {
2036
2266
  progress.end();
2037
2267
  }
2038
- const message = [
2039
- `Embeddings connected for ${agent}`,
2040
- "capability: memory embeddings",
2041
- `stored: ${stored.itemPath}`,
2042
- `running agent: ${reload}`,
2043
- "secret was not printed",
2044
- "",
2045
- "Next: ask the agent to search notes or memory.",
2046
- ].join("\n");
2047
- deps.writeStdout(message);
2048
- return message;
2268
+ return writeCapabilityOutcome(deps, {
2269
+ subtitle: `${agent}'s memory index is online.`,
2270
+ summary: `Memory embeddings are ready to travel with ${agent}.`,
2271
+ whatChanged: [
2272
+ "Capability: memory embeddings",
2273
+ `Stored: ${stored.itemPath}`,
2274
+ `Running agent: ${reload}`,
2275
+ "secret was not printed",
2276
+ ],
2277
+ nextMoves: [
2278
+ "Ask the agent to search notes or memory.",
2279
+ `Reopen the connect bay with ouro connect --agent ${agent} whenever you want to review capabilities.`,
2280
+ ],
2281
+ fallbackLines: [
2282
+ `Embeddings connected for ${agent}`,
2283
+ "capability: memory embeddings",
2284
+ `stored: ${stored.itemPath}`,
2285
+ `running agent: ${reload}`,
2286
+ "secret was not printed",
2287
+ "",
2288
+ "Next: ask the agent to search notes or memory.",
2289
+ ],
2290
+ });
2049
2291
  }
2050
2292
  async function executeConnectTeams(agent, deps) {
2051
2293
  if (agent === "SerpentGuide") {
@@ -2053,11 +2295,38 @@ async function executeConnectTeams(agent, deps) {
2053
2295
  }
2054
2296
  const promptInput = requirePromptInput(deps, "Teams setup");
2055
2297
  const promptSecret = requirePromptSecret(deps, "Teams client secret entry");
2056
- deps.writeStdout([
2057
- `Connect Teams for ${agent}`,
2058
- "This connects the Teams sense.",
2059
- "The client secret stays hidden while you type.",
2060
- ].join("\n"));
2298
+ writeConnectorIntro(deps, {
2299
+ title: "Connect Teams",
2300
+ subtitle: `${agent} gets a portable Teams sense.`,
2301
+ summary: "Add the app credentials once, keep the secret hidden, and let Teams travel with this agent.",
2302
+ sections: [
2303
+ {
2304
+ title: "Unlocks",
2305
+ lines: [
2306
+ "Microsoft Teams messages and actions inside Ouro.",
2307
+ ],
2308
+ },
2309
+ {
2310
+ title: "What you need",
2311
+ lines: [
2312
+ "A Teams client ID, client secret, and tenant ID.",
2313
+ "The client secret stays hidden while you type.",
2314
+ ],
2315
+ },
2316
+ {
2317
+ title: "Where it lives",
2318
+ lines: [
2319
+ `${agent}'s vault runtime/config item.`,
2320
+ "It travels with the agent across machines.",
2321
+ ],
2322
+ },
2323
+ ],
2324
+ fallbackLines: [
2325
+ `Connect Teams for ${agent}`,
2326
+ "The client secret stays hidden while you type.",
2327
+ `Ouro stores the setup in ${agent}'s vault runtime/config item.`,
2328
+ ],
2329
+ });
2061
2330
  const clientId = (await promptInput("Teams client ID: ")).trim();
2062
2331
  if (!clientId)
2063
2332
  throw new Error("Teams client ID cannot be blank");
@@ -2089,17 +2358,32 @@ async function executeConnectTeams(agent, deps) {
2089
2358
  finally {
2090
2359
  progress.end();
2091
2360
  }
2092
- const message = appendBundleSyncSummary([
2093
- `Teams connected for ${agent}`,
2094
- "capability: Teams sense",
2095
- `stored: ${stored.itemPath}`,
2096
- "agent.json: senses.teams.enabled = true",
2097
- "secret was not printed",
2098
- "",
2099
- "Next: run `ouro up` so the daemon picks up the Teams sense change.",
2100
- ].join("\n"), agent, deps);
2101
- deps.writeStdout(message);
2102
- return message;
2361
+ const syncSummary = pushAgentBundleAfterCliMutation(agent, deps);
2362
+ return writeCapabilityOutcome(deps, {
2363
+ subtitle: `${agent}'s Teams sense is configured.`,
2364
+ summary: `Teams is ready for ${agent}.`,
2365
+ whatChanged: [
2366
+ "Capability: Teams sense",
2367
+ `Stored: ${stored.itemPath}`,
2368
+ "agent.json: senses.teams.enabled = true",
2369
+ "secret was not printed",
2370
+ ...(syncSummary ? [syncSummary] : []),
2371
+ ],
2372
+ nextMoves: [
2373
+ "Run ouro up so the daemon picks up the Teams sense change.",
2374
+ `Reopen the connect bay with ouro connect --agent ${agent} whenever you want to review capabilities.`,
2375
+ ],
2376
+ fallbackLines: [
2377
+ `Teams connected for ${agent}`,
2378
+ "capability: Teams sense",
2379
+ `stored: ${stored.itemPath}`,
2380
+ "agent.json: senses.teams.enabled = true",
2381
+ "secret was not printed",
2382
+ "",
2383
+ "Next: run `ouro up` so the daemon picks up the Teams sense change.",
2384
+ ...(syncSummary ? [syncSummary] : []),
2385
+ ],
2386
+ });
2103
2387
  }
2104
2388
  async function executeConnectBlueBubbles(agent, deps) {
2105
2389
  if (agent === "SerpentGuide") {
@@ -2107,11 +2391,38 @@ async function executeConnectBlueBubbles(agent, deps) {
2107
2391
  }
2108
2392
  const promptInput = requirePromptInput(deps, "BlueBubbles setup");
2109
2393
  const promptSecret = requirePromptSecret(deps, "BlueBubbles password entry");
2110
- deps.writeStdout([
2111
- `Connect BlueBubbles for ${agent}`,
2112
- "This is a local attachment for this machine.",
2113
- "The app password stays hidden while you type.",
2114
- ].join("\n"));
2394
+ writeConnectorIntro(deps, {
2395
+ title: "Connect BlueBubbles",
2396
+ subtitle: `${agent} gets a local Messages bridge on this machine.`,
2397
+ summary: "Attach this Mac's BlueBubbles bridge without pretending it travels anywhere else.",
2398
+ sections: [
2399
+ {
2400
+ title: "Unlocks",
2401
+ lines: [
2402
+ "Local iMessage bridge access on this machine only.",
2403
+ ],
2404
+ },
2405
+ {
2406
+ title: "What you need",
2407
+ lines: [
2408
+ "A BlueBubbles server URL and app password.",
2409
+ "The app password stays hidden while you type.",
2410
+ ],
2411
+ },
2412
+ {
2413
+ title: "Where it lives",
2414
+ lines: [
2415
+ `${agent}'s machine runtime config for this machine.`,
2416
+ "It does not travel to other machines unless you attach them too.",
2417
+ ],
2418
+ },
2419
+ ],
2420
+ fallbackLines: [
2421
+ `Connect BlueBubbles for ${agent}`,
2422
+ "This is a local attachment for this machine.",
2423
+ "The app password stays hidden while you type.",
2424
+ ],
2425
+ });
2115
2426
  const serverUrl = (await promptInput("BlueBubbles server URL for this machine: ")).trim();
2116
2427
  if (!serverUrl)
2117
2428
  throw new Error("BlueBubbles server URL cannot be blank");
@@ -2156,17 +2467,32 @@ async function executeConnectBlueBubbles(agent, deps) {
2156
2467
  progress.end();
2157
2468
  throw error;
2158
2469
  }
2159
- const message = appendBundleSyncSummary([
2160
- `BlueBubbles attached for ${agent} on this machine`,
2161
- `machine: ${machineId}`,
2162
- `stored: ${stored.itemPath}`,
2163
- "agent.json: senses.bluebubbles.enabled = true",
2164
- "secret was not printed",
2165
- "",
2166
- "Next: point BlueBubbles at this machine's webhook, then run `ouro up`.",
2167
- ].join("\n"), agent, deps);
2168
- deps.writeStdout(message);
2169
- return message;
2470
+ const syncSummary = pushAgentBundleAfterCliMutation(agent, deps);
2471
+ return writeCapabilityOutcome(deps, {
2472
+ subtitle: `${agent}'s local bridge is attached.`,
2473
+ summary: `BlueBubbles is attached on this machine for ${agent}.`,
2474
+ whatChanged: [
2475
+ `Machine: ${machineId}`,
2476
+ `Stored: ${stored.itemPath}`,
2477
+ "agent.json: senses.bluebubbles.enabled = true",
2478
+ "secret was not printed",
2479
+ ...(syncSummary ? [syncSummary] : []),
2480
+ ],
2481
+ nextMoves: [
2482
+ "Point BlueBubbles at this machine's webhook, then run ouro up.",
2483
+ `Attach other machines separately if ${agent} should use BlueBubbles there too.`,
2484
+ ],
2485
+ fallbackLines: [
2486
+ `BlueBubbles attached for ${agent} on this machine`,
2487
+ `machine: ${machineId}`,
2488
+ `stored: ${stored.itemPath}`,
2489
+ "agent.json: senses.bluebubbles.enabled = true",
2490
+ "secret was not printed",
2491
+ "",
2492
+ "Next: point BlueBubbles at this machine's webhook, then run `ouro up`.",
2493
+ ...(syncSummary ? [syncSummary] : []),
2494
+ ],
2495
+ });
2170
2496
  }
2171
2497
  async function executeConnectProviders(agent, deps) {
2172
2498
  const promptInput = deps.promptInput;
@@ -2613,27 +2939,51 @@ async function executeAuthRun(command, deps) {
2613
2939
  // Behavior: ouro auth stores credentials only — does NOT switch provider.
2614
2940
  // Use `ouro auth switch` to change the active provider.
2615
2941
  // Verify the credentials actually work by pinging the provider.
2942
+ let verificationStatus = "not checked";
2616
2943
  /* v8 ignore start -- integration: real API ping after auth @preserve */
2617
2944
  try {
2618
2945
  progress.startPhase(`verifying ${provider}`);
2619
2946
  const credential = await readProviderCredentialRecord(command.agent, provider, deps, {
2620
2947
  onProgress: (message) => progress.updateDetail(message),
2621
2948
  });
2622
- const status = credential.ok
2949
+ verificationStatus = credential.ok
2623
2950
  ? await verifyProviderCredentials(provider, {
2624
2951
  [provider]: { ...credential.record.config, ...credential.record.credentials },
2625
2952
  })
2626
2953
  : `stored but could not be re-read from vault (${credential.error})`;
2627
- progress.completePhase(`verifying ${provider}`, status);
2954
+ progress.completePhase(`verifying ${provider}`, verificationStatus);
2628
2955
  }
2629
2956
  catch (error) {
2630
2957
  // Verification failure is non-blocking — credentials were saved regardless.
2631
- progress.completePhase(`verifying ${provider}`, `skipped (${error instanceof Error ? error.message : String(error)})`);
2958
+ verificationStatus = `skipped (${error instanceof Error ? error.message : String(error)})`;
2959
+ progress.completePhase(`verifying ${provider}`, verificationStatus);
2632
2960
  }
2633
2961
  /* v8 ignore stop */
2634
2962
  progress.end();
2635
- deps.writeStdout(result.message);
2636
- return result.message;
2963
+ return writeCommandOutcome(deps, {
2964
+ title: "Provider auth",
2965
+ subtitle: `${command.agent} just refreshed ${provider}.`,
2966
+ summary: `${command.agent} can now use ${provider} when this lane is selected.`,
2967
+ sections: [
2968
+ {
2969
+ title: "What changed",
2970
+ lines: [
2971
+ `Stored in: ${result.credentialPath}`,
2972
+ `Provider: ${provider}`,
2973
+ `Live check: ${verificationStatus}`,
2974
+ "secret was not printed",
2975
+ ],
2976
+ },
2977
+ {
2978
+ title: "Next moves",
2979
+ lines: [
2980
+ `Choose it for a lane with ouro use --agent ${command.agent} --lane <outward|inner> --provider ${provider} --model <model>.`,
2981
+ `Double-check it any time with ouro auth verify --agent ${command.agent} --provider ${provider}.`,
2982
+ ],
2983
+ },
2984
+ ],
2985
+ fallbackLines: [result.message],
2986
+ });
2637
2987
  }
2638
2988
  async function readinessReportForAgent(agent, deps) {
2639
2989
  const bundlesRoot = deps.bundlesRoot ?? (0, identity_1.getAgentBundlesRoot)();
@@ -3578,21 +3928,24 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
3578
3928
  deps.writeRaw(`${(0, terminal_ui_1.renderOuroMasthead)({
3579
3929
  isTTY: true,
3580
3930
  columns: deps.stdoutColumns ?? process.stdout.columns,
3581
- subtitle: "Bringing the house online.",
3931
+ subtitle: "Preparing the house.",
3582
3932
  }).trimEnd()}\n\n`);
3583
3933
  }
3584
3934
  const progress = new up_progress_1.UpProgress({
3585
3935
  write: deps.writeRaw ?? deps.writeStdout,
3586
3936
  isTTY: outputIsTTY,
3937
+ columns: deps.stdoutColumns ?? process.stdout.columns,
3587
3938
  now: deps.now ?? (() => Date.now()),
3588
3939
  autoRender: true,
3589
3940
  });
3590
3941
  // ── versioned CLI update check ──
3591
3942
  if (deps.checkForCliUpdate) {
3592
3943
  progress.startPhase("update check");
3944
+ progress.updateDetail("checking npm registry\ncontinuing startup if it stays quiet");
3593
3945
  let pendingReExec = false;
3946
+ let updateCheckStatus = "up to date";
3594
3947
  try {
3595
- const updateResult = await deps.checkForCliUpdate();
3948
+ const { result: updateResult, timedOut } = await runCliUpdateCheckWithTimeout(deps.checkForCliUpdate, deps.updateCheckTimeoutMs ?? update_checker_1.CLI_UPDATE_CHECK_TIMEOUT_MS);
3596
3949
  if (updateResult.available && updateResult.latestVersion) {
3597
3950
  /* v8 ignore next -- fallback: getCurrentCliVersion always injected in tests @preserve */
3598
3951
  const currentVersion = linkedVersionBeforeUp ?? "unknown";
@@ -3606,9 +3959,13 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
3606
3959
  }
3607
3960
  pendingReExec = true;
3608
3961
  }
3962
+ else if (updateResult.error) {
3963
+ updateCheckStatus = summarizeCliUpdateCheckStatus(updateResult.error, timedOut);
3964
+ }
3609
3965
  /* v8 ignore start -- update check error: tested via daemon-cli-update-flow.test.ts @preserve */
3610
3966
  }
3611
3967
  catch (error) {
3968
+ updateCheckStatus = summarizeCliUpdateCheckStatus(error instanceof Error ? error.message : String(error));
3612
3969
  (0, runtime_1.emitNervesEvent)({
3613
3970
  level: "warn",
3614
3971
  component: "daemon",
@@ -3623,7 +3980,7 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
3623
3980
  deps.reExecFromNewVersion(args);
3624
3981
  }
3625
3982
  else {
3626
- progress.completePhase("update check", "up to date");
3983
+ progress.completePhase("update check", updateCheckStatus);
3627
3984
  }
3628
3985
  }
3629
3986
  progress.startPhase("system setup");
@@ -3777,56 +4134,72 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
3777
4134
  });
3778
4135
  }
3779
4136
  else {
3780
- const repairResult = await (0, agentic_repair_1.runAgenticRepair)(daemonResult.stability.degraded, {
3781
- /* v8 ignore start -- production provider discovery wiring @preserve */
3782
- discoverWorkingProvider: async (agentName) => {
3783
- const { discoverWorkingProvider: discover } = await Promise.resolve().then(() => __importStar(require("./provider-discovery")));
3784
- const { pingProvider } = await Promise.resolve().then(() => __importStar(require("../provider-ping")));
3785
- return discover({
3786
- agentName,
3787
- pingProvider: pingProvider,
3788
- });
3789
- },
3790
- createProviderRuntime: agentic_repair_1.createAgenticDiagnosisProviderRuntime,
3791
- readDaemonLogsTail: () => {
3792
- try {
3793
- const fs = require("node:fs");
3794
- const path = require("node:path");
3795
- const logsDir = path.join(process.env["HOME"] ?? "", ".agentstate", "daemon", "logs");
3796
- const files = fs.readdirSync(logsDir).filter((f) => f.endsWith(".log")).sort();
3797
- if (files.length === 0)
3798
- return "(no daemon logs found)";
3799
- const lastLog = fs.readFileSync(path.join(logsDir, files[files.length - 1]), "utf8");
3800
- const lines = lastLog.split("\n");
3801
- return lines.slice(-50).join("\n");
3802
- }
3803
- catch {
3804
- return "(unable to read daemon logs)";
3805
- }
3806
- },
3807
- /* v8 ignore stop */
3808
- runInteractiveRepair: interactive_repair_1.runInteractiveRepair,
3809
- promptInput: deps.promptInput ?? (async () => "n"),
3810
- writeStdout: deps.writeStdout,
3811
- runAuthFlow: async (agent, providerOverride) => {
3812
- await executeAuthRun({
3813
- kind: "auth.run",
3814
- agent,
3815
- ...(providerOverride ? { provider: providerOverride } : {}),
3816
- }, deps);
3817
- },
3818
- runVaultUnlock: async (agent) => {
3819
- await executeVaultUnlock({ kind: "vault.unlock", agent }, deps);
3820
- },
3821
- skipQueueSummary: true,
3822
- });
3823
- if (repairResult.repairsAttempted) {
3824
- const repairedAgents = daemonResult.stability.degraded
3825
- .filter(interactive_repair_1.hasRunnableInteractiveRepair)
3826
- .map((entry) => entry.agent);
4137
+ const typedDegraded = daemonResult.stability.degraded.filter((entry) => (0, readiness_repair_1.isKnownReadinessIssue)(entry.issue));
4138
+ const untypedDegraded = daemonResult.stability.degraded.filter((entry) => !(0, readiness_repair_1.isKnownReadinessIssue)(entry.issue));
4139
+ let repairsAttempted = false;
4140
+ const repairedAgents = new Set();
4141
+ if (typedDegraded.length > 0) {
4142
+ const guidedRepair = await runReadinessRepairForDegraded(typedDegraded, deps);
4143
+ if (guidedRepair.repairsAttempted) {
4144
+ repairsAttempted = true;
4145
+ typedDegraded.forEach((entry) => repairedAgents.add(entry.agent));
4146
+ }
4147
+ }
4148
+ if (untypedDegraded.length > 0) {
4149
+ const repairResult = await (0, agentic_repair_1.runAgenticRepair)(untypedDegraded, {
4150
+ /* v8 ignore start -- production provider discovery wiring @preserve */
4151
+ discoverWorkingProvider: async (agentName) => {
4152
+ const { discoverWorkingProvider: discover } = await Promise.resolve().then(() => __importStar(require("./provider-discovery")));
4153
+ const { pingProvider } = await Promise.resolve().then(() => __importStar(require("../provider-ping")));
4154
+ return discover({
4155
+ agentName,
4156
+ pingProvider: pingProvider,
4157
+ });
4158
+ },
4159
+ createProviderRuntime: agentic_repair_1.createAgenticDiagnosisProviderRuntime,
4160
+ readDaemonLogsTail: () => {
4161
+ try {
4162
+ const fs = require("node:fs");
4163
+ const path = require("node:path");
4164
+ const logsDir = path.join(process.env["HOME"] ?? "", ".agentstate", "daemon", "logs");
4165
+ const files = fs.readdirSync(logsDir).filter((f) => f.endsWith(".log")).sort();
4166
+ if (files.length === 0)
4167
+ return "(no daemon logs found)";
4168
+ const lastLog = fs.readFileSync(path.join(logsDir, files[files.length - 1]), "utf8");
4169
+ const lines = lastLog.split("\n");
4170
+ return lines.slice(-50).join("\n");
4171
+ }
4172
+ catch {
4173
+ return "(unable to read daemon logs)";
4174
+ }
4175
+ },
4176
+ /* v8 ignore stop */
4177
+ runInteractiveRepair: interactive_repair_1.runInteractiveRepair,
4178
+ promptInput: deps.promptInput ?? (async () => "n"),
4179
+ writeStdout: deps.writeStdout,
4180
+ runAuthFlow: async (agent, providerOverride) => {
4181
+ await executeAuthRun({
4182
+ kind: "auth.run",
4183
+ agent,
4184
+ ...(providerOverride ? { provider: providerOverride } : {}),
4185
+ }, deps);
4186
+ },
4187
+ runVaultUnlock: async (agent) => {
4188
+ await executeVaultUnlock({ kind: "vault.unlock", agent }, deps);
4189
+ },
4190
+ skipQueueSummary: true,
4191
+ });
4192
+ if (repairResult.repairsAttempted) {
4193
+ repairsAttempted = true;
4194
+ untypedDegraded
4195
+ .filter(interactive_repair_1.hasRunnableInteractiveRepair)
4196
+ .forEach((entry) => repairedAgents.add(entry.agent));
4197
+ }
4198
+ }
4199
+ if (repairsAttempted) {
3827
4200
  progress.startPhase("post-repair check");
3828
- await reportPostRepairProviderHealth(deps, repairedAgents, (msg) => progress.updateDetail(msg));
3829
- progress.completePhase("post-repair check", providerRepairCountSummary(repairedAgents.length));
4201
+ await reportPostRepairProviderHealth(deps, [...repairedAgents], (msg) => progress.updateDetail(msg));
4202
+ progress.completePhase("post-repair check", providerRepairCountSummary(repairedAgents.size));
3830
4203
  }
3831
4204
  }
3832
4205
  }
@@ -4028,16 +4401,16 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
4028
4401
  const sections = [localSection];
4029
4402
  if (deps.checkForCliUpdate) {
4030
4403
  try {
4031
- const updateResult = await deps.checkForCliUpdate();
4404
+ const { result: updateResult, timedOut } = await runCliUpdateCheckWithTimeout(deps.checkForCliUpdate, deps.updateCheckTimeoutMs ?? update_checker_1.CLI_UPDATE_CHECK_TIMEOUT_MS);
4032
4405
  if (updateResult.latestVersion) {
4033
4406
  sections.push(`published latest: ${updateResult.latestVersion} (${updateResult.available ? "update available" : "up to date"})`);
4034
4407
  }
4035
4408
  else if (updateResult.error) {
4036
- sections.push(`published latest: unavailable (${updateResult.error})`);
4409
+ sections.push(`published latest: unavailable (${summarizeCliUpdateCheckStatus(updateResult.error, timedOut)})`);
4037
4410
  }
4038
4411
  }
4039
4412
  catch (err) {
4040
- const reason = err instanceof Error ? err.message : String(err);
4413
+ const reason = summarizeCliUpdateCheckStatus(err instanceof Error ? err.message : String(err));
4041
4414
  sections.push(`published latest: unavailable (${reason})`);
4042
4415
  }
4043
4416
  }
@@ -5040,9 +5413,31 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
5040
5413
  await deps.startChat(hatchInput.agentName);
5041
5414
  return "";
5042
5415
  }
5043
- const message = `hatched ${hatchInput.agentName} at ${result.bundleRoot} using specialist identity ${result.selectedIdentity}; ${daemonResult.message}`;
5044
- deps.writeStdout(message);
5045
- return message;
5416
+ return writeCommandOutcome(deps, {
5417
+ title: "Hatch complete",
5418
+ subtitle: `${hatchInput.agentName} just arrived.`,
5419
+ summary: `${hatchInput.agentName} is ready for first contact.`,
5420
+ sections: [
5421
+ {
5422
+ title: "What changed",
5423
+ lines: [
5424
+ `Bundle: ${result.bundleRoot}`,
5425
+ `Specialist identity: ${result.selectedIdentity}`,
5426
+ `House status: ${daemonResult.message}`,
5427
+ ],
5428
+ },
5429
+ {
5430
+ title: "Next moves",
5431
+ lines: [
5432
+ `Talk to them with ouro chat ${hatchInput.agentName}.`,
5433
+ `Use ouro connect --agent ${hatchInput.agentName} to bring more capabilities online.`,
5434
+ ],
5435
+ },
5436
+ ],
5437
+ fallbackLines: [
5438
+ `hatched ${hatchInput.agentName} at ${result.bundleRoot} using specialist identity ${result.selectedIdentity}; ${daemonResult.message}`,
5439
+ ],
5440
+ });
5046
5441
  }
5047
5442
  // ── doctor (local, no daemon socket needed) ──
5048
5443
  if (command.kind === "doctor") {
@@ -5227,7 +5622,17 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
5227
5622
  }
5228
5623
  const fallbackMessage = response.summary ?? response.message ?? (response.ok ? "ok" : `error: ${response.error ?? "unknown error"}`);
5229
5624
  const message = command.kind === "daemon.status"
5230
- ? (0, cli_render_1.formatDaemonStatusOutput)(response, fallbackMessage)
5625
+ ? (() => {
5626
+ const payload = (0, cli_render_1.parseStatusPayload)(response.data);
5627
+ if (payload && ttyBoardEnabled(deps)) {
5628
+ return (0, human_command_screens_1.renderHouseStatusScreen)({
5629
+ payload,
5630
+ isTTY: true,
5631
+ columns: deps.stdoutColumns ?? process.stdout.columns,
5632
+ }).trimEnd();
5633
+ }
5634
+ return (0, cli_render_1.formatDaemonStatusOutput)(response, fallbackMessage);
5635
+ })()
5231
5636
  : fallbackMessage;
5232
5637
  deps.writeStdout(message);
5233
5638
  return message;