@ouro.bot/cli 0.1.0-alpha.418 → 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,17 @@
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
+ },
4
15
  {
5
16
  "version": "0.1.0-alpha.418",
6
17
  "changes": [
@@ -179,6 +179,16 @@ 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
+ }
182
192
  function daemonProgressSummary(result) {
183
193
  if (result.verifyStartupStatus === false)
184
194
  return "not answering yet";
@@ -1142,18 +1152,35 @@ function runtimeScopeLabel(scope) {
1142
1152
  }
1143
1153
  async function storeRuntimeConfigKey(input) {
1144
1154
  const machineId = input.scope === "machine" ? currentMachineId(input.deps) : undefined;
1155
+ input.onProgress?.("checking existing runtime config");
1145
1156
  const current = input.scope === "machine"
1146
1157
  ? await (0, runtime_credentials_1.refreshMachineRuntimeCredentialConfig)(input.agent, machineId, { preserveCachedOnFailure: true })
1147
1158
  : await (0, runtime_credentials_1.refreshRuntimeCredentialConfig)(input.agent, { preserveCachedOnFailure: true });
1148
1159
  if (!current.ok && current.reason !== "missing") {
1149
1160
  throw new Error(`cannot read existing runtime credentials from ${current.itemPath}: ${current.error}`);
1150
1161
  }
1162
+ input.onProgress?.(`storing ${input.key} in ${current.itemPath}`);
1151
1163
  const nextConfig = setRuntimeConfigValue(current.ok ? current.config : {}, input.key, input.value);
1152
1164
  const stored = input.scope === "machine"
1153
1165
  ? await (0, runtime_credentials_1.upsertMachineRuntimeCredentialConfig)(input.agent, machineId, nextConfig, providerCliNow(input.deps))
1154
1166
  : await (0, runtime_credentials_1.upsertRuntimeCredentialConfig)(input.agent, nextConfig, providerCliNow(input.deps));
1167
+ input.onProgress?.(`stored ${input.key}; credential value was not printed`);
1155
1168
  return { revision: stored.revision, itemPath: stored.itemPath, ...(machineId ? { machineId } : {}) };
1156
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
+ }
1157
1184
  async function executeVaultConfigSet(command, deps) {
1158
1185
  if (command.agent === "SerpentGuide") {
1159
1186
  throw new Error("SerpentGuide does not have persistent runtime credentials. Store credentials in the hatchling agent vault.");
@@ -1268,18 +1295,16 @@ function enableAgentSense(agent, sense, deps) {
1268
1295
  function connectMenu(agent) {
1269
1296
  return [
1270
1297
  `Connect ${agent}`,
1298
+ "Pick what this agent should be able to use.",
1271
1299
  "",
1272
- "Portable agent capabilities",
1273
1300
  " 1. Perplexity search",
1274
- " stores: agent vault runtime/config -> integrations.perplexityApiKey",
1301
+ " Portable. Stores an API key in the agent vault.",
1275
1302
  "",
1276
- "This machine only",
1277
1303
  " 2. BlueBubbles iMessage",
1278
- " stores: agent vault runtime/machines/<this-machine>/config",
1304
+ " This machine only. Connects a local Mac Messages bridge.",
1279
1305
  "",
1280
- "Model providers",
1281
1306
  " 3. Provider auth",
1282
- ` runs separately: ouro auth --agent ${agent} --provider <provider>`,
1307
+ ` Model credentials: ouro auth --agent ${agent} --provider <provider>`,
1283
1308
  "",
1284
1309
  " 4. Cancel",
1285
1310
  "",
@@ -1291,23 +1316,45 @@ async function executeConnectPerplexity(agent, deps) {
1291
1316
  throw new Error("SerpentGuide has no persistent runtime credentials. Connect Perplexity on the hatchling agent instead.");
1292
1317
  }
1293
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"));
1294
1324
  const key = (await promptSecret("Perplexity API key: ")).trim();
1295
1325
  if (!key)
1296
1326
  throw new Error("Perplexity API key cannot be blank");
1297
- const stored = await storeRuntimeConfigKey({
1298
- agent,
1299
- key: "integrations.perplexityApiKey",
1300
- value: key,
1301
- scope: "agent",
1302
- deps,
1303
- });
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
+ }
1304
1350
  const message = [
1305
1351
  `Perplexity connected for ${agent}`,
1306
1352
  "capability: Perplexity search",
1307
1353
  `stored: ${stored.itemPath}`,
1354
+ `reload: ${reload}`,
1308
1355
  "secret was not printed",
1309
1356
  "",
1310
- "Next: ask the agent to search, or run `ouro up` again if the daemon was already running.",
1357
+ "Next: ask the agent to search.",
1311
1358
  ].join("\n");
1312
1359
  deps.writeStdout(message);
1313
1360
  return message;
@@ -1318,6 +1365,11 @@ async function executeConnectBlueBubbles(agent, deps) {
1318
1365
  }
1319
1366
  const promptInput = requirePromptInput(deps, "BlueBubbles setup");
1320
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"));
1321
1373
  const serverUrl = (await promptInput("BlueBubbles server URL for this machine: ")).trim();
1322
1374
  if (!serverUrl)
1323
1375
  throw new Error("BlueBubbles server URL cannot be blank");
@@ -1328,25 +1380,40 @@ async function executeConnectBlueBubbles(agent, deps) {
1328
1380
  const webhookPath = normalizeWebhookPath(await promptInput("Local webhook path [/bluebubbles-webhook]: "), "/bluebubbles-webhook");
1329
1381
  const requestTimeoutMs = parseOptionalPositiveInteger(await promptInput("Request timeout ms [30000]: "), 30000, "BlueBubbles request timeout");
1330
1382
  const machineId = currentMachineId(deps);
1331
- const current = await (0, runtime_credentials_1.refreshMachineRuntimeCredentialConfig)(agent, machineId, { preserveCachedOnFailure: true });
1332
- if (!current.ok && current.reason !== "missing") {
1333
- 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;
1334
1416
  }
1335
- const nextConfig = {
1336
- ...(current.ok ? current.config : {}),
1337
- bluebubbles: {
1338
- serverUrl,
1339
- password,
1340
- accountId: "default",
1341
- },
1342
- bluebubblesChannel: {
1343
- port,
1344
- webhookPath,
1345
- requestTimeoutMs,
1346
- },
1347
- };
1348
- const stored = await (0, runtime_credentials_1.upsertMachineRuntimeCredentialConfig)(agent, machineId, nextConfig, providerCliNow(deps));
1349
- enableAgentSense(agent, "bluebubbles", deps);
1350
1417
  const message = appendBundleSyncSummary([
1351
1418
  `BlueBubbles attached for ${agent} on this machine`,
1352
1419
  `machine: ${machineId}`,
@@ -1648,38 +1715,40 @@ async function executeProviderRefresh(command, deps) {
1648
1715
  deps.writeStdout(message);
1649
1716
  return message;
1650
1717
  }
1651
- 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
+ }
1652
1730
  const lines = [];
1653
1731
  if (pool.ok) {
1654
1732
  const summary = (0, provider_credentials_1.summarizeProviderCredentialPool)(pool.pool);
1655
1733
  lines.push(`refreshed provider credential snapshot for ${command.agent}`);
1656
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");
1657
1736
  }
1658
1737
  else {
1738
+ progress.end();
1659
1739
  lines.push(`provider credential refresh failed for ${command.agent}: ${pool.error}`);
1660
1740
  lines.push((0, vault_unlock_1.vaultUnlockReplaceRecoverFix)(command.agent, "Then retry 'ouro provider refresh'."));
1661
1741
  const message = lines.join("\n");
1662
1742
  deps.writeStdout(message);
1663
1743
  return message;
1664
1744
  }
1665
- try {
1666
- const alive = await deps.checkSocketAlive(deps.socketPath);
1667
- if (alive) {
1668
- const response = await deps.sendCommand(deps.socketPath, { kind: "agent.restart", agent: command.agent });
1669
- if (response.ok) {
1670
- lines.push(`restarted ${command.agent} so the running agent reloads credentials`);
1671
- }
1672
- else {
1673
- lines.push(`daemon restart skipped: ${response.error ?? response.message ?? "unknown daemon error"}`);
1674
- }
1675
- }
1676
- else {
1677
- lines.push("daemon is not running; the next start will load the refreshed snapshot");
1678
- }
1679
- }
1680
- catch (error) {
1681
- lines.push(`daemon restart skipped: ${error instanceof Error ? error.message : String(error)}`);
1682
- }
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);
1683
1752
  const message = lines.join("\n");
1684
1753
  deps.writeStdout(message);
1685
1754
  return message;
@@ -3348,31 +3417,43 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
3348
3417
  const provider = command.provider ?? (0, auth_flow_1.readAgentConfigForAgent)(command.agent, deps.bundlesRoot).config.humanFacing.provider;
3349
3418
  /* v8 ignore next -- tests always inject runAuthFlow; default is for production @preserve */
3350
3419
  const authRunner = deps.runAuthFlow ?? (await Promise.resolve().then(() => __importStar(require("../auth/auth-flow")))).runRuntimeAuthFlow;
3351
- const result = await authRunner({
3352
- agentName: command.agent,
3353
- provider,
3354
- promptInput: deps.promptInput,
3355
- onProgress: deps.writeStdout,
3356
- });
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");
3357
3436
  // Behavior: ouro auth stores credentials only — does NOT switch provider.
3358
3437
  // Use `ouro auth switch` to change the active provider.
3359
- deps.writeStdout(result.message);
3360
3438
  // Verify the credentials actually work by pinging the provider
3361
3439
  /* v8 ignore start -- integration: real API ping after auth @preserve */
3362
3440
  try {
3363
- deps.writeStdout(`verifying ${provider} credentials...`);
3441
+ progress.startPhase(`verifying ${provider}`);
3364
3442
  const credential = await readProviderCredentialRecord(command.agent, provider, deps);
3365
3443
  const status = credential.ok
3366
3444
  ? await verifyProviderCredentials(provider, {
3367
3445
  [provider]: { ...credential.record.config, ...credential.record.credentials },
3368
3446
  })
3369
3447
  : `stored but could not be re-read from vault (${credential.error})`;
3370
- deps.writeStdout(`${provider}: ${status}`);
3448
+ progress.completePhase(`verifying ${provider}`, status);
3371
3449
  }
3372
- catch {
3373
- // 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)})`);
3374
3453
  }
3375
3454
  /* v8 ignore stop */
3455
+ progress.end();
3456
+ deps.writeStdout(result.message);
3376
3457
  return result.message;
3377
3458
  }
3378
3459
  // ── auth verify (local, no daemon socket needed) ──
@@ -1,6 +1,6 @@
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
@@ -12,7 +12,7 @@
12
12
  * long operations never leave a dead-looking cursor.
13
13
  */
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
- exports.UpProgress = void 0;
15
+ exports.CommandProgress = exports.UpProgress = void 0;
16
16
  const runtime_1 = require("../../nerves/runtime");
17
17
  // ── ANSI constants (shared with startup-tui.ts pattern) ──
18
18
  const SPINNER_FRAMES = "\u280B\u2819\u2839\u2838\u283C\u2834\u2826\u2827\u2807\u280F";
@@ -29,6 +29,8 @@ class UpProgress {
29
29
  renderIntervalMs;
30
30
  setTimer;
31
31
  clearTimer;
32
+ eventScope;
33
+ commandName;
32
34
  completed = [];
33
35
  currentPhase = null;
34
36
  currentDetail = null;
@@ -48,6 +50,8 @@ class UpProgress {
48
50
  this.setTimer = options?.setInterval ?? ((callback, ms) => setInterval(callback, ms));
49
51
  this.clearTimer = options?.clearInterval ?? ((handle) => clearInterval(handle));
50
52
  /* v8 ignore stop */
53
+ this.eventScope = options?.eventScope ?? "up";
54
+ this.commandName = options?.commandName ?? null;
51
55
  }
52
56
  /**
53
57
  * Begin a new phase with spinner. If a phase is already active, it is
@@ -109,12 +113,22 @@ class UpProgress {
109
113
  this.currentPhase = null;
110
114
  this.currentDetail = null;
111
115
  this.stopAutoRender();
112
- (0, runtime_1.emitNervesEvent)({
113
- component: "daemon",
114
- event: "daemon.up_phase_complete",
115
- message: `phase complete: ${label}`,
116
- meta: { phase: label, detail: detail ?? null, elapsedMs },
117
- });
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
+ }
118
132
  if (this.isTTY) {
119
133
  this.flushRender();
120
134
  }
@@ -201,3 +215,4 @@ class UpProgress {
201
215
  }
202
216
  }
203
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.418",
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",