@ouro.bot/cli 0.1.0-alpha.422 → 0.1.0-alpha.425

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.
@@ -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");
@@ -157,6 +158,132 @@ async function listCliAgents(deps) {
157
158
  }
158
159
  return [];
159
160
  }
161
+ function runtimeContextAgentName(deps) {
162
+ try {
163
+ return deps.whoamiInfo?.().agentName.trim() || undefined;
164
+ }
165
+ catch {
166
+ return undefined;
167
+ }
168
+ }
169
+ function normalizeCliAgentNames(agentNames) {
170
+ return [...new Set(agentNames.map((agent) => agent.trim()).filter((agent) => agent.length > 0))];
171
+ }
172
+ function resolveSelectedCliAgent(answer, agentNames) {
173
+ const trimmed = answer.trim();
174
+ if (!trimmed)
175
+ return undefined;
176
+ const byName = agentNames.find((agent) => agent.toLowerCase() === trimmed.toLowerCase());
177
+ if (byName)
178
+ return byName;
179
+ const parsedIndex = Number.parseInt(trimmed, 10);
180
+ if (Number.isNaN(parsedIndex))
181
+ return undefined;
182
+ return agentNames[parsedIndex - 1];
183
+ }
184
+ function noAgentsFoundMessage() {
185
+ return "no agents found. Run `ouro` to hatch one or `ouro clone <remote>` to add an existing agent.";
186
+ }
187
+ function multipleAgentsFoundMessage(agentNames) {
188
+ return [
189
+ `multiple agents found: ${agentNames.join(", ")}`,
190
+ "Re-run with --agent <name>.",
191
+ ].join("\n");
192
+ }
193
+ function invalidAgentSelectionMessage(agentNames) {
194
+ return [
195
+ `invalid agent selection. Available agents: ${agentNames.join(", ")}`,
196
+ "Re-run with --agent <name>.",
197
+ ].join("\n");
198
+ }
199
+ function agentResolutionFailureMode(command) {
200
+ switch (command.kind) {
201
+ case "task.board":
202
+ case "task.create":
203
+ case "task.update":
204
+ case "task.show":
205
+ case "task.actionable":
206
+ case "task.deps":
207
+ case "task.sessions":
208
+ case "task.fix":
209
+ case "reminder.create":
210
+ case "friend.list":
211
+ case "friend.show":
212
+ case "friend.create":
213
+ case "friend.update":
214
+ case "habit.list":
215
+ case "habit.create":
216
+ case "thoughts":
217
+ case "attention.list":
218
+ case "attention.show":
219
+ case "attention.history":
220
+ case "inner.status":
221
+ case "session.list":
222
+ return "return-message";
223
+ case "provider.use":
224
+ case "provider.check":
225
+ case "provider.status":
226
+ case "provider.refresh":
227
+ case "vault.create":
228
+ case "vault.replace":
229
+ case "vault.recover":
230
+ case "vault.unlock":
231
+ case "vault.status":
232
+ case "vault.config.set":
233
+ case "vault.config.status":
234
+ case "connect":
235
+ case "auth.run":
236
+ case "auth.verify":
237
+ case "auth.switch":
238
+ case "config.model":
239
+ case "config.models":
240
+ case "setup":
241
+ case "bluebubbles.replay":
242
+ return "throw";
243
+ default:
244
+ return undefined;
245
+ }
246
+ }
247
+ async function resolveMissingAgentName(deps, failureMode) {
248
+ const runtimeAgent = runtimeContextAgentName(deps);
249
+ if (runtimeAgent) {
250
+ return { ok: true, agent: runtimeAgent };
251
+ }
252
+ const discoveredAgents = normalizeCliAgentNames(await listCliAgents(deps));
253
+ if (discoveredAgents.length === 0) {
254
+ return { ok: false, message: noAgentsFoundMessage(), failureMode };
255
+ }
256
+ if (discoveredAgents.length === 1) {
257
+ return { ok: true, agent: discoveredAgents[0] };
258
+ }
259
+ if (!deps.promptInput) {
260
+ return { ok: false, message: multipleAgentsFoundMessage(discoveredAgents), failureMode };
261
+ }
262
+ const answer = await deps.promptInput([
263
+ "Which agent should this use?",
264
+ ...discoveredAgents.map((agent, index) => `${index + 1}. ${agent}`),
265
+ `Choose [1-${discoveredAgents.length}] or type a name: `,
266
+ ].join("\n"));
267
+ const selectedAgent = resolveSelectedCliAgent(answer, discoveredAgents);
268
+ if (!selectedAgent) {
269
+ return { ok: false, message: invalidAgentSelectionMessage(discoveredAgents), failureMode };
270
+ }
271
+ return { ok: true, agent: selectedAgent };
272
+ }
273
+ async function resolveCommandAgent(command, deps) {
274
+ const failureMode = agentResolutionFailureMode(command);
275
+ if (!failureMode) {
276
+ return { ok: true, command: command };
277
+ }
278
+ const explicitAgent = "agent" in command && typeof command.agent === "string" ? command.agent.trim() : "";
279
+ if (explicitAgent) {
280
+ return { ok: true, command: { ...command, agent: explicitAgent } };
281
+ }
282
+ const resolvedAgent = await resolveMissingAgentName(deps, failureMode);
283
+ if (!resolvedAgent.ok)
284
+ return resolvedAgent;
285
+ return { ok: true, command: { ...command, agent: resolvedAgent.agent } };
286
+ }
160
287
  function managedAgentsSignature(agentNames) {
161
288
  const unique = [...new Set(agentNames.map((agent) => agent.trim()).filter((agent) => agent.length > 0))].sort();
162
289
  return unique.length > 0 ? unique.join(",") : "(none)";
@@ -197,7 +324,7 @@ async function runCommandProgressPhase(progress, label, run, detail) {
197
324
  return result;
198
325
  }
199
326
  catch (error) {
200
- progress.completePhase(label, "failed");
327
+ progress.failPhase(label, "failed");
201
328
  throw error;
202
329
  }
203
330
  }
@@ -1227,6 +1354,50 @@ function currentMachineId(deps) {
1227
1354
  now: () => providerCliNow(deps),
1228
1355
  }).machineId;
1229
1356
  }
1357
+ const DEFAULT_RUNTIME_APPLY_TIMEOUT_MS = 15_000;
1358
+ const DEFAULT_RUNTIME_APPLY_POLL_INTERVAL_MS = 500;
1359
+ const DEFAULT_DAEMON_STATUS_TIMEOUT_MS = 4_000;
1360
+ const DEFAULT_AGENT_RESTART_TIMEOUT_MS = 8_000;
1361
+ const CONNECT_PROVIDER_CHOICES = ["openai-codex", "anthropic", "minimax", "azure", "github-copilot"];
1362
+ function hasRuntimeConfigValue(config, key) {
1363
+ const segments = key.split(".");
1364
+ let cursor = config;
1365
+ for (const segment of segments) {
1366
+ if (!cursor || typeof cursor !== "object" || Array.isArray(cursor))
1367
+ return false;
1368
+ cursor = cursor[segment];
1369
+ }
1370
+ return typeof cursor === "string" && cursor.trim().length > 0;
1371
+ }
1372
+ function runtimeConfigReadStatus(runtime) {
1373
+ if (runtime.reason === "missing")
1374
+ return "missing";
1375
+ if (/locked/i.test(runtime.error))
1376
+ return "locked";
1377
+ return "needs attention";
1378
+ }
1379
+ function cliNowMs(deps) {
1380
+ return (deps.now ?? Date.now)();
1381
+ }
1382
+ function cliSleep(deps, ms) {
1383
+ if (deps.sleep)
1384
+ return deps.sleep(ms);
1385
+ return new Promise((resolve) => setTimeout(resolve, ms));
1386
+ }
1387
+ async function withCliTimeout(timeoutMs, label, run) {
1388
+ let timer;
1389
+ try {
1390
+ return await Promise.race([
1391
+ run(),
1392
+ new Promise((_, reject) => {
1393
+ timer = setTimeout(() => reject(new Error(label)), timeoutMs);
1394
+ }),
1395
+ ]);
1396
+ }
1397
+ finally {
1398
+ clearTimeout(timer);
1399
+ }
1400
+ }
1230
1401
  async function promptRuntimeConfigValue(command, deps) {
1231
1402
  if (command.value !== undefined)
1232
1403
  return command.value;
@@ -1242,7 +1413,7 @@ async function promptRuntimeConfigValue(command, deps) {
1242
1413
  function runtimeScopeLabel(scope) {
1243
1414
  return scope === "machine" ? "this machine's vault runtime config item" : "the agent vault runtime/config item";
1244
1415
  }
1245
- async function storeRuntimeConfigKey(input) {
1416
+ async function storeRuntimeConfigKeys(input) {
1246
1417
  const machineId = input.scope === "machine" ? currentMachineId(input.deps) : undefined;
1247
1418
  input.onProgress?.("checking existing runtime config");
1248
1419
  const current = input.scope === "machine"
@@ -1251,23 +1422,77 @@ async function storeRuntimeConfigKey(input) {
1251
1422
  if (!current.ok && current.reason !== "missing") {
1252
1423
  throw new Error(`cannot read existing runtime credentials from ${current.itemPath}: ${current.error}`);
1253
1424
  }
1254
- input.onProgress?.(`storing ${input.key} in ${current.itemPath}`);
1255
- const nextConfig = setRuntimeConfigValue(current.ok ? current.config : {}, input.key, input.value);
1425
+ let nextConfig = current.ok ? current.config : {};
1426
+ for (const entry of input.entries) {
1427
+ input.onProgress?.(`storing ${entry.key} in ${current.itemPath}`);
1428
+ nextConfig = setRuntimeConfigValue(nextConfig, entry.key, entry.value);
1429
+ }
1256
1430
  const stored = input.scope === "machine"
1257
1431
  ? await (0, runtime_credentials_1.upsertMachineRuntimeCredentialConfig)(input.agent, machineId, nextConfig, providerCliNow(input.deps))
1258
1432
  : await (0, runtime_credentials_1.upsertRuntimeCredentialConfig)(input.agent, nextConfig, providerCliNow(input.deps));
1259
- input.onProgress?.(`stored ${input.key}; credential value was not printed`);
1433
+ input.onProgress?.(`stored ${input.entries.map((entry) => entry.key).join(", ")}; credential values were not printed`);
1260
1434
  return { revision: stored.revision, itemPath: stored.itemPath, ...(machineId ? { machineId } : {}) };
1261
1435
  }
1262
- async function reloadRunningAgentAfterCredentialChange(agent, deps) {
1436
+ async function storeRuntimeConfigKey(input) {
1437
+ return storeRuntimeConfigKeys({
1438
+ agent: input.agent,
1439
+ entries: [{ key: input.key, value: input.value }],
1440
+ scope: input.scope,
1441
+ deps: input.deps,
1442
+ onProgress: input.onProgress,
1443
+ });
1444
+ }
1445
+ function readRuntimeApplyWorker(payload, agent) {
1446
+ return payload.workers.find((worker) => worker.agent === agent) ?? null;
1447
+ }
1448
+ async function applyRuntimeChangeToRunningAgent(agent, deps, onProgress) {
1263
1449
  try {
1450
+ onProgress?.("checking daemon socket");
1264
1451
  const alive = await deps.checkSocketAlive(deps.socketPath);
1265
1452
  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"}`;
1453
+ return "daemon is not running; next `ouro up` will load the change";
1454
+ onProgress?.("requesting restart from daemon");
1455
+ const response = await withCliTimeout(DEFAULT_AGENT_RESTART_TIMEOUT_MS, "daemon restart request timed out", () => deps.sendCommand(deps.socketPath, { kind: "agent.restart", agent }));
1456
+ if (!response.ok)
1457
+ return `daemon restart skipped: ${response.error ?? response.message ?? "unknown daemon error"}`;
1458
+ const deadline = cliNowMs(deps) + DEFAULT_RUNTIME_APPLY_TIMEOUT_MS;
1459
+ onProgress?.(`waiting for ${agent} to report running state`);
1460
+ while (cliNowMs(deps) < deadline) {
1461
+ try {
1462
+ const statusResponse = await withCliTimeout(DEFAULT_DAEMON_STATUS_TIMEOUT_MS, "daemon status timed out", () => deps.sendCommand(deps.socketPath, { kind: "daemon.status" }));
1463
+ if (statusResponse.ok) {
1464
+ const payload = (0, cli_render_1.parseStatusPayload)(statusResponse.data);
1465
+ if (!payload) {
1466
+ onProgress?.("daemon status did not include structured worker state");
1467
+ return "restart requested; daemon status is unavailable, so verify with `ouro status` if needed";
1468
+ }
1469
+ const worker = readRuntimeApplyWorker(payload, agent);
1470
+ if (!worker) {
1471
+ onProgress?.(`still waiting: ${agent} is not listed by daemon`);
1472
+ }
1473
+ else if (worker.status === "running") {
1474
+ onProgress?.(`daemon reports ${agent}/${worker.worker} running`);
1475
+ return `restarted ${agent} and the daemon reports it running`;
1476
+ }
1477
+ else if (worker.status === "crashed") {
1478
+ return `restart requested, but ${agent}/${worker.worker} crashed before reporting running${worker.errorReason ? `: ${worker.errorReason}` : worker.fixHint ? `: ${worker.fixHint}` : ""}`;
1479
+ }
1480
+ else {
1481
+ onProgress?.(`still waiting: ${agent}/${worker.worker} is ${worker.status}`);
1482
+ }
1483
+ }
1484
+ else {
1485
+ onProgress?.(`still waiting: daemon status returned ${statusResponse.error ?? statusResponse.message ?? "unknown error"}`);
1486
+ }
1487
+ }
1488
+ catch (error) {
1489
+ onProgress?.(`still waiting: ${error instanceof Error ? error.message : String(error)}`);
1490
+ }
1491
+ if (cliNowMs(deps) >= deadline - DEFAULT_RUNTIME_APPLY_POLL_INTERVAL_MS)
1492
+ break;
1493
+ await cliSleep(deps, DEFAULT_RUNTIME_APPLY_POLL_INTERVAL_MS);
1494
+ }
1495
+ 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
1496
  }
1272
1497
  catch (error) {
1273
1498
  return `daemon restart skipped: ${error instanceof Error ? error.message : String(error)}`;
@@ -1401,23 +1626,70 @@ function enableAgentSense(agent, sense, deps) {
1401
1626
  };
1402
1627
  fs.writeFileSync(configPath, `${JSON.stringify(raw, null, 2)}\n`, "utf-8");
1403
1628
  }
1404
- function connectMenu(agent) {
1629
+ async function buildConnectMenu(agent, deps) {
1630
+ const providerVisibility = (0, provider_visibility_1.buildAgentProviderVisibility)({
1631
+ agentName: agent,
1632
+ agentRoot: providerCliAgentRoot({ agent }, deps),
1633
+ homeDir: providerCliHomeDir(deps),
1634
+ });
1635
+ const providerStatus = providerVisibility.lanes.some((lane) => lane.status === "unconfigured")
1636
+ ? "needs setup"
1637
+ : providerVisibility.lanes.some((lane) => lane.status === "configured" && lane.credential.status !== "present")
1638
+ ? "needs auth"
1639
+ : providerVisibility.lanes.some((lane) => lane.status === "configured" && (lane.readiness.status === "failed" || lane.readiness.status === "stale"))
1640
+ ? "needs attention"
1641
+ : "ready";
1642
+ const providerDetail = providerVisibility.lanes.map((lane) => lane.status === "configured"
1643
+ ? `${lane.lane}: ${lane.provider} / ${lane.model}`
1644
+ : `${lane.lane}: choose provider/model`).join(" | ");
1645
+ const runtimeConfig = await (0, runtime_credentials_1.refreshRuntimeCredentialConfig)(agent, { preserveCachedOnFailure: true });
1646
+ const machineRuntime = await (0, runtime_credentials_1.refreshMachineRuntimeCredentialConfig)(agent, currentMachineId(deps), { preserveCachedOnFailure: true });
1647
+ const agentConfig = (0, auth_flow_1.readAgentConfigForAgent)(agent, deps.bundlesRoot).config;
1648
+ const teamsEnabled = agentConfig.senses?.teams?.enabled === true;
1649
+ const blueBubblesEnabled = agentConfig.senses?.bluebubbles?.enabled === true;
1650
+ const perplexityStatus = runtimeConfig.ok
1651
+ ? hasRuntimeConfigValue(runtimeConfig.config, "integrations.perplexityApiKey") ? "ready" : "missing"
1652
+ : runtimeConfigReadStatus(runtimeConfig);
1653
+ const embeddingsStatus = runtimeConfig.ok
1654
+ ? hasRuntimeConfigValue(runtimeConfig.config, "integrations.openaiEmbeddingsApiKey") ? "ready" : "missing"
1655
+ : runtimeConfigReadStatus(runtimeConfig);
1656
+ const teamsStatus = runtimeConfig.ok
1657
+ ? hasRuntimeConfigValue(runtimeConfig.config, "teams.clientId")
1658
+ && hasRuntimeConfigValue(runtimeConfig.config, "teams.clientSecret")
1659
+ && hasRuntimeConfigValue(runtimeConfig.config, "teams.tenantId")
1660
+ && teamsEnabled
1661
+ ? "ready"
1662
+ : "missing"
1663
+ : runtimeConfigReadStatus(runtimeConfig);
1664
+ const blueBubblesStatus = machineRuntime.ok
1665
+ ? hasRuntimeConfigValue(machineRuntime.config, "bluebubbles.serverUrl")
1666
+ && hasRuntimeConfigValue(machineRuntime.config, "bluebubbles.password")
1667
+ && blueBubblesEnabled
1668
+ ? "attached"
1669
+ : "not attached"
1670
+ : machineRuntimeReadStatus(machineRuntime);
1405
1671
  return [
1406
- `Connect ${agent}`,
1407
- "Pick what this agent should be able to use.",
1672
+ `${agent} // connect bay`,
1673
+ "Bring one capability online at a time.",
1674
+ "",
1675
+ ` 1. [${providerStatus}] Providers`,
1676
+ ` ${providerDetail}`,
1408
1677
  "",
1409
- " 1. Perplexity search",
1410
- " Portable. Stores an API key in the agent vault.",
1678
+ ` 2. [${perplexityStatus}] Perplexity search`,
1679
+ " Portable. Web search via Perplexity.",
1411
1680
  "",
1412
- " 2. BlueBubbles iMessage",
1413
- " This machine only. Connects a local Mac Messages bridge.",
1681
+ ` 3. [${embeddingsStatus}] Memory embeddings`,
1682
+ " Portable. Memory retrieval and note search.",
1414
1683
  "",
1415
- " 3. Provider auth",
1416
- ` Model credentials: ouro auth --agent ${agent} --provider <provider>`,
1684
+ ` 4. [${teamsStatus}] Teams`,
1685
+ " Portable. Microsoft Teams sense credentials.",
1417
1686
  "",
1418
- " 4. Cancel",
1687
+ ` 5. [${blueBubblesStatus}] BlueBubbles iMessage`,
1688
+ " This machine only. Local Mac Messages bridge.",
1419
1689
  "",
1420
- "Choose [1-4]: ",
1690
+ " 6. Cancel",
1691
+ "",
1692
+ "Choose [1-6] or type a name: ",
1421
1693
  ].join("\n");
1422
1694
  }
1423
1695
  async function executeConnectPerplexity(agent, deps) {
@@ -1437,30 +1709,24 @@ async function executeConnectPerplexity(agent, deps) {
1437
1709
  let stored;
1438
1710
  let reload;
1439
1711
  try {
1440
- progress.startPhase("saving Perplexity search");
1441
- stored = await storeRuntimeConfigKey({
1712
+ stored = await runCommandProgressPhase(progress, "saving Perplexity search", () => storeRuntimeConfigKey({
1442
1713
  agent,
1443
1714
  key: "integrations.perplexityApiKey",
1444
1715
  value: key,
1445
1716
  scope: "agent",
1446
1717
  deps,
1447
1718
  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();
1719
+ }), () => "secret stored");
1720
+ reload = await runCommandProgressPhase(progress, `applying change to running ${agent}`, () => applyRuntimeChangeToRunningAgent(agent, deps, (message) => progress.updateDetail(message)), (result) => result);
1454
1721
  }
1455
- catch (error) {
1722
+ finally {
1456
1723
  progress.end();
1457
- throw error;
1458
1724
  }
1459
1725
  const message = [
1460
1726
  `Perplexity connected for ${agent}`,
1461
1727
  "capability: Perplexity search",
1462
1728
  `stored: ${stored.itemPath}`,
1463
- `reload: ${reload}`,
1729
+ `running agent: ${reload}`,
1464
1730
  "secret was not printed",
1465
1731
  "",
1466
1732
  "Next: ask the agent to search.",
@@ -1468,6 +1734,102 @@ async function executeConnectPerplexity(agent, deps) {
1468
1734
  deps.writeStdout(message);
1469
1735
  return message;
1470
1736
  }
1737
+ async function executeConnectEmbeddings(agent, deps) {
1738
+ if (agent === "SerpentGuide") {
1739
+ throw new Error("SerpentGuide has no persistent runtime credentials. Connect embeddings on the hatchling agent instead.");
1740
+ }
1741
+ const promptSecret = requirePromptSecret(deps, "OpenAI embeddings API key entry");
1742
+ deps.writeStdout([
1743
+ `Connect embeddings for ${agent}`,
1744
+ "The API key stays hidden while you type.",
1745
+ `Ouro stores it in ${agent}'s vault runtime/config item.`,
1746
+ ].join("\n"));
1747
+ const key = (await promptSecret("OpenAI embeddings API key: ")).trim();
1748
+ if (!key)
1749
+ throw new Error("OpenAI embeddings API key cannot be blank");
1750
+ const progress = createHumanCommandProgress(deps, "connect embeddings");
1751
+ let stored;
1752
+ let reload;
1753
+ try {
1754
+ stored = await runCommandProgressPhase(progress, "saving memory embeddings", () => storeRuntimeConfigKey({
1755
+ agent,
1756
+ key: "integrations.openaiEmbeddingsApiKey",
1757
+ value: key,
1758
+ scope: "agent",
1759
+ deps,
1760
+ onProgress: (message) => progress.updateDetail(message),
1761
+ }), () => "secret stored");
1762
+ reload = await runCommandProgressPhase(progress, `applying change to running ${agent}`, () => applyRuntimeChangeToRunningAgent(agent, deps, (message) => progress.updateDetail(message)), (result) => result);
1763
+ }
1764
+ finally {
1765
+ progress.end();
1766
+ }
1767
+ const message = [
1768
+ `Embeddings connected for ${agent}`,
1769
+ "capability: memory embeddings",
1770
+ `stored: ${stored.itemPath}`,
1771
+ `running agent: ${reload}`,
1772
+ "secret was not printed",
1773
+ "",
1774
+ "Next: ask the agent to search notes or memory.",
1775
+ ].join("\n");
1776
+ deps.writeStdout(message);
1777
+ return message;
1778
+ }
1779
+ async function executeConnectTeams(agent, deps) {
1780
+ if (agent === "SerpentGuide") {
1781
+ throw new Error("SerpentGuide has no persistent runtime credentials. Connect Teams on the hatchling agent instead.");
1782
+ }
1783
+ const promptInput = requirePromptInput(deps, "Teams setup");
1784
+ const promptSecret = requirePromptSecret(deps, "Teams client secret entry");
1785
+ deps.writeStdout([
1786
+ `Connect Teams for ${agent}`,
1787
+ "This connects the Teams sense.",
1788
+ "The client secret stays hidden while you type.",
1789
+ ].join("\n"));
1790
+ const clientId = (await promptInput("Teams client ID: ")).trim();
1791
+ if (!clientId)
1792
+ throw new Error("Teams client ID cannot be blank");
1793
+ const clientSecret = (await promptSecret("Teams client secret: ")).trim();
1794
+ if (!clientSecret)
1795
+ throw new Error("Teams client secret cannot be blank");
1796
+ const tenantId = (await promptInput("Teams tenant ID: ")).trim();
1797
+ if (!tenantId)
1798
+ throw new Error("Teams tenant ID cannot be blank");
1799
+ const managedIdentityClientId = (await promptInput("Teams managed identity client ID [blank to skip]: ")).trim();
1800
+ const progress = createHumanCommandProgress(deps, "connect teams");
1801
+ let stored;
1802
+ try {
1803
+ stored = await runCommandProgressPhase(progress, "saving Teams setup", () => storeRuntimeConfigKeys({
1804
+ agent,
1805
+ entries: [
1806
+ { key: "teams.clientId", value: clientId },
1807
+ { key: "teams.clientSecret", value: clientSecret },
1808
+ { key: "teams.tenantId", value: tenantId },
1809
+ ...(managedIdentityClientId ? [{ key: "teams.managedIdentityClientId", value: managedIdentityClientId }] : []),
1810
+ ],
1811
+ scope: "agent",
1812
+ deps,
1813
+ onProgress: (message) => progress.updateDetail(message),
1814
+ }), () => "secret stored");
1815
+ progress.updateDetail("enabling Teams in agent.json");
1816
+ enableAgentSense(agent, "teams", deps);
1817
+ }
1818
+ finally {
1819
+ progress.end();
1820
+ }
1821
+ const message = appendBundleSyncSummary([
1822
+ `Teams connected for ${agent}`,
1823
+ "capability: Teams sense",
1824
+ `stored: ${stored.itemPath}`,
1825
+ "agent.json: senses.teams.enabled = true",
1826
+ "secret was not printed",
1827
+ "",
1828
+ "Next: run `ouro up` so the daemon picks up the Teams sense change.",
1829
+ ].join("\n"), agent, deps);
1830
+ deps.writeStdout(message);
1831
+ return message;
1832
+ }
1471
1833
  async function executeConnectBlueBubbles(agent, deps) {
1472
1834
  if (agent === "SerpentGuide") {
1473
1835
  throw new Error("SerpentGuide has no persistent runtime credentials. Attach BlueBubbles on the hatchling agent instead.");
@@ -1535,39 +1897,86 @@ async function executeConnectBlueBubbles(agent, deps) {
1535
1897
  deps.writeStdout(message);
1536
1898
  return message;
1537
1899
  }
1900
+ async function executeConnectProviders(agent, deps) {
1901
+ const promptInput = deps.promptInput;
1902
+ if (!promptInput) {
1903
+ const message = [
1904
+ `Provider auth for ${agent}`,
1905
+ "Run one of:",
1906
+ ...CONNECT_PROVIDER_CHOICES.map((provider) => ` ouro auth --agent ${agent} --provider ${provider}`),
1907
+ ].join("\n");
1908
+ deps.writeStdout(message);
1909
+ return message;
1910
+ }
1911
+ const choice = (await promptInput([
1912
+ `Which provider should ${agent} use credentials for?`,
1913
+ ...CONNECT_PROVIDER_CHOICES.map((provider) => ` - ${provider}`),
1914
+ "",
1915
+ "Provider: ",
1916
+ ].join("\n"))).trim().toLowerCase();
1917
+ if (!(0, cli_parse_2.isAgentProvider)(choice)) {
1918
+ throw new Error(`Unknown provider '${choice}'. Use ${CONNECT_PROVIDER_CHOICES.join("|")}.`);
1919
+ }
1920
+ return executeAuthRun({ kind: "auth.run", agent, provider: choice }, deps);
1921
+ }
1922
+ function machineRuntimeReadStatus(runtime) {
1923
+ if (runtime.reason === "missing")
1924
+ return "not attached";
1925
+ if (/locked/i.test(runtime.error))
1926
+ return "locked";
1927
+ return "needs attention";
1928
+ }
1929
+ function connectMenuTarget(answer) {
1930
+ const normalized = answer.trim().toLowerCase();
1931
+ if (normalized === "1" || normalized === "providers" || normalized === "provider" || normalized === "auth")
1932
+ return "providers";
1933
+ if (normalized === "2" || normalized === "perplexity" || normalized === "perplexity-search" || normalized === "search")
1934
+ return "perplexity";
1935
+ if (normalized === "3" || normalized === "embeddings" || normalized === "embedding" || normalized === "memory" || normalized === "note-search" || normalized === "notes")
1936
+ return "embeddings";
1937
+ if (normalized === "4" || normalized === "teams" || normalized === "msteams" || normalized === "microsoft-teams")
1938
+ return "teams";
1939
+ if (normalized === "5" || normalized === "bluebubbles" || normalized === "imessage" || normalized === "messages")
1940
+ return "bluebubbles";
1941
+ return "cancel";
1942
+ }
1538
1943
  async function executeConnect(command, deps) {
1944
+ if (command.target === "providers")
1945
+ return executeConnectProviders(command.agent, deps);
1539
1946
  if (command.target === "perplexity")
1540
1947
  return executeConnectPerplexity(command.agent, deps);
1948
+ if (command.target === "embeddings")
1949
+ return executeConnectEmbeddings(command.agent, deps);
1950
+ if (command.target === "teams")
1951
+ return executeConnectTeams(command.agent, deps);
1541
1952
  if (command.target === "bluebubbles")
1542
1953
  return executeConnectBlueBubbles(command.agent, deps);
1954
+ const menu = await buildConnectMenu(command.agent, deps);
1543
1955
  const promptInput = deps.promptInput;
1544
1956
  if (!promptInput) {
1545
1957
  const message = [
1546
- connectMenu(command.agent).replace(/\nChoose \[1-4\]: $/, ""),
1958
+ menu.replace(/\nChoose \[1-6\] or type a name: $/, ""),
1547
1959
  "",
1960
+ `Run: ouro connect providers --agent ${command.agent}`,
1548
1961
  `Run: ouro connect perplexity --agent ${command.agent}`,
1549
- `Or: ouro connect bluebubbles --agent ${command.agent}`,
1962
+ `Run: ouro connect embeddings --agent ${command.agent}`,
1963
+ `Run: ouro connect teams --agent ${command.agent}`,
1964
+ `Run: ouro connect bluebubbles --agent ${command.agent}`,
1550
1965
  ].join("\n");
1551
1966
  deps.writeStdout(message);
1552
1967
  return message;
1553
1968
  }
1554
- const answer = (await promptInput(connectMenu(command.agent))).trim().toLowerCase();
1555
- if (answer === "1" || answer === "perplexity" || answer === "perplexity-search") {
1969
+ const answer = connectMenuTarget(await promptInput(menu));
1970
+ if (answer === "providers")
1971
+ return executeConnectProviders(command.agent, deps);
1972
+ if (answer === "perplexity")
1556
1973
  return executeConnectPerplexity(command.agent, deps);
1557
- }
1558
- if (answer === "2" || answer === "bluebubbles" || answer === "imessage" || answer === "messages") {
1974
+ if (answer === "embeddings")
1975
+ return executeConnectEmbeddings(command.agent, deps);
1976
+ if (answer === "teams")
1977
+ return executeConnectTeams(command.agent, deps);
1978
+ if (answer === "bluebubbles")
1559
1979
  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
1980
  const message = "connect cancelled.";
1572
1981
  deps.writeStdout(message);
1573
1982
  return message;
@@ -1896,13 +2305,9 @@ async function executeProviderRefresh(command, deps) {
1896
2305
  deps.writeStdout(message);
1897
2306
  return message;
1898
2307
  }
1899
- progress.startPhase(`reloading ${command.agent}`);
1900
- const reload = await reloadRunningAgentAfterCredentialChange(command.agent, deps);
1901
- progress.completePhase(`reloading ${command.agent}`, reload);
2308
+ const reload = await runCommandProgressPhase(progress, `applying change to running ${command.agent}`, () => applyRuntimeChangeToRunningAgent(command.agent, deps, (message) => progress.updateDetail(message)), (result) => result);
1902
2309
  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);
2310
+ lines.push(`running agent: ${reload}`);
1906
2311
  const message = lines.join("\n");
1907
2312
  deps.writeStdout(message);
1908
2313
  return message;
@@ -2608,9 +3013,9 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
2608
3013
  deps.writeStdout(text);
2609
3014
  return text;
2610
3015
  }
2611
- let command;
3016
+ let parsedCommand;
2612
3017
  try {
2613
- command = (0, cli_parse_1.parseOuroCommand)(args);
3018
+ parsedCommand = (0, cli_parse_1.parseOuroCommand)(args);
2614
3019
  }
2615
3020
  catch (parseError) {
2616
3021
  if (deps.startChat && deps.listDiscoveredAgents && args.length === 1) {
@@ -2623,6 +3028,15 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
2623
3028
  }
2624
3029
  throw parseError;
2625
3030
  }
3031
+ const resolvedCommand = await resolveCommandAgent(parsedCommand, deps);
3032
+ if (!resolvedCommand.ok) {
3033
+ if (resolvedCommand.failureMode === "return-message") {
3034
+ deps.writeStdout(resolvedCommand.message);
3035
+ return resolvedCommand.message;
3036
+ }
3037
+ throw new Error(resolvedCommand.message);
3038
+ }
3039
+ let command = resolvedCommand.command;
2626
3040
  if (args.length === 0) {
2627
3041
  const discovered = await Promise.resolve(deps.listDiscoveredAgents ? deps.listDiscoveredAgents() : (0, cli_defaults_1.defaultListDiscoveredAgents)());
2628
3042
  if (discovered.length === 0 && deps.runSerpentGuide) {
@@ -3502,7 +3916,7 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
3502
3916
  command.kind === "task.show" || command.kind === "task.actionable" || command.kind === "task.deps" ||
3503
3917
  command.kind === "task.sessions" || command.kind === "task.fix") {
3504
3918
  /* v8 ignore start -- production default: requires full identity setup @preserve */
3505
- const taskMod = deps.taskModule ?? (0, tasks_1.getTaskModule)();
3919
+ const taskMod = deps.taskModule ?? (0, tasks_1.createTaskModule)(path.join(deps.bundlesRoot ?? (0, identity_1.getAgentBundlesRoot)(), `${command.agent}.ouro`, "tasks"));
3506
3920
  /* v8 ignore stop */
3507
3921
  const message = executeTaskCommand(command, taskMod);
3508
3922
  deps.writeStdout(message);
@@ -3511,7 +3925,7 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
3511
3925
  // ── reminder subcommands (local, no daemon socket needed) ──
3512
3926
  if (command.kind === "reminder.create") {
3513
3927
  /* v8 ignore start -- production default: requires full identity setup @preserve */
3514
- const taskMod = deps.taskModule ?? (0, tasks_1.getTaskModule)();
3928
+ const taskMod = deps.taskModule ?? (0, tasks_1.createTaskModule)(path.join(deps.bundlesRoot ?? (0, identity_1.getAgentBundlesRoot)(), `${command.agent}.ouro`, "tasks"));
3515
3929
  /* v8 ignore stop */
3516
3930
  const message = executeReminderCommand(command, taskMod);
3517
3931
  deps.writeStdout(message);
@@ -3521,8 +3935,7 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
3521
3935
  if (command.kind === "habit.list" || command.kind === "habit.create") {
3522
3936
  const { parseHabitFile, renderHabitFile } = await Promise.resolve().then(() => __importStar(require("../habits/habit-parser")));
3523
3937
  /* v8 ignore start -- production default: uses real bundle root @preserve */
3524
- const agentName = command.agent ?? (0, identity_1.getAgentName)();
3525
- const bundleRoot = deps.agentBundleRoot ?? path.join((0, identity_1.getAgentBundlesRoot)(), `${agentName}.ouro`);
3938
+ const bundleRoot = deps.agentBundleRoot ?? path.join(deps.bundlesRoot ?? (0, identity_1.getAgentBundlesRoot)(), `${command.agent}.ouro`);
3526
3939
  /* v8 ignore stop */
3527
3940
  const habitsDir = path.join(bundleRoot, "habits");
3528
3941
  if (command.kind === "habit.list") {
@@ -3581,8 +3994,8 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
3581
3994
  // Derive agent-scoped friends dir from --agent flag or link/unlink's agent field
3582
3995
  const agentName = ("agent" in command && command.agent) ? command.agent : undefined;
3583
3996
  const friendsDir = agentName
3584
- ? path.join((0, identity_1.getAgentBundlesRoot)(), `${agentName}.ouro`, "friends")
3585
- : path.join((0, identity_1.getAgentBundlesRoot)(), "friends");
3997
+ ? path.join(deps.bundlesRoot ?? (0, identity_1.getAgentBundlesRoot)(), `${agentName}.ouro`, "friends")
3998
+ : path.join(deps.bundlesRoot ?? (0, identity_1.getAgentBundlesRoot)(), "friends");
3586
3999
  store = new store_file_1.FileFriendStore(friendsDir);
3587
4000
  }
3588
4001
  /* v8 ignore stop */
@@ -3765,29 +4178,41 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
3765
4178
  deps.writeStdout(message);
3766
4179
  return message;
3767
4180
  }
3768
- /* v8 ignore start -- production default: requires full identity setup @preserve */
3769
4181
  try {
3770
- const info = deps.whoamiInfo
3771
- ? deps.whoamiInfo()
3772
- : {
3773
- agentName: (0, identity_1.getAgentName)(),
3774
- homePath: path.join((0, identity_1.getAgentBundlesRoot)(), `${(0, identity_1.getAgentName)()}.ouro`),
3775
- bonesVersion: (0, runtime_metadata_1.getRuntimeMetadata)().version,
3776
- };
4182
+ if (deps.whoamiInfo) {
4183
+ try {
4184
+ const info = deps.whoamiInfo();
4185
+ const message = [
4186
+ `agent: ${info.agentName}`,
4187
+ `home: ${info.homePath}`,
4188
+ `bones: ${info.bonesVersion}`,
4189
+ ].join("\n");
4190
+ deps.writeStdout(message);
4191
+ return message;
4192
+ }
4193
+ catch {
4194
+ // Fall through to shared agent selection when no runtime identity is available.
4195
+ }
4196
+ }
4197
+ const resolvedAgent = await resolveMissingAgentName(deps, "return-message");
4198
+ if (!resolvedAgent.ok) {
4199
+ deps.writeStdout(resolvedAgent.message);
4200
+ return resolvedAgent.message;
4201
+ }
4202
+ const agentRoot = path.join((0, identity_1.getAgentBundlesRoot)(), `${resolvedAgent.agent}.ouro`);
3777
4203
  const message = [
3778
- `agent: ${info.agentName}`,
3779
- `home: ${info.homePath}`,
3780
- `bones: ${info.bonesVersion}`,
4204
+ `agent: ${resolvedAgent.agent}`,
4205
+ `home: ${agentRoot}`,
4206
+ `bones: ${(0, runtime_metadata_1.getRuntimeMetadata)().version}`,
3781
4207
  ].join("\n");
3782
4208
  deps.writeStdout(message);
3783
4209
  return message;
3784
4210
  }
3785
4211
  catch {
3786
- const message = "error: no agent context — use --agent <name> to specify";
4212
+ const message = noAgentsFoundMessage();
3787
4213
  deps.writeStdout(message);
3788
4214
  return message;
3789
4215
  }
3790
- /* v8 ignore stop */
3791
4216
  }
3792
4217
  // ── changelog (local, no daemon socket needed) ──
3793
4218
  if (command.kind === "changelog") {
@@ -3831,10 +4256,9 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
3831
4256
  // ── thoughts (local, no daemon socket needed) ──
3832
4257
  if (command.kind === "thoughts") {
3833
4258
  try {
3834
- const agentName = command.agent ?? (0, identity_1.getAgentName)();
3835
4259
  /* v8 ignore next -- production fallback: tests always inject bundlesRoot via createTmpBundle @preserve */
3836
4260
  const bundlesRoot = deps.bundlesRoot ?? (0, identity_1.getAgentBundlesRoot)();
3837
- const agentRoot = path.join(bundlesRoot, `${agentName}.ouro`);
4261
+ const agentRoot = path.join(bundlesRoot, `${command.agent}.ouro`);
3838
4262
  const sessionFilePath = (0, thoughts_1.getInnerDialogSessionPath)(agentRoot);
3839
4263
  if (command.json) {
3840
4264
  try {
@@ -3877,10 +4301,9 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
3877
4301
  /* v8 ignore start -- CLI attention handler: requires real obligation store on disk @preserve */
3878
4302
  if (command.kind === "attention.list" || command.kind === "attention.show" || command.kind === "attention.history") {
3879
4303
  try {
3880
- const agentName = command.agent ?? (0, identity_1.getAgentName)();
3881
4304
  const { listActiveReturnObligations, readReturnObligation } = await Promise.resolve().then(() => __importStar(require("../../arc/obligations")));
3882
4305
  if (command.kind === "attention.list") {
3883
- const obligations = listActiveReturnObligations(agentName);
4306
+ const obligations = listActiveReturnObligations(command.agent);
3884
4307
  if (obligations.length === 0) {
3885
4308
  const message = "nothing held — attention queue is empty";
3886
4309
  deps.writeStdout(message);
@@ -3892,7 +4315,7 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
3892
4315
  return message;
3893
4316
  }
3894
4317
  if (command.kind === "attention.show") {
3895
- const obligation = readReturnObligation(agentName, command.id);
4318
+ const obligation = readReturnObligation(command.agent, command.id);
3896
4319
  if (!obligation) {
3897
4320
  const message = `no obligation found with id ${command.id}`;
3898
4321
  deps.writeStdout(message);
@@ -3904,7 +4327,7 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
3904
4327
  }
3905
4328
  // attention.history: show returned obligations
3906
4329
  const { getReturnObligationsDir } = await Promise.resolve().then(() => __importStar(require("../../arc/obligations")));
3907
- const obligationsDir = getReturnObligationsDir(agentName);
4330
+ const obligationsDir = getReturnObligationsDir(command.agent);
3908
4331
  let attEntries = [];
3909
4332
  try {
3910
4333
  attEntries = fs.readdirSync(obligationsDir);
@@ -3945,8 +4368,7 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
3945
4368
  /* v8 ignore start -- inner status handler: requires real agent state on disk @preserve */
3946
4369
  if (command.kind === "inner.status") {
3947
4370
  try {
3948
- const agentName = command.agent ?? (0, identity_1.getAgentName)();
3949
- const agentRoot = (0, identity_1.getAgentRoot)(agentName);
4371
+ const agentRoot = (0, identity_1.getAgentRoot)(command.agent);
3950
4372
  const { buildInnerStatusOutput } = await Promise.resolve().then(() => __importStar(require("./inner-status")));
3951
4373
  const { sessionPath: getSessionPath } = await Promise.resolve().then(() => __importStar(require("../config")));
3952
4374
  const { parseCadenceToMs: parseCadenceMs, DEFAULT_CADENCE_MS } = await Promise.resolve().then(() => __importStar(require("./cadence")));
@@ -4004,9 +4426,9 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
4004
4426
  }
4005
4427
  catch { /* no habits — heartbeat unknown */ }
4006
4428
  // Attention count
4007
- const activeObligations = listActiveReturnObligations(agentName);
4429
+ const activeObligations = listActiveReturnObligations(command.agent);
4008
4430
  const message = buildInnerStatusOutput({
4009
- agentName,
4431
+ agentName: command.agent,
4010
4432
  runtimeState,
4011
4433
  journalFiles,
4012
4434
  heartbeat,
@@ -4028,7 +4450,7 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
4028
4450
  /* v8 ignore start -- production default: requires full identity setup @preserve */
4029
4451
  const scanner = deps.scanSessions ?? (async () => []);
4030
4452
  /* v8 ignore stop */
4031
- const sessions = await scanner();
4453
+ const sessions = await scanner(command.agent);
4032
4454
  if (sessions.length === 0) {
4033
4455
  const message = "no active sessions";
4034
4456
  deps.writeStdout(message);