@ouro.bot/cli 0.1.0-alpha.421 → 0.1.0-alpha.423

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
@@ -179,7 +179,10 @@ ouro vault status --agent <name>
179
179
  ouro vault config set --agent <name> --key teams.clientSecret
180
180
  ouro vault config status --agent <name> --scope all
181
181
  ouro connect --agent <name>
182
+ ouro connect providers --agent <name>
182
183
  ouro connect perplexity --agent <name>
184
+ ouro connect embeddings --agent <name>
185
+ ouro connect teams --agent <name>
183
186
  ouro connect bluebubbles --agent <name>
184
187
  ouro auth --agent <name>
185
188
  ouro auth --agent <name> --provider <provider>
package/changelog.json CHANGED
@@ -1,6 +1,25 @@
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.423",
6
+ "changes": [
7
+ "`ouro connect` now opens a real connect bay with one guided surface for provider auth, Perplexity, OpenAI embeddings, Teams, and BlueBubbles instead of a tiny menu that still required humans to memorize follow-up commands.",
8
+ "Added guided `ouro connect providers`, `ouro connect embeddings`, and `ouro connect teams` flows, plus richer connect-bay routing that distinguishes portable runtime config from machine-local attachments without printing secrets.",
9
+ "Credential/runtime changes now reuse one shared runtime-apply helper that checks daemon socket access, requests restart, polls daemon status, times out cleanly, and falls back clearly when daemon status is unavailable.",
10
+ "Command progress rendering now marks failed phases honestly with a failure state instead of a success checkmark plus `failed`, and the auth/provider docs, README, testing guide, OAUTH setup guide, and prompt truth text all point at the new connect-bay workflow.",
11
+ "`@ouro.bot/cli` and the `ouro.bot` wrapper are version-synced for the connect-bay and runtime-apply progress release."
12
+ ]
13
+ },
14
+ {
15
+ "version": "0.1.0-alpha.422",
16
+ "changes": [
17
+ "`ouro auth` now streams provider-vault read progress before browser/token work begins, so a locked, slow, or remote vault never looks like a dead terminal after the initial access check.",
18
+ "Provider credential saves now stream the post-write provider snapshot reload, including vault item reads and parsing, instead of going silent after `refreshing local provider snapshot`.",
19
+ "Auth-flow tests cover the pre-auth vault read and real Bitwarden-backed post-save refresh progress so the visible credential path stays protected.",
20
+ "`@ouro.bot/cli` and the `ouro.bot` wrapper are version-synced for the final auth-flow vault progress sweep."
21
+ ]
22
+ },
4
23
  {
5
24
  "version": "0.1.0-alpha.421",
6
25
  "changes": [
@@ -387,7 +387,9 @@ async function runRuntimeAuthFlow(input, deps = {}) {
387
387
  meta: { agentName: input.agentName, provider: input.provider },
388
388
  });
389
389
  writeAuthProgress(input, `checking ${input.agentName}'s vault access...`);
390
- const vault = await (0, provider_credentials_1.refreshProviderCredentialPool)(input.agentName);
390
+ const vault = await (0, provider_credentials_1.refreshProviderCredentialPool)(input.agentName, {
391
+ onProgress: (message) => writeAuthProgress(input, message),
392
+ });
391
393
  if (!vault.ok && vault.reason === "unavailable") {
392
394
  const fix = (0, vault_unlock_1.isCredentialVaultNotConfiguredError)(vault.error)
393
395
  ? (0, vault_unlock_1.vaultCreateRecoverFix)(input.agentName, `Then retry 'ouro auth --agent ${input.agentName} --provider ${input.provider}'.`)
@@ -71,6 +71,7 @@ const auth_flow_1 = require("../auth/auth-flow");
71
71
  const provider_credentials_1 = require("../provider-credentials");
72
72
  const runtime_credentials_1 = require("../runtime-credentials");
73
73
  const provider_binding_resolver_1 = require("../provider-binding-resolver");
74
+ const provider_visibility_1 = require("../provider-visibility");
74
75
  const provider_state_1 = require("../provider-state");
75
76
  const machine_identity_1 = require("../machine-identity");
76
77
  const provider_models_1 = require("../provider-models");
@@ -197,7 +198,7 @@ async function runCommandProgressPhase(progress, label, run, detail) {
197
198
  return result;
198
199
  }
199
200
  catch (error) {
200
- progress.completePhase(label, "failed");
201
+ progress.failPhase(label, "failed");
201
202
  throw error;
202
203
  }
203
204
  }
@@ -1227,6 +1228,50 @@ function currentMachineId(deps) {
1227
1228
  now: () => providerCliNow(deps),
1228
1229
  }).machineId;
1229
1230
  }
1231
+ const DEFAULT_RUNTIME_APPLY_TIMEOUT_MS = 15_000;
1232
+ const DEFAULT_RUNTIME_APPLY_POLL_INTERVAL_MS = 500;
1233
+ const DEFAULT_DAEMON_STATUS_TIMEOUT_MS = 4_000;
1234
+ const DEFAULT_AGENT_RESTART_TIMEOUT_MS = 8_000;
1235
+ const CONNECT_PROVIDER_CHOICES = ["openai-codex", "anthropic", "minimax", "azure", "github-copilot"];
1236
+ function hasRuntimeConfigValue(config, key) {
1237
+ const segments = key.split(".");
1238
+ let cursor = config;
1239
+ for (const segment of segments) {
1240
+ if (!cursor || typeof cursor !== "object" || Array.isArray(cursor))
1241
+ return false;
1242
+ cursor = cursor[segment];
1243
+ }
1244
+ return typeof cursor === "string" && cursor.trim().length > 0;
1245
+ }
1246
+ function runtimeConfigReadStatus(runtime) {
1247
+ if (runtime.reason === "missing")
1248
+ return "missing";
1249
+ if (/locked/i.test(runtime.error))
1250
+ return "locked";
1251
+ return "needs attention";
1252
+ }
1253
+ function cliNowMs(deps) {
1254
+ return (deps.now ?? Date.now)();
1255
+ }
1256
+ function cliSleep(deps, ms) {
1257
+ if (deps.sleep)
1258
+ return deps.sleep(ms);
1259
+ return new Promise((resolve) => setTimeout(resolve, ms));
1260
+ }
1261
+ async function withCliTimeout(timeoutMs, label, run) {
1262
+ let timer;
1263
+ try {
1264
+ return await Promise.race([
1265
+ run(),
1266
+ new Promise((_, reject) => {
1267
+ timer = setTimeout(() => reject(new Error(label)), timeoutMs);
1268
+ }),
1269
+ ]);
1270
+ }
1271
+ finally {
1272
+ clearTimeout(timer);
1273
+ }
1274
+ }
1230
1275
  async function promptRuntimeConfigValue(command, deps) {
1231
1276
  if (command.value !== undefined)
1232
1277
  return command.value;
@@ -1242,7 +1287,7 @@ async function promptRuntimeConfigValue(command, deps) {
1242
1287
  function runtimeScopeLabel(scope) {
1243
1288
  return scope === "machine" ? "this machine's vault runtime config item" : "the agent vault runtime/config item";
1244
1289
  }
1245
- async function storeRuntimeConfigKey(input) {
1290
+ async function storeRuntimeConfigKeys(input) {
1246
1291
  const machineId = input.scope === "machine" ? currentMachineId(input.deps) : undefined;
1247
1292
  input.onProgress?.("checking existing runtime config");
1248
1293
  const current = input.scope === "machine"
@@ -1251,23 +1296,77 @@ async function storeRuntimeConfigKey(input) {
1251
1296
  if (!current.ok && current.reason !== "missing") {
1252
1297
  throw new Error(`cannot read existing runtime credentials from ${current.itemPath}: ${current.error}`);
1253
1298
  }
1254
- input.onProgress?.(`storing ${input.key} in ${current.itemPath}`);
1255
- const nextConfig = setRuntimeConfigValue(current.ok ? current.config : {}, input.key, input.value);
1299
+ let nextConfig = current.ok ? current.config : {};
1300
+ for (const entry of input.entries) {
1301
+ input.onProgress?.(`storing ${entry.key} in ${current.itemPath}`);
1302
+ nextConfig = setRuntimeConfigValue(nextConfig, entry.key, entry.value);
1303
+ }
1256
1304
  const stored = input.scope === "machine"
1257
1305
  ? await (0, runtime_credentials_1.upsertMachineRuntimeCredentialConfig)(input.agent, machineId, nextConfig, providerCliNow(input.deps))
1258
1306
  : await (0, runtime_credentials_1.upsertRuntimeCredentialConfig)(input.agent, nextConfig, providerCliNow(input.deps));
1259
- input.onProgress?.(`stored ${input.key}; credential value was not printed`);
1307
+ input.onProgress?.(`stored ${input.entries.map((entry) => entry.key).join(", ")}; credential values were not printed`);
1260
1308
  return { revision: stored.revision, itemPath: stored.itemPath, ...(machineId ? { machineId } : {}) };
1261
1309
  }
1262
- async function reloadRunningAgentAfterCredentialChange(agent, deps) {
1310
+ async function storeRuntimeConfigKey(input) {
1311
+ return storeRuntimeConfigKeys({
1312
+ agent: input.agent,
1313
+ entries: [{ key: input.key, value: input.value }],
1314
+ scope: input.scope,
1315
+ deps: input.deps,
1316
+ onProgress: input.onProgress,
1317
+ });
1318
+ }
1319
+ function readRuntimeApplyWorker(payload, agent) {
1320
+ return payload.workers.find((worker) => worker.agent === agent) ?? null;
1321
+ }
1322
+ async function applyRuntimeChangeToRunningAgent(agent, deps, onProgress) {
1263
1323
  try {
1324
+ onProgress?.("checking daemon socket");
1264
1325
  const alive = await deps.checkSocketAlive(deps.socketPath);
1265
1326
  if (!alive)
1266
- return "daemon is not running; next `ouro up` will load it";
1267
- const response = await deps.sendCommand(deps.socketPath, { kind: "agent.restart", agent });
1268
- if (response.ok)
1269
- return `restarted ${agent} so the running agent reloads credentials`;
1270
- return `daemon restart skipped: ${response.error ?? response.message ?? "unknown daemon error"}`;
1327
+ return "daemon is not running; next `ouro up` will load the change";
1328
+ onProgress?.("requesting restart from daemon");
1329
+ const response = await withCliTimeout(DEFAULT_AGENT_RESTART_TIMEOUT_MS, "daemon restart request timed out", () => deps.sendCommand(deps.socketPath, { kind: "agent.restart", agent }));
1330
+ if (!response.ok)
1331
+ return `daemon restart skipped: ${response.error ?? response.message ?? "unknown daemon error"}`;
1332
+ const deadline = cliNowMs(deps) + DEFAULT_RUNTIME_APPLY_TIMEOUT_MS;
1333
+ onProgress?.(`waiting for ${agent} to report running state`);
1334
+ while (cliNowMs(deps) < deadline) {
1335
+ try {
1336
+ const statusResponse = await withCliTimeout(DEFAULT_DAEMON_STATUS_TIMEOUT_MS, "daemon status timed out", () => deps.sendCommand(deps.socketPath, { kind: "daemon.status" }));
1337
+ if (statusResponse.ok) {
1338
+ const payload = (0, cli_render_1.parseStatusPayload)(statusResponse.data);
1339
+ if (!payload) {
1340
+ onProgress?.("daemon status did not include structured worker state");
1341
+ return "restart requested; daemon status is unavailable, so verify with `ouro status` if needed";
1342
+ }
1343
+ const worker = readRuntimeApplyWorker(payload, agent);
1344
+ if (!worker) {
1345
+ onProgress?.(`still waiting: ${agent} is not listed by daemon`);
1346
+ }
1347
+ else if (worker.status === "running") {
1348
+ onProgress?.(`daemon reports ${agent}/${worker.worker} running`);
1349
+ return `restarted ${agent} and the daemon reports it running`;
1350
+ }
1351
+ else if (worker.status === "crashed") {
1352
+ return `restart requested, but ${agent}/${worker.worker} crashed before reporting running${worker.errorReason ? `: ${worker.errorReason}` : worker.fixHint ? `: ${worker.fixHint}` : ""}`;
1353
+ }
1354
+ else {
1355
+ onProgress?.(`still waiting: ${agent}/${worker.worker} is ${worker.status}`);
1356
+ }
1357
+ }
1358
+ else {
1359
+ onProgress?.(`still waiting: daemon status returned ${statusResponse.error ?? statusResponse.message ?? "unknown error"}`);
1360
+ }
1361
+ }
1362
+ catch (error) {
1363
+ onProgress?.(`still waiting: ${error instanceof Error ? error.message : String(error)}`);
1364
+ }
1365
+ if (cliNowMs(deps) >= deadline - DEFAULT_RUNTIME_APPLY_POLL_INTERVAL_MS)
1366
+ break;
1367
+ await cliSleep(deps, DEFAULT_RUNTIME_APPLY_POLL_INTERVAL_MS);
1368
+ }
1369
+ return `restart requested, but ${agent} did not report running before timeout. The change is stored; run \`ouro status\` or \`ouro up\` if it still looks stale.`;
1271
1370
  }
1272
1371
  catch (error) {
1273
1372
  return `daemon restart skipped: ${error instanceof Error ? error.message : String(error)}`;
@@ -1401,23 +1500,70 @@ function enableAgentSense(agent, sense, deps) {
1401
1500
  };
1402
1501
  fs.writeFileSync(configPath, `${JSON.stringify(raw, null, 2)}\n`, "utf-8");
1403
1502
  }
1404
- function connectMenu(agent) {
1503
+ async function buildConnectMenu(agent, deps) {
1504
+ const providerVisibility = (0, provider_visibility_1.buildAgentProviderVisibility)({
1505
+ agentName: agent,
1506
+ agentRoot: providerCliAgentRoot({ agent }, deps),
1507
+ homeDir: providerCliHomeDir(deps),
1508
+ });
1509
+ const providerStatus = providerVisibility.lanes.some((lane) => lane.status === "unconfigured")
1510
+ ? "needs setup"
1511
+ : providerVisibility.lanes.some((lane) => lane.status === "configured" && lane.credential.status !== "present")
1512
+ ? "needs auth"
1513
+ : providerVisibility.lanes.some((lane) => lane.status === "configured" && (lane.readiness.status === "failed" || lane.readiness.status === "stale"))
1514
+ ? "needs attention"
1515
+ : "ready";
1516
+ const providerDetail = providerVisibility.lanes.map((lane) => lane.status === "configured"
1517
+ ? `${lane.lane}: ${lane.provider} / ${lane.model}`
1518
+ : `${lane.lane}: choose provider/model`).join(" | ");
1519
+ const runtimeConfig = await (0, runtime_credentials_1.refreshRuntimeCredentialConfig)(agent, { preserveCachedOnFailure: true });
1520
+ const machineRuntime = await (0, runtime_credentials_1.refreshMachineRuntimeCredentialConfig)(agent, currentMachineId(deps), { preserveCachedOnFailure: true });
1521
+ const agentConfig = (0, auth_flow_1.readAgentConfigForAgent)(agent, deps.bundlesRoot).config;
1522
+ const teamsEnabled = agentConfig.senses?.teams?.enabled === true;
1523
+ const blueBubblesEnabled = agentConfig.senses?.bluebubbles?.enabled === true;
1524
+ const perplexityStatus = runtimeConfig.ok
1525
+ ? hasRuntimeConfigValue(runtimeConfig.config, "integrations.perplexityApiKey") ? "ready" : "missing"
1526
+ : runtimeConfigReadStatus(runtimeConfig);
1527
+ const embeddingsStatus = runtimeConfig.ok
1528
+ ? hasRuntimeConfigValue(runtimeConfig.config, "integrations.openaiEmbeddingsApiKey") ? "ready" : "missing"
1529
+ : runtimeConfigReadStatus(runtimeConfig);
1530
+ const teamsStatus = runtimeConfig.ok
1531
+ ? hasRuntimeConfigValue(runtimeConfig.config, "teams.clientId")
1532
+ && hasRuntimeConfigValue(runtimeConfig.config, "teams.clientSecret")
1533
+ && hasRuntimeConfigValue(runtimeConfig.config, "teams.tenantId")
1534
+ && teamsEnabled
1535
+ ? "ready"
1536
+ : "missing"
1537
+ : runtimeConfigReadStatus(runtimeConfig);
1538
+ const blueBubblesStatus = machineRuntime.ok
1539
+ ? hasRuntimeConfigValue(machineRuntime.config, "bluebubbles.serverUrl")
1540
+ && hasRuntimeConfigValue(machineRuntime.config, "bluebubbles.password")
1541
+ && blueBubblesEnabled
1542
+ ? "attached"
1543
+ : "not attached"
1544
+ : machineRuntimeReadStatus(machineRuntime);
1405
1545
  return [
1406
- `Connect ${agent}`,
1407
- "Pick what this agent should be able to use.",
1546
+ `${agent} // connect bay`,
1547
+ "Bring one capability online at a time.",
1408
1548
  "",
1409
- " 1. Perplexity search",
1410
- " Portable. Stores an API key in the agent vault.",
1549
+ ` 1. [${providerStatus}] Providers`,
1550
+ ` ${providerDetail}`,
1411
1551
  "",
1412
- " 2. BlueBubbles iMessage",
1413
- " This machine only. Connects a local Mac Messages bridge.",
1552
+ ` 2. [${perplexityStatus}] Perplexity search`,
1553
+ " Portable. Web search via Perplexity.",
1414
1554
  "",
1415
- " 3. Provider auth",
1416
- ` Model credentials: ouro auth --agent ${agent} --provider <provider>`,
1555
+ ` 3. [${embeddingsStatus}] Memory embeddings`,
1556
+ " Portable. Memory retrieval and note search.",
1417
1557
  "",
1418
- " 4. Cancel",
1558
+ ` 4. [${teamsStatus}] Teams`,
1559
+ " Portable. Microsoft Teams sense credentials.",
1419
1560
  "",
1420
- "Choose [1-4]: ",
1561
+ ` 5. [${blueBubblesStatus}] BlueBubbles iMessage`,
1562
+ " This machine only. Local Mac Messages bridge.",
1563
+ "",
1564
+ " 6. Cancel",
1565
+ "",
1566
+ "Choose [1-6] or type a name: ",
1421
1567
  ].join("\n");
1422
1568
  }
1423
1569
  async function executeConnectPerplexity(agent, deps) {
@@ -1437,30 +1583,24 @@ async function executeConnectPerplexity(agent, deps) {
1437
1583
  let stored;
1438
1584
  let reload;
1439
1585
  try {
1440
- progress.startPhase("saving Perplexity search");
1441
- stored = await storeRuntimeConfigKey({
1586
+ stored = await runCommandProgressPhase(progress, "saving Perplexity search", () => storeRuntimeConfigKey({
1442
1587
  agent,
1443
1588
  key: "integrations.perplexityApiKey",
1444
1589
  value: key,
1445
1590
  scope: "agent",
1446
1591
  deps,
1447
1592
  onProgress: (message) => progress.updateDetail(message),
1448
- });
1449
- progress.completePhase("saving Perplexity search", "secret stored");
1450
- progress.startPhase(`reloading ${agent}`);
1451
- reload = await reloadRunningAgentAfterCredentialChange(agent, deps);
1452
- progress.completePhase(`reloading ${agent}`, reload);
1453
- progress.end();
1593
+ }), () => "secret stored");
1594
+ reload = await runCommandProgressPhase(progress, `applying change to running ${agent}`, () => applyRuntimeChangeToRunningAgent(agent, deps, (message) => progress.updateDetail(message)), (result) => result);
1454
1595
  }
1455
- catch (error) {
1596
+ finally {
1456
1597
  progress.end();
1457
- throw error;
1458
1598
  }
1459
1599
  const message = [
1460
1600
  `Perplexity connected for ${agent}`,
1461
1601
  "capability: Perplexity search",
1462
1602
  `stored: ${stored.itemPath}`,
1463
- `reload: ${reload}`,
1603
+ `running agent: ${reload}`,
1464
1604
  "secret was not printed",
1465
1605
  "",
1466
1606
  "Next: ask the agent to search.",
@@ -1468,6 +1608,102 @@ async function executeConnectPerplexity(agent, deps) {
1468
1608
  deps.writeStdout(message);
1469
1609
  return message;
1470
1610
  }
1611
+ async function executeConnectEmbeddings(agent, deps) {
1612
+ if (agent === "SerpentGuide") {
1613
+ throw new Error("SerpentGuide has no persistent runtime credentials. Connect embeddings on the hatchling agent instead.");
1614
+ }
1615
+ const promptSecret = requirePromptSecret(deps, "OpenAI embeddings API key entry");
1616
+ deps.writeStdout([
1617
+ `Connect embeddings for ${agent}`,
1618
+ "The API key stays hidden while you type.",
1619
+ `Ouro stores it in ${agent}'s vault runtime/config item.`,
1620
+ ].join("\n"));
1621
+ const key = (await promptSecret("OpenAI embeddings API key: ")).trim();
1622
+ if (!key)
1623
+ throw new Error("OpenAI embeddings API key cannot be blank");
1624
+ const progress = createHumanCommandProgress(deps, "connect embeddings");
1625
+ let stored;
1626
+ let reload;
1627
+ try {
1628
+ stored = await runCommandProgressPhase(progress, "saving memory embeddings", () => storeRuntimeConfigKey({
1629
+ agent,
1630
+ key: "integrations.openaiEmbeddingsApiKey",
1631
+ value: key,
1632
+ scope: "agent",
1633
+ deps,
1634
+ onProgress: (message) => progress.updateDetail(message),
1635
+ }), () => "secret stored");
1636
+ reload = await runCommandProgressPhase(progress, `applying change to running ${agent}`, () => applyRuntimeChangeToRunningAgent(agent, deps, (message) => progress.updateDetail(message)), (result) => result);
1637
+ }
1638
+ finally {
1639
+ progress.end();
1640
+ }
1641
+ const message = [
1642
+ `Embeddings connected for ${agent}`,
1643
+ "capability: memory embeddings",
1644
+ `stored: ${stored.itemPath}`,
1645
+ `running agent: ${reload}`,
1646
+ "secret was not printed",
1647
+ "",
1648
+ "Next: ask the agent to search notes or memory.",
1649
+ ].join("\n");
1650
+ deps.writeStdout(message);
1651
+ return message;
1652
+ }
1653
+ async function executeConnectTeams(agent, deps) {
1654
+ if (agent === "SerpentGuide") {
1655
+ throw new Error("SerpentGuide has no persistent runtime credentials. Connect Teams on the hatchling agent instead.");
1656
+ }
1657
+ const promptInput = requirePromptInput(deps, "Teams setup");
1658
+ const promptSecret = requirePromptSecret(deps, "Teams client secret entry");
1659
+ deps.writeStdout([
1660
+ `Connect Teams for ${agent}`,
1661
+ "This connects the Teams sense.",
1662
+ "The client secret stays hidden while you type.",
1663
+ ].join("\n"));
1664
+ const clientId = (await promptInput("Teams client ID: ")).trim();
1665
+ if (!clientId)
1666
+ throw new Error("Teams client ID cannot be blank");
1667
+ const clientSecret = (await promptSecret("Teams client secret: ")).trim();
1668
+ if (!clientSecret)
1669
+ throw new Error("Teams client secret cannot be blank");
1670
+ const tenantId = (await promptInput("Teams tenant ID: ")).trim();
1671
+ if (!tenantId)
1672
+ throw new Error("Teams tenant ID cannot be blank");
1673
+ const managedIdentityClientId = (await promptInput("Teams managed identity client ID [blank to skip]: ")).trim();
1674
+ const progress = createHumanCommandProgress(deps, "connect teams");
1675
+ let stored;
1676
+ try {
1677
+ stored = await runCommandProgressPhase(progress, "saving Teams setup", () => storeRuntimeConfigKeys({
1678
+ agent,
1679
+ entries: [
1680
+ { key: "teams.clientId", value: clientId },
1681
+ { key: "teams.clientSecret", value: clientSecret },
1682
+ { key: "teams.tenantId", value: tenantId },
1683
+ ...(managedIdentityClientId ? [{ key: "teams.managedIdentityClientId", value: managedIdentityClientId }] : []),
1684
+ ],
1685
+ scope: "agent",
1686
+ deps,
1687
+ onProgress: (message) => progress.updateDetail(message),
1688
+ }), () => "secret stored");
1689
+ progress.updateDetail("enabling Teams in agent.json");
1690
+ enableAgentSense(agent, "teams", deps);
1691
+ }
1692
+ finally {
1693
+ progress.end();
1694
+ }
1695
+ const message = appendBundleSyncSummary([
1696
+ `Teams connected for ${agent}`,
1697
+ "capability: Teams sense",
1698
+ `stored: ${stored.itemPath}`,
1699
+ "agent.json: senses.teams.enabled = true",
1700
+ "secret was not printed",
1701
+ "",
1702
+ "Next: run `ouro up` so the daemon picks up the Teams sense change.",
1703
+ ].join("\n"), agent, deps);
1704
+ deps.writeStdout(message);
1705
+ return message;
1706
+ }
1471
1707
  async function executeConnectBlueBubbles(agent, deps) {
1472
1708
  if (agent === "SerpentGuide") {
1473
1709
  throw new Error("SerpentGuide has no persistent runtime credentials. Attach BlueBubbles on the hatchling agent instead.");
@@ -1535,39 +1771,86 @@ async function executeConnectBlueBubbles(agent, deps) {
1535
1771
  deps.writeStdout(message);
1536
1772
  return message;
1537
1773
  }
1774
+ async function executeConnectProviders(agent, deps) {
1775
+ const promptInput = deps.promptInput;
1776
+ if (!promptInput) {
1777
+ const message = [
1778
+ `Provider auth for ${agent}`,
1779
+ "Run one of:",
1780
+ ...CONNECT_PROVIDER_CHOICES.map((provider) => ` ouro auth --agent ${agent} --provider ${provider}`),
1781
+ ].join("\n");
1782
+ deps.writeStdout(message);
1783
+ return message;
1784
+ }
1785
+ const choice = (await promptInput([
1786
+ `Which provider should ${agent} use credentials for?`,
1787
+ ...CONNECT_PROVIDER_CHOICES.map((provider) => ` - ${provider}`),
1788
+ "",
1789
+ "Provider: ",
1790
+ ].join("\n"))).trim().toLowerCase();
1791
+ if (!(0, cli_parse_2.isAgentProvider)(choice)) {
1792
+ throw new Error(`Unknown provider '${choice}'. Use ${CONNECT_PROVIDER_CHOICES.join("|")}.`);
1793
+ }
1794
+ return executeAuthRun({ kind: "auth.run", agent, provider: choice }, deps);
1795
+ }
1796
+ function machineRuntimeReadStatus(runtime) {
1797
+ if (runtime.reason === "missing")
1798
+ return "not attached";
1799
+ if (/locked/i.test(runtime.error))
1800
+ return "locked";
1801
+ return "needs attention";
1802
+ }
1803
+ function connectMenuTarget(answer) {
1804
+ const normalized = answer.trim().toLowerCase();
1805
+ if (normalized === "1" || normalized === "providers" || normalized === "provider" || normalized === "auth")
1806
+ return "providers";
1807
+ if (normalized === "2" || normalized === "perplexity" || normalized === "perplexity-search" || normalized === "search")
1808
+ return "perplexity";
1809
+ if (normalized === "3" || normalized === "embeddings" || normalized === "embedding" || normalized === "memory" || normalized === "note-search" || normalized === "notes")
1810
+ return "embeddings";
1811
+ if (normalized === "4" || normalized === "teams" || normalized === "msteams" || normalized === "microsoft-teams")
1812
+ return "teams";
1813
+ if (normalized === "5" || normalized === "bluebubbles" || normalized === "imessage" || normalized === "messages")
1814
+ return "bluebubbles";
1815
+ return "cancel";
1816
+ }
1538
1817
  async function executeConnect(command, deps) {
1818
+ if (command.target === "providers")
1819
+ return executeConnectProviders(command.agent, deps);
1539
1820
  if (command.target === "perplexity")
1540
1821
  return executeConnectPerplexity(command.agent, deps);
1822
+ if (command.target === "embeddings")
1823
+ return executeConnectEmbeddings(command.agent, deps);
1824
+ if (command.target === "teams")
1825
+ return executeConnectTeams(command.agent, deps);
1541
1826
  if (command.target === "bluebubbles")
1542
1827
  return executeConnectBlueBubbles(command.agent, deps);
1828
+ const menu = await buildConnectMenu(command.agent, deps);
1543
1829
  const promptInput = deps.promptInput;
1544
1830
  if (!promptInput) {
1545
1831
  const message = [
1546
- connectMenu(command.agent).replace(/\nChoose \[1-4\]: $/, ""),
1832
+ menu.replace(/\nChoose \[1-6\] or type a name: $/, ""),
1547
1833
  "",
1834
+ `Run: ouro connect providers --agent ${command.agent}`,
1548
1835
  `Run: ouro connect perplexity --agent ${command.agent}`,
1549
- `Or: ouro connect bluebubbles --agent ${command.agent}`,
1836
+ `Run: ouro connect embeddings --agent ${command.agent}`,
1837
+ `Run: ouro connect teams --agent ${command.agent}`,
1838
+ `Run: ouro connect bluebubbles --agent ${command.agent}`,
1550
1839
  ].join("\n");
1551
1840
  deps.writeStdout(message);
1552
1841
  return message;
1553
1842
  }
1554
- const answer = (await promptInput(connectMenu(command.agent))).trim().toLowerCase();
1555
- if (answer === "1" || answer === "perplexity" || answer === "perplexity-search") {
1843
+ const answer = connectMenuTarget(await promptInput(menu));
1844
+ if (answer === "providers")
1845
+ return executeConnectProviders(command.agent, deps);
1846
+ if (answer === "perplexity")
1556
1847
  return executeConnectPerplexity(command.agent, deps);
1557
- }
1558
- if (answer === "2" || answer === "bluebubbles" || answer === "imessage" || answer === "messages") {
1848
+ if (answer === "embeddings")
1849
+ return executeConnectEmbeddings(command.agent, deps);
1850
+ if (answer === "teams")
1851
+ return executeConnectTeams(command.agent, deps);
1852
+ if (answer === "bluebubbles")
1559
1853
  return executeConnectBlueBubbles(command.agent, deps);
1560
- }
1561
- if (answer === "3" || answer === "provider" || answer === "providers" || answer === "auth") {
1562
- const message = [
1563
- "Provider auth is its own flow:",
1564
- ` ouro auth --agent ${command.agent} --provider <provider>`,
1565
- "",
1566
- "Use `ouro connect` for integrations and local senses after provider auth is ready.",
1567
- ].join("\n");
1568
- deps.writeStdout(message);
1569
- return message;
1570
- }
1571
1854
  const message = "connect cancelled.";
1572
1855
  deps.writeStdout(message);
1573
1856
  return message;
@@ -1896,13 +2179,9 @@ async function executeProviderRefresh(command, deps) {
1896
2179
  deps.writeStdout(message);
1897
2180
  return message;
1898
2181
  }
1899
- progress.startPhase(`reloading ${command.agent}`);
1900
- const reload = await reloadRunningAgentAfterCredentialChange(command.agent, deps);
1901
- progress.completePhase(`reloading ${command.agent}`, reload);
2182
+ const reload = await runCommandProgressPhase(progress, `applying change to running ${command.agent}`, () => applyRuntimeChangeToRunningAgent(command.agent, deps, (message) => progress.updateDetail(message)), (result) => result);
1902
2183
  progress.end();
1903
- lines.push(reload === "daemon is not running; next `ouro up` will load it"
1904
- ? "daemon is not running; the next start will load the refreshed snapshot"
1905
- : reload);
2184
+ lines.push(`running agent: ${reload}`);
1906
2185
  const message = lines.join("\n");
1907
2186
  deps.writeStdout(message);
1908
2187
  return message;
@@ -165,10 +165,10 @@ exports.COMMAND_REGISTRY = {
165
165
  },
166
166
  connect: {
167
167
  category: "Auth",
168
- description: "Connect integrations and local senses such as Perplexity search and BlueBubbles iMessage",
169
- usage: "ouro connect [perplexity|bluebubbles] --agent <name>",
170
- example: "ouro connect perplexity --agent ouroboros",
171
- subcommands: ["perplexity", "bluebubbles"],
168
+ description: "Connect providers, portable integrations, and local senses from one guided bay",
169
+ usage: "ouro connect [providers|perplexity|embeddings|teams|bluebubbles] --agent <name>",
170
+ example: "ouro connect --agent ouroboros",
171
+ subcommands: ["providers", "perplexity", "embeddings", "teams", "bluebubbles"],
172
172
  },
173
173
  use: {
174
174
  category: "Auth",
@@ -277,6 +277,21 @@ const SUBCOMMAND_HELP = {
277
277
  usage: "ouro connect perplexity --agent <name>",
278
278
  example: "ouro connect perplexity --agent ouroboros",
279
279
  },
280
+ "connect providers": {
281
+ description: "Open provider auth from the connect bay without remembering the auth command",
282
+ usage: "ouro connect providers --agent <name>",
283
+ example: "ouro connect providers --agent ouroboros",
284
+ },
285
+ "connect embeddings": {
286
+ description: "Connect memory embeddings for this agent",
287
+ usage: "ouro connect embeddings --agent <name>",
288
+ example: "ouro connect embeddings --agent ouroboros",
289
+ },
290
+ "connect teams": {
291
+ description: "Connect Microsoft Teams credentials and enable the Teams sense",
292
+ usage: "ouro connect teams --agent <name>",
293
+ example: "ouro connect teams --agent ouroboros",
294
+ },
280
295
  "connect bluebubbles": {
281
296
  description: "Attach BlueBubbles iMessage to this machine only",
282
297
  usage: "ouro connect bluebubbles --agent <name>",
@@ -83,7 +83,7 @@ function usage() {
83
83
  " ouro config model --agent <name> <model-name>",
84
84
  " ouro config models --agent <name>",
85
85
  " ouro auth --agent <name> [--provider <provider>]",
86
- " ouro connect [perplexity|bluebubbles] --agent <name>",
86
+ " ouro connect [providers|perplexity|embeddings|teams|bluebubbles] --agent <name>",
87
87
  " ouro auth verify --agent <name> [--provider <provider>]",
88
88
  " ouro auth switch --agent <name> --provider <provider>",
89
89
  " ouro vault create --agent <name> --email <email> [--server <url>] [--store <store>]",
@@ -588,18 +588,24 @@ function parseVaultConfigCommand(args) {
588
588
  function normalizeConnectTarget(value) {
589
589
  if (!value)
590
590
  return undefined;
591
+ if (value === "providers" || value === "provider" || value === "auth")
592
+ return "providers";
591
593
  if (value === "perplexity" || value === "perplexity-search")
592
594
  return "perplexity";
595
+ if (value === "embeddings" || value === "embedding" || value === "memory" || value === "note-search" || value === "notes")
596
+ return "embeddings";
597
+ if (value === "teams" || value === "msteams" || value === "microsoft-teams")
598
+ return "teams";
593
599
  if (value === "bluebubbles" || value === "imessage" || value === "messages")
594
600
  return "bluebubbles";
595
- throw new Error("Usage: ouro connect [perplexity|bluebubbles] --agent <name>");
601
+ throw new Error("Usage: ouro connect [providers|perplexity|embeddings|teams|bluebubbles] --agent <name>");
596
602
  }
597
603
  function parseConnectCommand(args) {
598
604
  const { agent, rest } = extractAgentFlag(args);
599
605
  if (!agent)
600
- throw new Error("Usage: ouro connect --agent <name> [perplexity|bluebubbles]");
606
+ throw new Error("Usage: ouro connect --agent <name> [providers|perplexity|embeddings|teams|bluebubbles]");
601
607
  if (rest.length > 1)
602
- throw new Error("Usage: ouro connect [perplexity|bluebubbles] --agent <name>");
608
+ throw new Error("Usage: ouro connect [providers|perplexity|embeddings|teams|bluebubbles] --agent <name>");
603
609
  const target = normalizeConnectTarget(rest[0]);
604
610
  return { kind: "connect", agent, ...(target ? { target } : {}) };
605
611
  }
@@ -20,6 +20,7 @@ const RESET = "\x1b[0m";
20
20
  const BOLD = "\x1b[1m";
21
21
  const DIM = "\x1b[2m";
22
22
  const GREEN = "\x1b[38;2;46;204;64m";
23
+ const RED = "\x1b[38;2;255;106;106m";
23
24
  // ── UpProgress class ──
24
25
  class UpProgress {
25
26
  write;
@@ -109,7 +110,7 @@ class UpProgress {
109
110
  return;
110
111
  }
111
112
  const elapsedMs = this.now() - this.currentPhase.startedAt;
112
- this.completed.push({ label, detail });
113
+ this.completed.push({ status: "success", label, detail });
113
114
  this.currentPhase = null;
114
115
  this.currentDetail = null;
115
116
  this.stopAutoRender();
@@ -137,6 +138,41 @@ class UpProgress {
137
138
  this.write(` \u2713 ${label}${detailStr}\n`);
138
139
  }
139
140
  }
141
+ failPhase(label, detail) {
142
+ if (!this.currentPhase) {
143
+ return;
144
+ }
145
+ const elapsedMs = this.now() - this.currentPhase.startedAt;
146
+ this.completed.push({ status: "failure", label, detail });
147
+ this.currentPhase = null;
148
+ this.currentDetail = null;
149
+ this.stopAutoRender();
150
+ if (this.eventScope === "command") {
151
+ (0, runtime_1.emitNervesEvent)({
152
+ level: "warn",
153
+ component: "daemon",
154
+ event: "daemon.cli_progress_phase_failed",
155
+ message: `phase failed: ${label}`,
156
+ meta: { command: this.commandName, phase: label, detail: detail ?? null, elapsedMs },
157
+ });
158
+ }
159
+ else {
160
+ (0, runtime_1.emitNervesEvent)({
161
+ level: "warn",
162
+ component: "daemon",
163
+ event: "daemon.up_phase_failed",
164
+ message: `phase failed: ${label}`,
165
+ meta: { phase: label, detail: detail ?? null, elapsedMs },
166
+ });
167
+ }
168
+ if (this.isTTY) {
169
+ this.flushRender();
170
+ }
171
+ else {
172
+ const detailStr = detail ? ` \u2014 ${detail}` : "";
173
+ this.write(` \u2717 ${label}${detailStr}\n`);
174
+ }
175
+ }
140
176
  /**
141
177
  * Build an ANSI string for in-place terminal display. Returns empty
142
178
  * string in non-TTY mode (output is written eagerly in completePhase).
@@ -149,7 +185,12 @@ class UpProgress {
149
185
  // Completed phases
150
186
  for (const phase of this.completed) {
151
187
  const detailStr = phase.detail ? ` ${DIM}\u2014 ${phase.detail}${RESET}` : "";
152
- lines.push(` ${GREEN}\u2713${RESET} ${phase.label}${detailStr}`);
188
+ if (phase.status === "failure") {
189
+ lines.push(` ${RED}\u2717${RESET} ${phase.label}${detailStr}`);
190
+ }
191
+ else {
192
+ lines.push(` ${GREEN}\u2713${RESET} ${phase.label}${detailStr}`);
193
+ }
153
194
  }
154
195
  // Current phase with spinner
155
196
  if (this.currentPhase) {
@@ -340,7 +340,9 @@ async function upsertProviderCredential(input) {
340
340
  notes: "Ouro provider credentials. The vault item password is a versioned JSON payload.",
341
341
  });
342
342
  input.onProgress?.(`refreshing local provider snapshot from ${input.agentName}'s vault...`);
343
- const refreshResult = await refreshProviderCredentialPool(input.agentName);
343
+ const refreshResult = await refreshProviderCredentialPool(input.agentName, {
344
+ onProgress: input.onProgress,
345
+ });
344
346
  if (!refreshResult.ok) {
345
347
  throw new Error(`credential stored in vault, but the local provider snapshot could not be refreshed: ${refreshResult.error}. ` +
346
348
  `Run 'ouro provider refresh --agent ${input.agentName}' after fixing vault access, then run 'ouro auth verify --agent ${input.agentName}'.`);
@@ -451,8 +451,8 @@ function senseRuntimeGuidance(channel, preReadStatusLines) {
451
451
  lines.push("- running = enabled and currently active");
452
452
  lines.push("- error = enabled but unhealthy");
453
453
  lines.push("If asked how to enable another sense, I explain the relevant agent.json senses entry and required agent-vault runtime/config fields instead of guessing.");
454
- lines.push("teams setup truth: enable `senses.teams.enabled`, then store `teams.clientId`, `teams.clientSecret`, and `teams.tenantId` in the agent vault runtime/config item.");
455
- lines.push("bluebubbles setup truth: run `ouro connect bluebubbles --agent <agent>`; it stores this machine's BlueBubbles URL/password/listener config in the agent vault machine runtime item.");
454
+ lines.push("teams setup truth: run `ouro connect teams --agent <agent>` from the connect bay; it stores Teams runtime/config fields and enables `senses.teams.enabled`.");
455
+ lines.push("bluebubbles setup truth: run `ouro connect bluebubbles --agent <agent>` from the connect bay; it stores this machine's BlueBubbles URL/password/listener config in the agent vault machine runtime item.");
456
456
  if (channel === "cli") {
457
457
  lines.push("cli is interactive: it is available when the user opens it, not something `ouro up` daemonizes.");
458
458
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.421",
3
+ "version": "0.1.0-alpha.423",
4
4
  "main": "dist/heart/daemon/ouro-entry.js",
5
5
  "bin": {
6
6
  "cli": "dist/heart/daemon/ouro-bot-entry.js",