@ouro.bot/cli 0.1.0-alpha.417 → 0.1.0-alpha.419

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,6 +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-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.
102
103
  - 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.
103
104
  - The daemon discovers bundles dynamically from `~/AgentBundles`.
104
105
  - `ouro status` reports version, last-updated time, discovered agents, senses, and workers.
@@ -120,7 +121,7 @@ ouro auth --agent <name>
120
121
  ouro auth --agent <name> --provider <provider>
121
122
  ```
122
123
 
123
- `ouro auth` stores credentials in the owning agent's vault. It does not switch a lane or write provider/model selection.
124
+ `ouro auth` stores credentials in the owning agent's vault. It does not switch a lane or write provider/model selection. The command shows progress while browser login, vault storage, refresh, and verification are happening.
124
125
 
125
126
  When you want this machine to use a provider/model for a lane, use:
126
127
 
package/changelog.json CHANGED
@@ -1,6 +1,29 @@
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.419",
6
+ "changes": [
7
+ "`ouro auth`, `ouro provider refresh`, `ouro connect perplexity`, and `ouro connect bluebubbles` now use the shared human CLI progress checklist so browser login, vault reads/writes, daemon reloads, and verification steps stay visible instead of leaving a blinking cursor.",
8
+ "`ouro connect perplexity` now introduces the flow before the hidden API-key prompt, shows vault storage and reload progress, keeps the key out of output, and ends with a compact next step for using Perplexity search.",
9
+ "`ouro connect bluebubbles` now introduces the local-machine attachment before prompts, shows machine runtime config and `agent.json` enablement progress, and keeps the app password out of output.",
10
+ "`ouro provider refresh` now shows vault-read progress and reload status, and refresh/auth/connect failure paths preserve the last visible progress context while still rethrowing actionable errors.",
11
+ "The CLI progress renderer is documented as a shared human-facing contract: long-running human flows should show current work, stay concise, and never print secrets.",
12
+ "`@ouro.bot/cli` and the `ouro.bot` wrapper are version-synced for the provider onboarding progress release."
13
+ ]
14
+ },
15
+ {
16
+ "version": "0.1.0-alpha.418",
17
+ "changes": [
18
+ "`ouro up` now uses the live progress renderer in interactive terminals instead of forcing static non-TTY output, so long startup/check phases animate instead of leaving a blinking cursor.",
19
+ "Non-TTY and captured `ouro up` output now prints current phase starts and changed detail lines (`launching daemon process`, `waiting for daemon socket`, `daemon answered`) before completion, avoiding silent waits in logs and terminal sessions that cannot render spinners.",
20
+ "Daemon startup readiness is completed explicitly before provider checks begin; `ouro up` no longer marks `starting daemon` as done by accidentally auto-completing it when the next phase starts.",
21
+ "Daemon startup polling can now report progress into the parent `ouro up` checklist without rendering its own nested startup TUI, keeping startup output as one coherent surface.",
22
+ "Runtime drift restart messages now summarize drift categories such as `code path` or `managed agents` without printing raw worktree/package paths in normal CLI output.",
23
+ "Default CLI stdout now writes exactly one newline instead of using `console.log`, removing the extra blank lines caused when progress renderers already include newline-terminated output.",
24
+ "`@ouro.bot/cli` and the `ouro.bot` wrapper are version-synced for the `ouro up` progress polish release."
25
+ ]
26
+ },
4
27
  {
5
28
  "version": "0.1.0-alpha.417",
6
29
  "changes": [
@@ -89,9 +89,13 @@ function defaultStartDaemonProcess(socketPath) {
89
89
  return Promise.resolve({ pid: child.pid ?? null });
90
90
  }
91
91
  function defaultWriteStdout(text) {
92
- // eslint-disable-next-line no-console -- terminal UX: CLI command output
93
- console.log(text);
92
+ process.stdout.write(text.endsWith("\n") ? text : `${text}\n`);
94
93
  }
94
+ /* v8 ignore start -- thin terminal adapter around process stdout @preserve */
95
+ function defaultWriteRaw(text) {
96
+ process.stdout.write(text);
97
+ }
98
+ /* v8 ignore stop */
95
99
  /**
96
100
  * Read the runtimeVersion from the first .ouro bundle's bundle-meta.json.
97
101
  * Returns undefined if none found or unreadable.
@@ -486,6 +490,8 @@ function createDefaultOuroCliDeps(socketPath = socket_client_1.DEFAULT_DAEMON_SO
486
490
  sendCommand: socket_client_1.sendDaemonCommand,
487
491
  startDaemonProcess: defaultStartDaemonProcess,
488
492
  writeStdout: defaultWriteStdout,
493
+ writeRaw: defaultWriteRaw,
494
+ isTTY: process.stdout.isTTY === true,
489
495
  checkSocketAlive: socket_client_1.checkDaemonSocketAlive,
490
496
  cleanupStaleSocket: defaultCleanupStaleSocket,
491
497
  fallbackPendingMessage: defaultFallbackPendingMessage,
@@ -179,6 +179,25 @@ function providerRepairCountSummary(count) {
179
179
  return "ok";
180
180
  return `${count} ${count === 1 ? "needs" : "need"} attention`;
181
181
  }
182
+ function createHumanCommandProgress(deps, commandName) {
183
+ return new up_progress_1.CommandProgress({
184
+ write: deps.writeRaw ?? deps.writeStdout,
185
+ isTTY: deps.isTTY ?? false,
186
+ now: deps.now ?? (() => Date.now()),
187
+ autoRender: true,
188
+ eventScope: "command",
189
+ commandName,
190
+ });
191
+ }
192
+ function daemonProgressSummary(result) {
193
+ if (result.verifyStartupStatus === false)
194
+ return "not answering yet";
195
+ if (result.alreadyRunning)
196
+ return "already running";
197
+ if (result.message.includes("restarted"))
198
+ return "restarted and ready";
199
+ return "ready";
200
+ }
182
201
  async function reportPostRepairProviderHealth(deps, repairedAgents, onProgress) {
183
202
  const remainingDegraded = await checkAgentProviders(deps, repairedAgents, onProgress);
184
203
  (0, runtime_1.emitNervesEvent)({
@@ -314,17 +333,19 @@ async function ensureDaemonRunning(deps, options = {}) {
314
333
  sendCommand: deps.sendCommand,
315
334
  socketPath: deps.socketPath,
316
335
  daemonPid: runtimeResult.startedPid ?? null,
317
- /* v8 ignore next -- thin wrapper: raw process.stdout.write for ANSI cursor control @preserve */
318
- writeRaw: (text) => process.stdout.write(text),
319
- /* v8 ignore next -- thin wrapper: real stdout TTY detection injected for captured-output safety @preserve */
320
- isTTY: process.stdout.isTTY === true,
321
- /* v8 ignore next -- thin wrapper: real Date.now() injected for testability @preserve */
322
- now: () => Date.now(),
323
- /* v8 ignore next -- thin wrapper: real setTimeout injected for testability @preserve */
324
- sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
336
+ /* v8 ignore next -- thin wrapper: real stdout fallback injected by default deps @preserve */
337
+ writeRaw: deps.writeRaw ?? ((text) => process.stdout.write(text)),
338
+ /* v8 ignore next -- thin wrapper: real stdout TTY detection injected by default deps @preserve */
339
+ isTTY: deps.isTTY ?? process.stdout.isTTY === true,
340
+ /* v8 ignore next -- thin wrapper: real Date.now fallback injected by default deps @preserve */
341
+ now: deps.now ?? (() => Date.now()),
342
+ /* v8 ignore next -- thin wrapper: real setTimeout fallback injected by default deps @preserve */
343
+ sleep: deps.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms))),
325
344
  /* v8 ignore start -- daemon log tail + pid check: reads real filesystem, tested via deployment @preserve */
326
345
  readLatestDaemonEvent: readLatestDaemonStartupEvent,
327
346
  /* v8 ignore stop */
347
+ onProgress: deps.reportDaemonStartupPhase,
348
+ render: !deps.reportDaemonStartupPhase,
328
349
  });
329
350
  return {
330
351
  alreadyRunning: runtimeResult.alreadyRunning,
@@ -339,8 +360,8 @@ async function ensureDaemonRunning(deps, options = {}) {
339
360
  };
340
361
  let lastPid = null;
341
362
  for (let attempt = 0; attempt <= retryLimit; attempt += 1) {
342
- deps.reportDaemonStartupPhase?.("starting daemon...");
343
- deps.reportDaemonStartupPhase?.("waiting for daemon socket...");
363
+ deps.reportDaemonStartupPhase?.("launching daemon process");
364
+ deps.reportDaemonStartupPhase?.("waiting for daemon socket");
344
365
  deps.cleanupStaleSocket(deps.socketPath);
345
366
  const bootStartedAtMs = (deps.now ?? Date.now)();
346
367
  const started = await deps.startDaemonProcess(deps.socketPath);
@@ -354,17 +375,19 @@ async function ensureDaemonRunning(deps, options = {}) {
354
375
  sendCommand: deps.sendCommand,
355
376
  socketPath: deps.socketPath,
356
377
  daemonPid: lastPid,
357
- /* v8 ignore next -- thin wrapper: raw process.stdout.write for ANSI cursor control @preserve */
358
- writeRaw: (text) => process.stdout.write(text),
359
- /* v8 ignore next -- thin wrapper: real stdout TTY detection injected for captured-output safety @preserve */
360
- isTTY: process.stdout.isTTY === true,
361
- /* v8 ignore next -- thin wrapper: real Date.now() injected for testability @preserve */
362
- now: () => Date.now(),
363
- /* v8 ignore next -- thin wrapper: real setTimeout injected for testability @preserve */
364
- sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
378
+ /* v8 ignore next -- thin wrapper: real stdout fallback injected by default deps @preserve */
379
+ writeRaw: deps.writeRaw ?? ((text) => process.stdout.write(text)),
380
+ /* v8 ignore next -- thin wrapper: real stdout TTY detection injected by default deps @preserve */
381
+ isTTY: deps.isTTY ?? process.stdout.isTTY === true,
382
+ /* v8 ignore next -- thin wrapper: real Date.now fallback injected by default deps @preserve */
383
+ now: deps.now ?? (() => Date.now()),
384
+ /* v8 ignore next -- thin wrapper: real setTimeout fallback injected by default deps @preserve */
385
+ sleep: deps.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms))),
365
386
  /* v8 ignore start -- daemon log tail + pid check: reads real filesystem, tested via deployment @preserve */
366
387
  readLatestDaemonEvent: readLatestDaemonStartupEvent,
367
388
  /* v8 ignore stop */
389
+ onProgress: deps.reportDaemonStartupPhase,
390
+ render: !deps.reportDaemonStartupPhase,
368
391
  });
369
392
  return {
370
393
  alreadyRunning: false,
@@ -376,7 +399,7 @@ async function ensureDaemonRunning(deps, options = {}) {
376
399
  if (!startupFailure.retryable || attempt >= retryLimit) {
377
400
  break;
378
401
  }
379
- deps.reportDaemonStartupPhase?.("daemon startup lost stability; cleaning up and retrying once...");
402
+ deps.reportDaemonStartupPhase?.("daemon startup lost stability; retrying once");
380
403
  }
381
404
  return {
382
405
  alreadyRunning: false,
@@ -449,7 +472,7 @@ async function waitForDaemonStartup(deps, options) {
449
472
  if (!sawSocket) {
450
473
  sawSocket = true;
451
474
  stableSinceMs = now();
452
- deps.reportDaemonStartupPhase?.("verifying daemon health...");
475
+ deps.reportDaemonStartupPhase?.("verifying daemon health");
453
476
  }
454
477
  if (!hasFreshCurrentBootHealthSignal(deps, options.bootStartedAtMs, options.pid)) {
455
478
  continue;
@@ -1129,18 +1152,35 @@ function runtimeScopeLabel(scope) {
1129
1152
  }
1130
1153
  async function storeRuntimeConfigKey(input) {
1131
1154
  const machineId = input.scope === "machine" ? currentMachineId(input.deps) : undefined;
1155
+ input.onProgress?.("checking existing runtime config");
1132
1156
  const current = input.scope === "machine"
1133
1157
  ? await (0, runtime_credentials_1.refreshMachineRuntimeCredentialConfig)(input.agent, machineId, { preserveCachedOnFailure: true })
1134
1158
  : await (0, runtime_credentials_1.refreshRuntimeCredentialConfig)(input.agent, { preserveCachedOnFailure: true });
1135
1159
  if (!current.ok && current.reason !== "missing") {
1136
1160
  throw new Error(`cannot read existing runtime credentials from ${current.itemPath}: ${current.error}`);
1137
1161
  }
1162
+ input.onProgress?.(`storing ${input.key} in ${current.itemPath}`);
1138
1163
  const nextConfig = setRuntimeConfigValue(current.ok ? current.config : {}, input.key, input.value);
1139
1164
  const stored = input.scope === "machine"
1140
1165
  ? await (0, runtime_credentials_1.upsertMachineRuntimeCredentialConfig)(input.agent, machineId, nextConfig, providerCliNow(input.deps))
1141
1166
  : await (0, runtime_credentials_1.upsertRuntimeCredentialConfig)(input.agent, nextConfig, providerCliNow(input.deps));
1167
+ input.onProgress?.(`stored ${input.key}; credential value was not printed`);
1142
1168
  return { revision: stored.revision, itemPath: stored.itemPath, ...(machineId ? { machineId } : {}) };
1143
1169
  }
1170
+ async function reloadRunningAgentAfterCredentialChange(agent, deps) {
1171
+ try {
1172
+ const alive = await deps.checkSocketAlive(deps.socketPath);
1173
+ if (!alive)
1174
+ return "daemon is not running; next `ouro up` will load it";
1175
+ const response = await deps.sendCommand(deps.socketPath, { kind: "agent.restart", agent });
1176
+ if (response.ok)
1177
+ return `restarted ${agent} so the running agent reloads credentials`;
1178
+ return `daemon restart skipped: ${response.error ?? response.message ?? "unknown daemon error"}`;
1179
+ }
1180
+ catch (error) {
1181
+ return `daemon restart skipped: ${error instanceof Error ? error.message : String(error)}`;
1182
+ }
1183
+ }
1144
1184
  async function executeVaultConfigSet(command, deps) {
1145
1185
  if (command.agent === "SerpentGuide") {
1146
1186
  throw new Error("SerpentGuide does not have persistent runtime credentials. Store credentials in the hatchling agent vault.");
@@ -1255,18 +1295,16 @@ function enableAgentSense(agent, sense, deps) {
1255
1295
  function connectMenu(agent) {
1256
1296
  return [
1257
1297
  `Connect ${agent}`,
1298
+ "Pick what this agent should be able to use.",
1258
1299
  "",
1259
- "Portable agent capabilities",
1260
1300
  " 1. Perplexity search",
1261
- " stores: agent vault runtime/config -> integrations.perplexityApiKey",
1301
+ " Portable. Stores an API key in the agent vault.",
1262
1302
  "",
1263
- "This machine only",
1264
1303
  " 2. BlueBubbles iMessage",
1265
- " stores: agent vault runtime/machines/<this-machine>/config",
1304
+ " This machine only. Connects a local Mac Messages bridge.",
1266
1305
  "",
1267
- "Model providers",
1268
1306
  " 3. Provider auth",
1269
- ` runs separately: ouro auth --agent ${agent} --provider <provider>`,
1307
+ ` Model credentials: ouro auth --agent ${agent} --provider <provider>`,
1270
1308
  "",
1271
1309
  " 4. Cancel",
1272
1310
  "",
@@ -1278,23 +1316,45 @@ async function executeConnectPerplexity(agent, deps) {
1278
1316
  throw new Error("SerpentGuide has no persistent runtime credentials. Connect Perplexity on the hatchling agent instead.");
1279
1317
  }
1280
1318
  const promptSecret = requirePromptSecret(deps, "Perplexity API key entry");
1319
+ deps.writeStdout([
1320
+ `Connect Perplexity for ${agent}`,
1321
+ "The API key stays hidden while you type.",
1322
+ `Ouro stores it in ${agent}'s vault runtime/config item.`,
1323
+ ].join("\n"));
1281
1324
  const key = (await promptSecret("Perplexity API key: ")).trim();
1282
1325
  if (!key)
1283
1326
  throw new Error("Perplexity API key cannot be blank");
1284
- const stored = await storeRuntimeConfigKey({
1285
- agent,
1286
- key: "integrations.perplexityApiKey",
1287
- value: key,
1288
- scope: "agent",
1289
- deps,
1290
- });
1327
+ const progress = createHumanCommandProgress(deps, "connect perplexity");
1328
+ let stored;
1329
+ let reload;
1330
+ try {
1331
+ progress.startPhase("saving Perplexity search");
1332
+ stored = await storeRuntimeConfigKey({
1333
+ agent,
1334
+ key: "integrations.perplexityApiKey",
1335
+ value: key,
1336
+ scope: "agent",
1337
+ deps,
1338
+ onProgress: (message) => progress.updateDetail(message),
1339
+ });
1340
+ progress.completePhase("saving Perplexity search", "secret stored");
1341
+ progress.startPhase(`reloading ${agent}`);
1342
+ reload = await reloadRunningAgentAfterCredentialChange(agent, deps);
1343
+ progress.completePhase(`reloading ${agent}`, reload);
1344
+ progress.end();
1345
+ }
1346
+ catch (error) {
1347
+ progress.end();
1348
+ throw error;
1349
+ }
1291
1350
  const message = [
1292
1351
  `Perplexity connected for ${agent}`,
1293
1352
  "capability: Perplexity search",
1294
1353
  `stored: ${stored.itemPath}`,
1354
+ `reload: ${reload}`,
1295
1355
  "secret was not printed",
1296
1356
  "",
1297
- "Next: ask the agent to search, or run `ouro up` again if the daemon was already running.",
1357
+ "Next: ask the agent to search.",
1298
1358
  ].join("\n");
1299
1359
  deps.writeStdout(message);
1300
1360
  return message;
@@ -1305,6 +1365,11 @@ async function executeConnectBlueBubbles(agent, deps) {
1305
1365
  }
1306
1366
  const promptInput = requirePromptInput(deps, "BlueBubbles setup");
1307
1367
  const promptSecret = requirePromptSecret(deps, "BlueBubbles password entry");
1368
+ deps.writeStdout([
1369
+ `Connect BlueBubbles for ${agent}`,
1370
+ "This is a local attachment for this machine.",
1371
+ "The app password stays hidden while you type.",
1372
+ ].join("\n"));
1308
1373
  const serverUrl = (await promptInput("BlueBubbles server URL for this machine: ")).trim();
1309
1374
  if (!serverUrl)
1310
1375
  throw new Error("BlueBubbles server URL cannot be blank");
@@ -1315,25 +1380,40 @@ async function executeConnectBlueBubbles(agent, deps) {
1315
1380
  const webhookPath = normalizeWebhookPath(await promptInput("Local webhook path [/bluebubbles-webhook]: "), "/bluebubbles-webhook");
1316
1381
  const requestTimeoutMs = parseOptionalPositiveInteger(await promptInput("Request timeout ms [30000]: "), 30000, "BlueBubbles request timeout");
1317
1382
  const machineId = currentMachineId(deps);
1318
- const current = await (0, runtime_credentials_1.refreshMachineRuntimeCredentialConfig)(agent, machineId, { preserveCachedOnFailure: true });
1319
- if (!current.ok && current.reason !== "missing") {
1320
- throw new Error(`cannot read existing machine runtime credentials from ${current.itemPath}: ${current.error}`);
1383
+ const progress = createHumanCommandProgress(deps, "connect bluebubbles");
1384
+ let stored;
1385
+ try {
1386
+ progress.startPhase("saving BlueBubbles attachment");
1387
+ progress.updateDetail("checking existing machine runtime config");
1388
+ const current = await (0, runtime_credentials_1.refreshMachineRuntimeCredentialConfig)(agent, machineId, { preserveCachedOnFailure: true });
1389
+ if (!current.ok && current.reason !== "missing") {
1390
+ progress.end();
1391
+ throw new Error(`cannot read existing machine runtime credentials from ${current.itemPath}: ${current.error}`);
1392
+ }
1393
+ const nextConfig = {
1394
+ ...(current.ok ? current.config : {}),
1395
+ bluebubbles: {
1396
+ serverUrl,
1397
+ password,
1398
+ accountId: "default",
1399
+ },
1400
+ bluebubblesChannel: {
1401
+ port,
1402
+ webhookPath,
1403
+ requestTimeoutMs,
1404
+ },
1405
+ };
1406
+ progress.updateDetail("storing local machine config");
1407
+ stored = await (0, runtime_credentials_1.upsertMachineRuntimeCredentialConfig)(agent, machineId, nextConfig, providerCliNow(deps));
1408
+ progress.updateDetail("enabling BlueBubbles in agent.json");
1409
+ enableAgentSense(agent, "bluebubbles", deps);
1410
+ progress.completePhase("saving BlueBubbles attachment", "secret stored");
1411
+ progress.end();
1412
+ }
1413
+ catch (error) {
1414
+ progress.end();
1415
+ throw error;
1321
1416
  }
1322
- const nextConfig = {
1323
- ...(current.ok ? current.config : {}),
1324
- bluebubbles: {
1325
- serverUrl,
1326
- password,
1327
- accountId: "default",
1328
- },
1329
- bluebubblesChannel: {
1330
- port,
1331
- webhookPath,
1332
- requestTimeoutMs,
1333
- },
1334
- };
1335
- const stored = await (0, runtime_credentials_1.upsertMachineRuntimeCredentialConfig)(agent, machineId, nextConfig, providerCliNow(deps));
1336
- enableAgentSense(agent, "bluebubbles", deps);
1337
1417
  const message = appendBundleSyncSummary([
1338
1418
  `BlueBubbles attached for ${agent} on this machine`,
1339
1419
  `machine: ${machineId}`,
@@ -1635,38 +1715,40 @@ async function executeProviderRefresh(command, deps) {
1635
1715
  deps.writeStdout(message);
1636
1716
  return message;
1637
1717
  }
1638
- const pool = await (0, provider_credentials_1.refreshProviderCredentialPool)(command.agent);
1718
+ const progress = createHumanCommandProgress(deps, "provider refresh");
1719
+ progress.startPhase("refreshing provider credentials");
1720
+ let pool;
1721
+ try {
1722
+ pool = await (0, provider_credentials_1.refreshProviderCredentialPool)(command.agent, {
1723
+ onProgress: (message) => progress.updateDetail(message),
1724
+ });
1725
+ }
1726
+ catch (error) {
1727
+ progress.end();
1728
+ throw error;
1729
+ }
1639
1730
  const lines = [];
1640
1731
  if (pool.ok) {
1641
1732
  const summary = (0, provider_credentials_1.summarizeProviderCredentialPool)(pool.pool);
1642
1733
  lines.push(`refreshed provider credential snapshot for ${command.agent}`);
1643
1734
  lines.push(`providers: ${summary.providers.map((provider) => provider.provider).join(", ") || "none"}`);
1735
+ progress.completePhase("refreshing provider credentials", summary.providers.map((provider) => provider.provider).join(", ") || "none");
1644
1736
  }
1645
1737
  else {
1738
+ progress.end();
1646
1739
  lines.push(`provider credential refresh failed for ${command.agent}: ${pool.error}`);
1647
1740
  lines.push((0, vault_unlock_1.vaultUnlockReplaceRecoverFix)(command.agent, "Then retry 'ouro provider refresh'."));
1648
1741
  const message = lines.join("\n");
1649
1742
  deps.writeStdout(message);
1650
1743
  return message;
1651
1744
  }
1652
- try {
1653
- const alive = await deps.checkSocketAlive(deps.socketPath);
1654
- if (alive) {
1655
- const response = await deps.sendCommand(deps.socketPath, { kind: "agent.restart", agent: command.agent });
1656
- if (response.ok) {
1657
- lines.push(`restarted ${command.agent} so the running agent reloads credentials`);
1658
- }
1659
- else {
1660
- lines.push(`daemon restart skipped: ${response.error ?? response.message ?? "unknown daemon error"}`);
1661
- }
1662
- }
1663
- else {
1664
- lines.push("daemon is not running; the next start will load the refreshed snapshot");
1665
- }
1666
- }
1667
- catch (error) {
1668
- lines.push(`daemon restart skipped: ${error instanceof Error ? error.message : String(error)}`);
1669
- }
1745
+ progress.startPhase(`reloading ${command.agent}`);
1746
+ const reload = await reloadRunningAgentAfterCredentialChange(command.agent, deps);
1747
+ progress.completePhase(`reloading ${command.agent}`, reload);
1748
+ progress.end();
1749
+ lines.push(reload === "daemon is not running; next `ouro up` will load it"
1750
+ ? "daemon is not running; the next start will load the refreshed snapshot"
1751
+ : reload);
1670
1752
  const message = lines.join("\n");
1671
1753
  deps.writeStdout(message);
1672
1754
  return message;
@@ -2469,7 +2551,13 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
2469
2551
  }
2470
2552
  }
2471
2553
  const linkedVersionBeforeUp = deps.getCurrentCliVersion?.() ?? null;
2472
- const progress = new up_progress_1.UpProgress({ write: deps.writeStdout, isTTY: false });
2554
+ const outputIsTTY = deps.isTTY ?? process.stdout.isTTY === true;
2555
+ const progress = new up_progress_1.UpProgress({
2556
+ write: deps.writeRaw ?? deps.writeStdout,
2557
+ isTTY: outputIsTTY,
2558
+ now: deps.now ?? (() => Date.now()),
2559
+ autoRender: true,
2560
+ });
2473
2561
  // ── versioned CLI update check ──
2474
2562
  if (deps.checkForCliUpdate) {
2475
2563
  progress.startPhase("update check");
@@ -2631,6 +2719,12 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
2631
2719
  progress.announceStep?.(label);
2632
2720
  },
2633
2721
  }, { initialAlive: daemonAliveBeforeStart });
2722
+ progress.completePhase("starting daemon", daemonProgressSummary(daemonResult));
2723
+ if (daemonResult.verifyStartupStatus === false) {
2724
+ progress.end();
2725
+ deps.writeStdout(daemonResult.message);
2726
+ return daemonResult.message;
2727
+ }
2634
2728
  if (!providerChecksAlreadyRun || daemonResult.alreadyRunning) {
2635
2729
  progress.startPhase("provider checks");
2636
2730
  const providerDegraded = await checkAlreadyRunningAgentProviders(deps, (msg) => progress.updateDetail(msg));
@@ -2638,7 +2732,6 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
2638
2732
  progress.completePhase("provider checks", providerRepairCountSummary(providerDegraded.length));
2639
2733
  }
2640
2734
  progress.end();
2641
- deps.writeStdout(daemonResult.message);
2642
2735
  // Interactive repair for degraded agents (Unit 5) — skipped by --no-repair (Unit 6)
2643
2736
  if (daemonResult.stability?.degraded && daemonResult.stability.degraded.length > 0) {
2644
2737
  if (command.noRepair) {
@@ -3324,31 +3417,43 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
3324
3417
  const provider = command.provider ?? (0, auth_flow_1.readAgentConfigForAgent)(command.agent, deps.bundlesRoot).config.humanFacing.provider;
3325
3418
  /* v8 ignore next -- tests always inject runAuthFlow; default is for production @preserve */
3326
3419
  const authRunner = deps.runAuthFlow ?? (await Promise.resolve().then(() => __importStar(require("../auth/auth-flow")))).runRuntimeAuthFlow;
3327
- const result = await authRunner({
3328
- agentName: command.agent,
3329
- provider,
3330
- promptInput: deps.promptInput,
3331
- onProgress: deps.writeStdout,
3332
- });
3420
+ const progress = createHumanCommandProgress(deps, "auth");
3421
+ progress.startPhase(`authenticating ${provider}`);
3422
+ let result;
3423
+ try {
3424
+ result = await authRunner({
3425
+ agentName: command.agent,
3426
+ provider,
3427
+ promptInput: deps.promptInput,
3428
+ onProgress: (message) => progress.updateDetail(message),
3429
+ });
3430
+ }
3431
+ catch (error) {
3432
+ progress.end();
3433
+ throw error;
3434
+ }
3435
+ progress.completePhase(`authenticating ${provider}`, "credentials stored");
3333
3436
  // Behavior: ouro auth stores credentials only — does NOT switch provider.
3334
3437
  // Use `ouro auth switch` to change the active provider.
3335
- deps.writeStdout(result.message);
3336
3438
  // Verify the credentials actually work by pinging the provider
3337
3439
  /* v8 ignore start -- integration: real API ping after auth @preserve */
3338
3440
  try {
3339
- deps.writeStdout(`verifying ${provider} credentials...`);
3441
+ progress.startPhase(`verifying ${provider}`);
3340
3442
  const credential = await readProviderCredentialRecord(command.agent, provider, deps);
3341
3443
  const status = credential.ok
3342
3444
  ? await verifyProviderCredentials(provider, {
3343
3445
  [provider]: { ...credential.record.config, ...credential.record.credentials },
3344
3446
  })
3345
3447
  : `stored but could not be re-read from vault (${credential.error})`;
3346
- deps.writeStdout(`${provider}: ${status}`);
3448
+ progress.completePhase(`verifying ${provider}`, status);
3347
3449
  }
3348
- catch {
3349
- // Verification failure is non-blocking — credentials were saved regardless
3450
+ catch (error) {
3451
+ // Verification failure is non-blocking — credentials were saved regardless.
3452
+ progress.completePhase(`verifying ${provider}`, `skipped (${error instanceof Error ? error.message : String(error)})`);
3350
3453
  }
3351
3454
  /* v8 ignore stop */
3455
+ progress.end();
3456
+ deps.writeStdout(result.message);
3352
3457
  return result.message;
3353
3458
  }
3354
3459
  // ── auth verify (local, no daemon socket needed) ──
@@ -83,14 +83,8 @@ function collectRuntimeDriftReasons(local, running) {
83
83
  }
84
84
  return reasons;
85
85
  }
86
- function formatRuntimeValue(reason) {
87
- if (reason.key === "configFingerprint") {
88
- return `${reason.running.slice(0, 12)} -> ${reason.local.slice(0, 12)}`;
89
- }
90
- return `${reason.running} -> ${reason.local}`;
91
- }
92
- function formatRuntimeDriftSummary(reasons) {
93
- return reasons.map((reason) => `${reason.label} ${formatRuntimeValue(reason)}`).join("; ");
86
+ function formatRuntimeDriftPublicSummary(reasons) {
87
+ return reasons.map((reason) => reason.label).join(", ");
94
88
  }
95
89
  async function ensureCurrentDaemonRuntime(deps) {
96
90
  const localRuntime = normalizeRuntimeIdentity({
@@ -107,7 +101,7 @@ async function ensureCurrentDaemonRuntime(deps) {
107
101
  let result;
108
102
  if (driftReasons.length > 0) {
109
103
  const includesVersionDrift = driftReasons.some((entry) => entry.key === "version");
110
- const driftSummary = formatRuntimeDriftSummary(driftReasons);
104
+ const publicDriftSummary = formatRuntimeDriftPublicSummary(driftReasons);
111
105
  try {
112
106
  await deps.stopDaemon();
113
107
  }
@@ -117,7 +111,7 @@ async function ensureCurrentDaemonRuntime(deps) {
117
111
  alreadyRunning: true,
118
112
  message: includesVersionDrift
119
113
  ? `daemon already running (${deps.socketPath}; could not replace stale daemon ${runningVersion} -> ${deps.localVersion}: ${reason})`
120
- : `daemon already running (${deps.socketPath}; could not replace drifted daemon ${driftSummary}: ${reason})`,
114
+ : `daemon already running (${deps.socketPath}; could not replace runtime drift ${publicDriftSummary}: ${reason})`,
121
115
  };
122
116
  (0, runtime_1.emitNervesEvent)({
123
117
  level: "warn",
@@ -148,12 +142,12 @@ async function ensureCurrentDaemonRuntime(deps) {
148
142
  const pid = started.pid ?? "unknown";
149
143
  const verified = await verifyDaemonStarted(deps);
150
144
  /* v8 ignore next -- daemon liveness failure: requires real daemon crash timing @preserve */
151
- const suffix = verified ? "" : "\ndaemon did not answer yet, so Ouro is checking repair paths next.";
145
+ const suffix = verified ? "" : "\ndaemon restart has not answered yet; check logs with `ouro logs` or run `ouro doctor`.";
152
146
  result = {
153
147
  alreadyRunning: false,
154
148
  message: includesVersionDrift
155
149
  ? `restarted stale daemon ${runningVersion} -> ${deps.localVersion} (pid ${pid})${suffix}`
156
- : `restarted drifted daemon (${driftSummary}) (pid ${pid})${suffix}`,
150
+ : `restarted daemon after runtime drift: ${publicDriftSummary} (pid ${pid})${suffix}`,
157
151
  verifyStartupStatus: verified,
158
152
  startedPid: started.pid ?? null,
159
153
  };
@@ -134,6 +134,14 @@ async function pollDaemonStartup(deps) {
134
134
  let prevLineCount = 0;
135
135
  const isTTY = deps.isTTY ?? true;
136
136
  const isAlive = deps.isProcessAlive ?? defaultIsProcessAlive;
137
+ const shouldRender = deps.render ?? true;
138
+ let lastProgress = null;
139
+ const reportProgress = (message) => {
140
+ if (!deps.onProgress || message === lastProgress)
141
+ return;
142
+ lastProgress = message;
143
+ deps.onProgress(message);
144
+ };
137
145
  (0, runtime_1.emitNervesEvent)({
138
146
  component: "daemon",
139
147
  event: "daemon.startup_poll_start",
@@ -174,22 +182,30 @@ async function pollDaemonStartup(deps) {
174
182
  }
175
183
  // Show what the daemon is doing from its log
176
184
  const latestEvent = deps.readLatestDaemonEvent?.() ?? null;
177
- const output = renderWaitingForDaemon(elapsed, latestEvent, prevLineCount, { isTTY });
178
- deps.writeRaw(output);
179
- prevLineCount = latestEvent ? 2 : 1;
185
+ reportProgress(latestEvent ?? "waiting for daemon");
186
+ if (shouldRender) {
187
+ const output = renderWaitingForDaemon(elapsed, latestEvent, prevLineCount, { isTTY });
188
+ deps.writeRaw(output);
189
+ prevLineCount = latestEvent ? 2 : 1;
190
+ }
180
191
  }
181
192
  if (payload) {
182
- const output = renderStartupProgress(payload, elapsed, prevLineCount, { isTTY });
183
- deps.writeRaw(output);
184
- prevLineCount = payload.workers.length + 1;
193
+ reportProgress(formatStartupProgressDetail(payload));
194
+ if (shouldRender) {
195
+ const output = renderStartupProgress(payload, elapsed, prevLineCount, { isTTY });
196
+ deps.writeRaw(output);
197
+ prevLineCount = payload.workers.length + 1;
198
+ }
185
199
  const assessment = assessStability(payload, now);
186
200
  if (assessment.resolved) {
187
201
  const result = {
188
202
  stable: assessment.stable,
189
203
  degraded: assessment.degraded,
190
204
  };
191
- const summary = renderFinalSummary(result, isTTY);
192
- deps.writeRaw(summary);
205
+ if (shouldRender) {
206
+ const summary = renderFinalSummary(result, isTTY);
207
+ deps.writeRaw(summary);
208
+ }
193
209
  (0, runtime_1.emitNervesEvent)({
194
210
  component: "daemon",
195
211
  event: "daemon.startup_poll_end",
@@ -206,6 +222,12 @@ async function pollDaemonStartup(deps) {
206
222
  await deps.sleep(POLL_INTERVAL_MS);
207
223
  }
208
224
  }
225
+ function formatStartupProgressDetail(payload) {
226
+ if (payload.workers.length === 0)
227
+ return "daemon answered";
228
+ const workers = payload.workers.map((worker) => `${worker.agent}/${worker.worker} ${worker.status}`).join(", ");
229
+ return `waiting for agents: ${workers}`;
230
+ }
209
231
  function colorStatus(status) {
210
232
  const statusColor = status === "running" ? GREEN
211
233
  : status === "crashed" ? RED
@@ -1,17 +1,18 @@
1
1
  "use strict";
2
2
  /**
3
- * UpProgress — accumulated-checklist progress renderer for `ouro up`.
3
+ * UpProgress — accumulated-checklist progress renderer.
4
4
  *
5
5
  * Displays completed phases with checkmarks, the current phase with a
6
6
  * spinner and elapsed time, and pending phases as plain text. Uses ANSI
7
7
  * cursor control for in-place overwriting in TTY mode, and falls back to
8
8
  * static line-per-phase output in non-TTY mode.
9
9
  *
10
- * The caller drives animation by calling `render(now)` on a setInterval.
11
- * This module owns no timers.
10
+ * The caller can drive animation by calling `render(now)`. In production CLI
11
+ * use, `autoRender` starts a short-lived timer while a TTY phase is active so
12
+ * long operations never leave a dead-looking cursor.
12
13
  */
13
14
  Object.defineProperty(exports, "__esModule", { value: true });
14
- exports.UpProgress = void 0;
15
+ exports.CommandProgress = exports.UpProgress = void 0;
15
16
  const runtime_1 = require("../../nerves/runtime");
16
17
  // ── ANSI constants (shared with startup-tui.ts pattern) ──
17
18
  const SPINNER_FRAMES = "\u280B\u2819\u2839\u2838\u283C\u2834\u2826\u2827\u2807\u280F";
@@ -23,15 +24,34 @@ const GREEN = "\x1b[38;2;46;204;64m";
23
24
  class UpProgress {
24
25
  write;
25
26
  isTTY;
27
+ now;
28
+ autoRender;
29
+ renderIntervalMs;
30
+ setTimer;
31
+ clearTimer;
32
+ eventScope;
33
+ commandName;
26
34
  completed = [];
27
35
  currentPhase = null;
36
+ currentDetail = null;
28
37
  prevLineCount = 0;
29
38
  ended = false;
39
+ renderTimer = null;
30
40
  constructor(options) {
31
41
  /* v8 ignore next -- thin wrapper: raw process.stdout.write for ANSI cursor control @preserve */
32
42
  this.write = options?.write ?? ((text) => process.stdout.write(text));
33
43
  /* v8 ignore next -- thin wrapper: real isTTY check injected for testability @preserve */
34
44
  this.isTTY = options?.isTTY ?? (process.stdout.isTTY === true);
45
+ /* v8 ignore next -- thin wrapper: real Date.now injected for testability @preserve */
46
+ this.now = options?.now ?? (() => Date.now());
47
+ this.autoRender = options?.autoRender ?? false;
48
+ this.renderIntervalMs = options?.renderIntervalMs ?? 80;
49
+ /* v8 ignore start -- real timers are injected in tests when needed @preserve */
50
+ this.setTimer = options?.setInterval ?? ((callback, ms) => setInterval(callback, ms));
51
+ this.clearTimer = options?.clearInterval ?? ((handle) => clearInterval(handle));
52
+ /* v8 ignore stop */
53
+ this.eventScope = options?.eventScope ?? "up";
54
+ this.commandName = options?.commandName ?? null;
35
55
  }
36
56
  /**
37
57
  * Begin a new phase with spinner. If a phase is already active, it is
@@ -41,26 +61,44 @@ class UpProgress {
41
61
  if (this.currentPhase) {
42
62
  this.completePhase(this.currentPhase.label);
43
63
  }
44
- this.currentPhase = { label, startedAt: Date.now() };
64
+ this.currentPhase = { label, startedAt: this.now() };
65
+ this.currentDetail = null;
66
+ if (this.isTTY) {
67
+ this.ensureAutoRender();
68
+ this.flushRender();
69
+ }
70
+ else {
71
+ this.write(` ... ${label}\n`);
72
+ }
45
73
  }
46
74
  /**
47
75
  * Emit a one-line status breadcrumb in non-TTY mode without affecting the
48
76
  * accumulated checklist state. Used for daemon startup sub-steps.
49
77
  */
50
78
  announceStep(label) {
79
+ if (this.currentPhase) {
80
+ this.updateDetail(label);
81
+ return;
82
+ }
51
83
  if (this.isTTY)
52
84
  return;
53
- this.write(label);
85
+ this.write(` ${label}\n`);
54
86
  }
55
87
  /**
56
88
  * Update the sub-step detail on the current spinner phase. Rendered as
57
- * "label (Xs) -- detail" in TTY mode. No-op in non-TTY mode or when
58
- * no phase is active.
89
+ * "label (Xs) -- detail" in TTY mode. In non-TTY mode, writes changed
90
+ * detail lines so long operations remain visible in logs and captured output.
59
91
  */
60
92
  updateDetail(detail) {
61
- if (!this.isTTY || !this.currentPhase)
93
+ if (!this.currentPhase || detail === this.currentDetail)
62
94
  return;
95
+ this.currentDetail = detail;
63
96
  this.currentPhase.detail = detail;
97
+ if (this.isTTY) {
98
+ this.flushRender();
99
+ return;
100
+ }
101
+ this.write(` ${detail}\n`);
64
102
  }
65
103
  /**
66
104
  * Mark the current phase as done. In non-TTY mode, immediately writes
@@ -70,16 +108,31 @@ class UpProgress {
70
108
  if (!this.currentPhase) {
71
109
  return;
72
110
  }
73
- const elapsedMs = Date.now() - this.currentPhase.startedAt;
111
+ const elapsedMs = this.now() - this.currentPhase.startedAt;
74
112
  this.completed.push({ label, detail });
75
113
  this.currentPhase = null;
76
- (0, runtime_1.emitNervesEvent)({
77
- component: "daemon",
78
- event: "daemon.up_phase_complete",
79
- message: `phase complete: ${label}`,
80
- meta: { phase: label, detail: detail ?? null, elapsedMs },
81
- });
82
- if (!this.isTTY) {
114
+ this.currentDetail = null;
115
+ this.stopAutoRender();
116
+ if (this.eventScope === "command") {
117
+ (0, runtime_1.emitNervesEvent)({
118
+ component: "daemon",
119
+ event: "daemon.cli_progress_phase_complete",
120
+ message: `phase complete: ${label}`,
121
+ meta: { command: this.commandName, phase: label, detail: detail ?? null, elapsedMs },
122
+ });
123
+ }
124
+ else {
125
+ (0, runtime_1.emitNervesEvent)({
126
+ component: "daemon",
127
+ event: "daemon.up_phase_complete",
128
+ message: `phase complete: ${label}`,
129
+ meta: { phase: label, detail: detail ?? null, elapsedMs },
130
+ });
131
+ }
132
+ if (this.isTTY) {
133
+ this.flushRender();
134
+ }
135
+ else {
83
136
  const detailStr = detail ? ` \u2014 ${detail}` : "";
84
137
  this.write(` \u2713 ${label}${detailStr}\n`);
85
138
  }
@@ -134,13 +187,32 @@ class UpProgress {
134
187
  this.ended = true;
135
188
  if (this.currentPhase) {
136
189
  this.currentPhase = null;
190
+ this.currentDetail = null;
137
191
  }
192
+ this.stopAutoRender();
138
193
  if (this.isTTY) {
139
- const output = this.render(Date.now());
140
- if (output) {
141
- this.write(output);
142
- }
194
+ this.flushRender();
195
+ }
196
+ }
197
+ ensureAutoRender() {
198
+ if (!this.autoRender || !this.isTTY || this.renderTimer !== null) {
199
+ return;
200
+ }
201
+ this.renderTimer = this.setTimer(() => this.flushRender(), this.renderIntervalMs);
202
+ }
203
+ stopAutoRender() {
204
+ if (this.renderTimer === null) {
205
+ return;
206
+ }
207
+ this.clearTimer(this.renderTimer);
208
+ this.renderTimer = null;
209
+ }
210
+ flushRender() {
211
+ const output = this.render(this.now());
212
+ if (output) {
213
+ this.write(output);
143
214
  }
144
215
  }
145
216
  }
146
217
  exports.UpProgress = UpProgress;
218
+ exports.CommandProgress = UpProgress;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.417",
3
+ "version": "0.1.0-alpha.419",
4
4
  "main": "dist/heart/daemon/ouro-entry.js",
5
5
  "bin": {
6
6
  "cli": "dist/heart/daemon/ouro-bot-entry.js",