@ouro.bot/cli 0.1.0-alpha.430 → 0.1.0-alpha.432

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.
@@ -53,6 +53,7 @@ const runtime_1 = require("../../nerves/runtime");
53
53
  const ouro_path_installer_1 = require("../versioning/ouro-path-installer");
54
54
  const ouro_uti_1 = require("../versioning/ouro-uti");
55
55
  const ouro_version_manager_1 = require("../versioning/ouro-version-manager");
56
+ const update_checker_1 = require("../versioning/update-checker");
56
57
  const skill_management_installer_1 = require("./skill-management-installer");
57
58
  const hatch_flow_1 = require("../hatch/hatch-flow");
58
59
  const specialist_orchestrator_1 = require("../hatch/specialist-orchestrator");
@@ -144,6 +145,29 @@ function defaultReadRecentDaemonLogLines(lines = 10) {
144
145
  }
145
146
  return recentLines.slice(-lines).map((line) => (0, log_tailer_1.formatLogLine)(line));
146
147
  }
148
+ /* v8 ignore start -- CLI npm registry fetch wrapper: integration code @preserve */
149
+ async function defaultFetchCliRegistryJson(timeoutMs) {
150
+ const controller = new AbortController();
151
+ const timeoutId = setTimeout(() => {
152
+ controller.abort();
153
+ }, timeoutMs);
154
+ try {
155
+ const res = await fetch("https://registry.npmjs.org/@ouro.bot/cli", {
156
+ signal: controller.signal,
157
+ });
158
+ return res.json();
159
+ }
160
+ catch (error) {
161
+ if (error instanceof Error && error.name === "AbortError") {
162
+ throw new Error(`update check timed out after ${Math.max(1, Math.round(timeoutMs / 1000))}s`);
163
+ }
164
+ throw error;
165
+ }
166
+ finally {
167
+ clearTimeout(timeoutId);
168
+ }
169
+ }
170
+ /* v8 ignore stop */
147
171
  function defaultSleep(ms) {
148
172
  return new Promise((resolve) => setTimeout(resolve, ms));
149
173
  }
@@ -501,6 +525,7 @@ function createDefaultOuroCliDeps(socketPath = socket_client_1.DEFAULT_DAEMON_SO
501
525
  readRecentDaemonLogLines: defaultReadRecentDaemonLogLines,
502
526
  sleep: defaultSleep,
503
527
  now: () => Date.now(),
528
+ updateCheckTimeoutMs: update_checker_1.CLI_UPDATE_CHECK_TIMEOUT_MS,
504
529
  startupPollIntervalMs: 250,
505
530
  startupStabilityWindowMs: 1_500,
506
531
  startupTimeoutMs: 10_000,
@@ -540,10 +565,7 @@ function createDefaultOuroCliDeps(socketPath = socket_client_1.DEFAULT_DAEMON_SO
540
565
  checkForCliUpdate: async () => {
541
566
  const { checkForUpdate } = await Promise.resolve().then(() => __importStar(require("../versioning/update-checker")));
542
567
  return checkForUpdate((0, bundle_manifest_1.getPackageVersion)(), {
543
- fetchRegistryJson: async () => {
544
- const res = await fetch("https://registry.npmjs.org/@ouro.bot/cli");
545
- return res.json();
546
- },
568
+ fetchRegistryJson: () => defaultFetchCliRegistryJson(update_checker_1.CLI_UPDATE_CHECK_TIMEOUT_MS),
547
569
  distTag: "latest",
548
570
  });
549
571
  },
@@ -76,6 +76,7 @@ const provider_state_1 = require("../provider-state");
76
76
  const machine_identity_1 = require("../machine-identity");
77
77
  const provider_models_1 = require("../provider-models");
78
78
  const ouro_version_manager_1 = require("../versioning/ouro-version-manager");
79
+ const update_checker_1 = require("../versioning/update-checker");
79
80
  const sync_1 = require("../sync");
80
81
  const cli_parse_1 = require("./cli-parse");
81
82
  const cli_parse_2 = require("./cli-parse");
@@ -97,12 +98,62 @@ const up_progress_1 = require("./up-progress");
97
98
  const provider_ping_1 = require("../provider-ping");
98
99
  const agent_discovery_1 = require("./agent-discovery");
99
100
  const connect_bay_1 = require("./connect-bay");
101
+ const runtime_capability_check_1 = require("../runtime-capability-check");
100
102
  // ── ensureDaemonRunning ──
101
103
  const DEFAULT_DAEMON_STARTUP_TIMEOUT_MS = 10_000;
102
104
  const DEFAULT_DAEMON_STARTUP_POLL_INTERVAL_MS = 500;
103
105
  const DEFAULT_DAEMON_STARTUP_STABILITY_WINDOW_MS = 1_500;
104
106
  const DEFAULT_DAEMON_STARTUP_RETRY_LIMIT = 1;
105
107
  const DEFAULT_DAEMON_STARTUP_LOG_LINES = 10;
108
+ function summarizeCliUpdateCheckStatus(error, timedOut = false) {
109
+ const normalized = error.trim().toLowerCase();
110
+ if (timedOut || normalized.includes("timed out") || normalized.includes("abort")) {
111
+ return "skipped; registry did not answer";
112
+ }
113
+ if (normalized.includes("registry unavailable")) {
114
+ return "skipped; registry unavailable";
115
+ }
116
+ if (normalized.includes("network") ||
117
+ normalized.includes("fetch failed") ||
118
+ normalized.includes("enotfound") ||
119
+ normalized.includes("eai_again") ||
120
+ normalized.includes("econnreset")) {
121
+ return "skipped; registry unavailable";
122
+ }
123
+ return "skipped; update check unavailable";
124
+ }
125
+ async function runCliUpdateCheckWithTimeout(checkForCliUpdate, timeoutMs = update_checker_1.CLI_UPDATE_CHECK_TIMEOUT_MS) {
126
+ return await new Promise((resolve, reject) => {
127
+ let settled = false;
128
+ const timeoutId = setTimeout(() => {
129
+ settled = true;
130
+ resolve({
131
+ timedOut: true,
132
+ result: {
133
+ available: false,
134
+ error: `update check timed out after ${Math.max(1, Math.round(timeoutMs / 1000))}s`,
135
+ },
136
+ });
137
+ }, timeoutMs);
138
+ /* v8 ignore start -- Promise-settlement wiring is exercised by command tests; v8 misses these callback lines @preserve */
139
+ void checkForCliUpdate()
140
+ .then((result) => {
141
+ if (settled)
142
+ return;
143
+ settled = true;
144
+ clearTimeout(timeoutId);
145
+ resolve({ result, timedOut: false });
146
+ })
147
+ .catch((error) => {
148
+ if (settled)
149
+ return;
150
+ settled = true;
151
+ clearTimeout(timeoutId);
152
+ reject(error);
153
+ });
154
+ /* v8 ignore stop */
155
+ });
156
+ }
106
157
  async function checkAgentProviders(deps, agentsOverride, onProgress) {
107
158
  const agents = agentsOverride ?? await listCliAgents(deps);
108
159
  const bundlesRoot = deps.bundlesRoot ?? (0, identity_1.getAgentBundlesRoot)();
@@ -386,6 +437,17 @@ function renderCommandBoard(deps, options) {
386
437
  sections: options.sections,
387
438
  }).trimEnd();
388
439
  }
440
+ function writeConnectorIntro(deps, options) {
441
+ const text = ttyBoardEnabled(deps)
442
+ ? renderCommandBoard(deps, {
443
+ title: options.title,
444
+ subtitle: options.subtitle,
445
+ summary: options.summary,
446
+ sections: options.sections,
447
+ })
448
+ : options.fallbackLines.join("\n");
449
+ deps.writeStdout(text);
450
+ }
389
451
  async function promptForNamedAgent(title, subtitle, agents, deps) {
390
452
  if (!deps.promptInput)
391
453
  throw new Error("agent selection requires interactive input");
@@ -566,8 +628,8 @@ async function ensureDaemonRunning(deps, options = {}) {
566
628
  };
567
629
  let lastPid = null;
568
630
  for (let attempt = 0; attempt <= retryLimit; attempt += 1) {
569
- deps.reportDaemonStartupPhase?.("launching daemon process");
570
- deps.reportDaemonStartupPhase?.("waiting for daemon socket");
631
+ deps.reportDaemonStartupPhase?.("starting a fresh background service");
632
+ deps.reportDaemonStartupPhase?.("waiting for the new background service to answer");
571
633
  deps.cleanupStaleSocket(deps.socketPath);
572
634
  const bootStartedAtMs = (deps.now ?? Date.now)();
573
635
  const started = await deps.startDaemonProcess(deps.socketPath);
@@ -605,7 +667,7 @@ async function ensureDaemonRunning(deps, options = {}) {
605
667
  if (!startupFailure.retryable || attempt >= retryLimit) {
606
668
  break;
607
669
  }
608
- deps.reportDaemonStartupPhase?.("daemon startup lost stability; retrying once");
670
+ deps.reportDaemonStartupPhase?.("background service startup went sideways once; trying one more time");
609
671
  }
610
672
  return {
611
673
  alreadyRunning: false,
@@ -1427,14 +1489,17 @@ const DEFAULT_DAEMON_STATUS_TIMEOUT_MS = 4_000;
1427
1489
  const DEFAULT_AGENT_RESTART_TIMEOUT_MS = 8_000;
1428
1490
  const CONNECT_PROVIDER_CHOICES = ["openai-codex", "anthropic", "minimax", "azure", "github-copilot"];
1429
1491
  function hasRuntimeConfigValue(config, key) {
1492
+ return readRuntimeConfigString(config, key) !== null;
1493
+ }
1494
+ function readRuntimeConfigString(config, key) {
1430
1495
  const segments = key.split(".");
1431
1496
  let cursor = config;
1432
1497
  for (const segment of segments) {
1433
1498
  if (!cursor || typeof cursor !== "object" || Array.isArray(cursor))
1434
- return false;
1499
+ return null;
1435
1500
  cursor = cursor[segment];
1436
1501
  }
1437
- return typeof cursor === "string" && cursor.trim().length > 0;
1502
+ return typeof cursor === "string" && cursor.trim().length > 0 ? cursor : null;
1438
1503
  }
1439
1504
  function runtimeConfigReadStatus(runtime) {
1440
1505
  if (runtime.reason === "missing")
@@ -1514,46 +1579,47 @@ function readRuntimeApplyWorker(payload, agent) {
1514
1579
  }
1515
1580
  async function applyRuntimeChangeToRunningAgent(agent, deps, onProgress) {
1516
1581
  try {
1517
- onProgress?.("checking daemon socket");
1582
+ onProgress?.("checking whether Ouro is already running");
1518
1583
  const alive = await deps.checkSocketAlive(deps.socketPath);
1519
1584
  if (!alive)
1520
1585
  return "daemon is not running; next `ouro up` will load the change";
1521
- onProgress?.("requesting restart from daemon");
1586
+ onProgress?.(`asking Ouro to reload ${agent}`);
1522
1587
  const response = await withCliTimeout(DEFAULT_AGENT_RESTART_TIMEOUT_MS, "daemon restart request timed out", () => deps.sendCommand(deps.socketPath, { kind: "agent.restart", agent }));
1523
1588
  if (!response.ok)
1524
1589
  return `daemon restart skipped: ${response.error ?? response.message ?? "unknown daemon error"}`;
1525
1590
  const deadline = cliNowMs(deps) + DEFAULT_RUNTIME_APPLY_TIMEOUT_MS;
1526
- onProgress?.(`waiting for ${agent} to report running state`);
1591
+ onProgress?.(`waiting for ${agent} to come back\n- reload request accepted`);
1527
1592
  while (cliNowMs(deps) < deadline) {
1528
1593
  try {
1529
1594
  const statusResponse = await withCliTimeout(DEFAULT_DAEMON_STATUS_TIMEOUT_MS, "daemon status timed out", () => deps.sendCommand(deps.socketPath, { kind: "daemon.status" }));
1530
1595
  if (statusResponse.ok) {
1531
1596
  const payload = (0, cli_render_1.parseStatusPayload)(statusResponse.data);
1532
1597
  if (!payload) {
1533
- onProgress?.("daemon status did not include structured worker state");
1598
+ onProgress?.("waiting for Ouro to confirm the reload\n- daemon status did not include structured worker state");
1534
1599
  return "restart requested; daemon status is unavailable, so verify with `ouro status` if needed";
1535
1600
  }
1536
1601
  const worker = readRuntimeApplyWorker(payload, agent);
1537
1602
  if (!worker) {
1538
- onProgress?.(`still waiting: ${agent} is not listed by daemon`);
1603
+ onProgress?.(`waiting for ${agent} to come back\n- ${agent} is not listed by daemon yet`);
1539
1604
  }
1540
1605
  else if (worker.status === "running") {
1541
- onProgress?.(`daemon reports ${agent}/${worker.worker} running`);
1606
+ onProgress?.(`waiting for ${agent} to come back\n- daemon reports ${agent}/${worker.worker} running`);
1542
1607
  return `restarted ${agent} and the daemon reports it running`;
1543
1608
  }
1544
1609
  else if (worker.status === "crashed") {
1610
+ onProgress?.(`waiting for ${agent} to come back\n- daemon reports ${agent}/${worker.worker} crashed`);
1545
1611
  return `restart requested, but ${agent}/${worker.worker} crashed before reporting running${worker.errorReason ? `: ${worker.errorReason}` : worker.fixHint ? `: ${worker.fixHint}` : ""}`;
1546
1612
  }
1547
1613
  else {
1548
- onProgress?.(`still waiting: ${agent}/${worker.worker} is ${worker.status}`);
1614
+ onProgress?.(`waiting for ${agent} to come back\n- current worker state: ${worker.status}`);
1549
1615
  }
1550
1616
  }
1551
1617
  else {
1552
- onProgress?.(`still waiting: daemon status returned ${statusResponse.error ?? statusResponse.message ?? "unknown error"}`);
1618
+ onProgress?.(`waiting for ${agent} to come back\n- latest status check: ${statusResponse.error ?? statusResponse.message ?? "unknown error"}`);
1553
1619
  }
1554
1620
  }
1555
1621
  catch (error) {
1556
- onProgress?.(`still waiting: ${error instanceof Error ? error.message : String(error)}`);
1622
+ onProgress?.(`waiting for ${agent} to come back\n- latest status check: ${error instanceof Error ? error.message : String(error)}`);
1557
1623
  }
1558
1624
  if (cliNowMs(deps) >= deadline - DEFAULT_RUNTIME_APPLY_POLL_INTERVAL_MS)
1559
1625
  break;
@@ -1730,12 +1796,44 @@ async function buildConnectMenu(agent, deps, onProgress) {
1730
1796
  onProgress?.("loading this machine's settings");
1731
1797
  const machineRuntime = await (0, runtime_credentials_1.refreshMachineRuntimeCredentialConfig)(agent, currentMachineId(deps), { preserveCachedOnFailure: true });
1732
1798
  const { teamsEnabled, blueBubblesEnabled } = readConnectBaySenseFlags(agent, deps);
1733
- const perplexityStatus = runtimeConfig.ok
1734
- ? hasRuntimeConfigValue(runtimeConfig.config, "integrations.perplexityApiKey") ? "ready" : "missing"
1735
- : runtimeConfigReadStatus(runtimeConfig);
1736
- const embeddingsStatus = runtimeConfig.ok
1737
- ? hasRuntimeConfigValue(runtimeConfig.config, "integrations.openaiEmbeddingsApiKey") ? "ready" : "missing"
1738
- : runtimeConfigReadStatus(runtimeConfig);
1799
+ let perplexityStatus;
1800
+ let perplexityDetailLines;
1801
+ const perplexityApiKey = runtimeConfig.ok
1802
+ ? readRuntimeConfigString(runtimeConfig.config, "integrations.perplexityApiKey")
1803
+ : null;
1804
+ if (!runtimeConfig.ok) {
1805
+ perplexityStatus = runtimeConfigReadStatus(runtimeConfig);
1806
+ perplexityDetailLines = [];
1807
+ }
1808
+ else if (!perplexityApiKey) {
1809
+ perplexityStatus = "missing";
1810
+ perplexityDetailLines = ["no API key saved yet"];
1811
+ }
1812
+ else {
1813
+ onProgress?.("verifying Perplexity search");
1814
+ const verification = await (0, runtime_capability_check_1.verifyPerplexityCapability)(perplexityApiKey);
1815
+ perplexityStatus = verification.ok ? "ready" : "needs attention";
1816
+ perplexityDetailLines = [verification.ok ? "verified live just now" : `live check failed: ${verification.summary}`];
1817
+ }
1818
+ let embeddingsStatus;
1819
+ let embeddingsDetailLines;
1820
+ const embeddingsApiKey = runtimeConfig.ok
1821
+ ? readRuntimeConfigString(runtimeConfig.config, "integrations.openaiEmbeddingsApiKey")
1822
+ : null;
1823
+ if (!runtimeConfig.ok) {
1824
+ embeddingsStatus = runtimeConfigReadStatus(runtimeConfig);
1825
+ embeddingsDetailLines = [];
1826
+ }
1827
+ else if (!embeddingsApiKey) {
1828
+ embeddingsStatus = "missing";
1829
+ embeddingsDetailLines = ["no API key saved yet"];
1830
+ }
1831
+ else {
1832
+ onProgress?.("verifying memory embeddings");
1833
+ const verification = await (0, runtime_capability_check_1.verifyEmbeddingsCapability)(embeddingsApiKey);
1834
+ embeddingsStatus = verification.ok ? "ready" : "needs attention";
1835
+ embeddingsDetailLines = [verification.ok ? "verified live just now" : `live check failed: ${verification.summary}`];
1836
+ }
1739
1837
  const teamsStatus = runtimeConfig.ok
1740
1838
  ? hasRuntimeConfigValue(runtimeConfig.config, "teams.clientId")
1741
1839
  && hasRuntimeConfigValue(runtimeConfig.config, "teams.clientSecret")
@@ -1768,6 +1866,7 @@ async function buildConnectMenu(agent, deps, onProgress) {
1768
1866
  section: "Portable",
1769
1867
  status: perplexityStatus,
1770
1868
  description: "Web search via Perplexity.",
1869
+ detailLines: perplexityDetailLines,
1771
1870
  nextAction: (0, connect_bay_1.connectEntryNeedsAttention)({
1772
1871
  option: "2",
1773
1872
  name: "Perplexity search",
@@ -1781,6 +1880,7 @@ async function buildConnectMenu(agent, deps, onProgress) {
1781
1880
  section: "Portable",
1782
1881
  status: embeddingsStatus,
1783
1882
  description: "Memory retrieval and note search.",
1883
+ detailLines: embeddingsDetailLines,
1784
1884
  nextAction: (0, connect_bay_1.connectEntryNeedsAttention)({
1785
1885
  option: "3",
1786
1886
  name: "Memory embeddings",
@@ -1828,16 +1928,44 @@ async function executeConnectPerplexity(agent, deps) {
1828
1928
  throw new Error("SerpentGuide has no persistent runtime credentials. Connect Perplexity on the hatchling agent instead.");
1829
1929
  }
1830
1930
  const promptSecret = requirePromptSecret(deps, "Perplexity API key entry");
1831
- deps.writeStdout([
1832
- `Connect Perplexity for ${agent}`,
1833
- "The API key stays hidden while you type.",
1834
- `Ouro stores it in ${agent}'s vault runtime/config item.`,
1835
- ].join("\n"));
1931
+ writeConnectorIntro(deps, {
1932
+ title: "Connect Perplexity",
1933
+ subtitle: `${agent} gets portable web search.`,
1934
+ summary: "Add one hidden API key, verify it live, and make web search travel with this agent.",
1935
+ sections: [
1936
+ {
1937
+ title: "Unlocks",
1938
+ lines: [
1939
+ "Portable web search inside Ouro.",
1940
+ ],
1941
+ },
1942
+ {
1943
+ title: "What you need",
1944
+ lines: [
1945
+ "One Perplexity API key.",
1946
+ "It stays hidden while you type.",
1947
+ ],
1948
+ },
1949
+ {
1950
+ title: "Where it lives",
1951
+ lines: [
1952
+ `${agent}'s vault runtime/config item.`,
1953
+ "It travels with the agent across machines.",
1954
+ ],
1955
+ },
1956
+ ],
1957
+ fallbackLines: [
1958
+ `Connect Perplexity for ${agent}`,
1959
+ "The API key stays hidden while you type.",
1960
+ `Ouro stores it in ${agent}'s vault runtime/config item.`,
1961
+ ],
1962
+ });
1836
1963
  const key = (await promptSecret("Perplexity API key: ")).trim();
1837
1964
  if (!key)
1838
1965
  throw new Error("Perplexity API key cannot be blank");
1839
1966
  const progress = createHumanCommandProgress(deps, "connect perplexity");
1840
1967
  let stored;
1968
+ let verification;
1841
1969
  let reload;
1842
1970
  try {
1843
1971
  stored = await runCommandProgressPhase(progress, "saving Perplexity search", () => storeRuntimeConfigKey({
@@ -1848,6 +1976,23 @@ async function executeConnectPerplexity(agent, deps) {
1848
1976
  deps,
1849
1977
  onProgress: (message) => progress.updateDetail(message),
1850
1978
  }), () => "secret stored");
1979
+ progress.startPhase("verifying Perplexity search");
1980
+ verification = await (0, runtime_capability_check_1.verifyPerplexityCapability)(key);
1981
+ if (!verification.ok) {
1982
+ progress.failPhase("verifying Perplexity search", verification.summary);
1983
+ progress.end();
1984
+ const message = [
1985
+ `Perplexity key was saved for ${agent}, but the live check failed.`,
1986
+ `stored: ${stored.itemPath}`,
1987
+ `live check: ${verification.summary}`,
1988
+ "secret was not printed",
1989
+ "",
1990
+ `Next: rerun \`ouro connect perplexity --agent ${agent}\` with a working key.`,
1991
+ ].join("\n");
1992
+ deps.writeStdout(message);
1993
+ return message;
1994
+ }
1995
+ progress.completePhase("verifying Perplexity search", verification.summary);
1851
1996
  reload = await runCommandProgressPhase(progress, `applying change to running ${agent}`, () => applyRuntimeChangeToRunningAgent(agent, deps, (message) => progress.updateDetail(message)), (result) => result);
1852
1997
  }
1853
1998
  finally {
@@ -1870,16 +2015,44 @@ async function executeConnectEmbeddings(agent, deps) {
1870
2015
  throw new Error("SerpentGuide has no persistent runtime credentials. Connect embeddings on the hatchling agent instead.");
1871
2016
  }
1872
2017
  const promptSecret = requirePromptSecret(deps, "OpenAI embeddings API key entry");
1873
- deps.writeStdout([
1874
- `Connect embeddings for ${agent}`,
1875
- "The API key stays hidden while you type.",
1876
- `Ouro stores it in ${agent}'s vault runtime/config item.`,
1877
- ].join("\n"));
2018
+ writeConnectorIntro(deps, {
2019
+ title: "Connect Embeddings",
2020
+ subtitle: `${agent} gets portable note and memory search.`,
2021
+ summary: "Add one hidden API key, verify it live, and let semantic memory travel with this agent.",
2022
+ sections: [
2023
+ {
2024
+ title: "Unlocks",
2025
+ lines: [
2026
+ "Portable note search and memory retrieval.",
2027
+ ],
2028
+ },
2029
+ {
2030
+ title: "What you need",
2031
+ lines: [
2032
+ "One OpenAI embeddings API key.",
2033
+ "It stays hidden while you type.",
2034
+ ],
2035
+ },
2036
+ {
2037
+ title: "Where it lives",
2038
+ lines: [
2039
+ `${agent}'s vault runtime/config item.`,
2040
+ "It travels with the agent across machines.",
2041
+ ],
2042
+ },
2043
+ ],
2044
+ fallbackLines: [
2045
+ `Connect embeddings for ${agent}`,
2046
+ "The API key stays hidden while you type.",
2047
+ `Ouro stores it in ${agent}'s vault runtime/config item.`,
2048
+ ],
2049
+ });
1878
2050
  const key = (await promptSecret("OpenAI embeddings API key: ")).trim();
1879
2051
  if (!key)
1880
2052
  throw new Error("OpenAI embeddings API key cannot be blank");
1881
2053
  const progress = createHumanCommandProgress(deps, "connect embeddings");
1882
2054
  let stored;
2055
+ let verification;
1883
2056
  let reload;
1884
2057
  try {
1885
2058
  stored = await runCommandProgressPhase(progress, "saving memory embeddings", () => storeRuntimeConfigKey({
@@ -1890,6 +2063,23 @@ async function executeConnectEmbeddings(agent, deps) {
1890
2063
  deps,
1891
2064
  onProgress: (message) => progress.updateDetail(message),
1892
2065
  }), () => "secret stored");
2066
+ progress.startPhase("verifying memory embeddings");
2067
+ verification = await (0, runtime_capability_check_1.verifyEmbeddingsCapability)(key);
2068
+ if (!verification.ok) {
2069
+ progress.failPhase("verifying memory embeddings", verification.summary);
2070
+ progress.end();
2071
+ const message = [
2072
+ `Embeddings key was saved for ${agent}, but the live check failed.`,
2073
+ `stored: ${stored.itemPath}`,
2074
+ `live check: ${verification.summary}`,
2075
+ "secret was not printed",
2076
+ "",
2077
+ `Next: rerun \`ouro connect embeddings --agent ${agent}\` with a working key.`,
2078
+ ].join("\n");
2079
+ deps.writeStdout(message);
2080
+ return message;
2081
+ }
2082
+ progress.completePhase("verifying memory embeddings", verification.summary);
1893
2083
  reload = await runCommandProgressPhase(progress, `applying change to running ${agent}`, () => applyRuntimeChangeToRunningAgent(agent, deps, (message) => progress.updateDetail(message)), (result) => result);
1894
2084
  }
1895
2085
  finally {
@@ -3450,9 +3640,11 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
3450
3640
  // ── versioned CLI update check ──
3451
3641
  if (deps.checkForCliUpdate) {
3452
3642
  progress.startPhase("update check");
3643
+ progress.updateDetail("checking npm registry\ncontinuing startup if it stays quiet");
3453
3644
  let pendingReExec = false;
3645
+ let updateCheckStatus = "up to date";
3454
3646
  try {
3455
- const updateResult = await deps.checkForCliUpdate();
3647
+ const { result: updateResult, timedOut } = await runCliUpdateCheckWithTimeout(deps.checkForCliUpdate, deps.updateCheckTimeoutMs ?? update_checker_1.CLI_UPDATE_CHECK_TIMEOUT_MS);
3456
3648
  if (updateResult.available && updateResult.latestVersion) {
3457
3649
  /* v8 ignore next -- fallback: getCurrentCliVersion always injected in tests @preserve */
3458
3650
  const currentVersion = linkedVersionBeforeUp ?? "unknown";
@@ -3466,9 +3658,13 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
3466
3658
  }
3467
3659
  pendingReExec = true;
3468
3660
  }
3661
+ else if (updateResult.error) {
3662
+ updateCheckStatus = summarizeCliUpdateCheckStatus(updateResult.error, timedOut);
3663
+ }
3469
3664
  /* v8 ignore start -- update check error: tested via daemon-cli-update-flow.test.ts @preserve */
3470
3665
  }
3471
3666
  catch (error) {
3667
+ updateCheckStatus = summarizeCliUpdateCheckStatus(error instanceof Error ? error.message : String(error));
3472
3668
  (0, runtime_1.emitNervesEvent)({
3473
3669
  level: "warn",
3474
3670
  component: "daemon",
@@ -3483,7 +3679,7 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
3483
3679
  deps.reExecFromNewVersion(args);
3484
3680
  }
3485
3681
  else {
3486
- progress.completePhase("update check", "up to date");
3682
+ progress.completePhase("update check", updateCheckStatus);
3487
3683
  }
3488
3684
  }
3489
3685
  progress.startPhase("system setup");
@@ -3888,16 +4084,16 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
3888
4084
  const sections = [localSection];
3889
4085
  if (deps.checkForCliUpdate) {
3890
4086
  try {
3891
- const updateResult = await deps.checkForCliUpdate();
4087
+ const { result: updateResult, timedOut } = await runCliUpdateCheckWithTimeout(deps.checkForCliUpdate, deps.updateCheckTimeoutMs ?? update_checker_1.CLI_UPDATE_CHECK_TIMEOUT_MS);
3892
4088
  if (updateResult.latestVersion) {
3893
4089
  sections.push(`published latest: ${updateResult.latestVersion} (${updateResult.available ? "update available" : "up to date"})`);
3894
4090
  }
3895
4091
  else if (updateResult.error) {
3896
- sections.push(`published latest: unavailable (${updateResult.error})`);
4092
+ sections.push(`published latest: unavailable (${summarizeCliUpdateCheckStatus(updateResult.error, timedOut)})`);
3897
4093
  }
3898
4094
  }
3899
4095
  catch (err) {
3900
- const reason = err instanceof Error ? err.message : String(err);
4096
+ const reason = summarizeCliUpdateCheckStatus(err instanceof Error ? err.message : String(err));
3901
4097
  sections.push(`published latest: unavailable (${reason})`);
3902
4098
  }
3903
4099
  }
@@ -9,7 +9,7 @@ async function verifyDaemonStarted(deps) {
9
9
  const maxWaitMs = 10_000;
10
10
  const pollIntervalMs = 500;
11
11
  const deadline = Date.now() + maxWaitMs;
12
- deps.onProgress?.("waiting for the new background service to answer");
12
+ deps.onProgress?.("waiting for the replacement background service to answer");
13
13
  while (Date.now() < deadline) {
14
14
  await new Promise((r) => setTimeout(r, pollIntervalMs));
15
15
  if (await deps.checkSocketAlive(deps.socketPath))
@@ -88,7 +88,7 @@ function formatRuntimeDriftPublicSummary(reasons) {
88
88
  return reasons.map((reason) => reason.label).join(", ");
89
89
  }
90
90
  async function ensureCurrentDaemonRuntime(deps) {
91
- deps.onProgress?.("checking the running background service");
91
+ deps.onProgress?.("checking whether an older background service is already running");
92
92
  const localRuntime = normalizeRuntimeIdentity({
93
93
  version: deps.localVersion,
94
94
  lastUpdated: deps.localLastUpdated,
@@ -105,7 +105,7 @@ async function ensureCurrentDaemonRuntime(deps) {
105
105
  const includesVersionDrift = driftReasons.some((entry) => entry.key === "version");
106
106
  const publicDriftSummary = formatRuntimeDriftPublicSummary(driftReasons);
107
107
  try {
108
- deps.onProgress?.("stopping the old background service");
108
+ deps.onProgress?.("stopping the older background service");
109
109
  await deps.stopDaemon();
110
110
  }
111
111
  catch (error) {
@@ -113,8 +113,8 @@ async function ensureCurrentDaemonRuntime(deps) {
113
113
  result = {
114
114
  alreadyRunning: true,
115
115
  message: includesVersionDrift
116
- ? `daemon already running (${deps.socketPath}; could not replace the running background service ${runningVersion} -> ${deps.localVersion}: ${reason})`
117
- : `daemon already running (${deps.socketPath}; could not replace the running background service after runtime drift ${publicDriftSummary}: ${reason})`,
116
+ ? `daemon already running (${deps.socketPath}; could not replace the older background service ${runningVersion} -> ${deps.localVersion}: ${reason})`
117
+ : `daemon already running (${deps.socketPath}; could not replace the older background service after runtime drift ${publicDriftSummary}: ${reason})`,
118
118
  };
119
119
  (0, runtime_1.emitNervesEvent)({
120
120
  level: "warn",
@@ -141,7 +141,7 @@ async function ensureCurrentDaemonRuntime(deps) {
141
141
  return result;
142
142
  }
143
143
  deps.cleanupStaleSocket(deps.socketPath);
144
- deps.onProgress?.("starting the new background service");
144
+ deps.onProgress?.("starting the replacement background service");
145
145
  const started = await deps.startDaemonProcess(deps.socketPath);
146
146
  const pid = started.pid ?? "unknown";
147
147
  const verified = await verifyDaemonStarted(deps);
@@ -150,8 +150,8 @@ async function ensureCurrentDaemonRuntime(deps) {
150
150
  result = {
151
151
  alreadyRunning: false,
152
152
  message: includesVersionDrift
153
- ? `replaced the running background service ${runningVersion} -> ${deps.localVersion} (pid ${pid})${suffix}`
154
- : `replaced the running background service after runtime drift: ${publicDriftSummary} (pid ${pid})${suffix}`,
153
+ ? `replaced an older background service ${runningVersion} -> ${deps.localVersion} (pid ${pid})${suffix}`
154
+ : `replaced an older background service after runtime drift: ${publicDriftSummary} (pid ${pid})${suffix}`,
155
155
  verifyStartupStatus: verified,
156
156
  startedPid: started.pid ?? null,
157
157
  };
@@ -93,10 +93,11 @@ function renderWaitingForDaemon(elapsed, latestEvent, prevLineCount = 0, options
93
93
  const spinner = SPINNER_FRAMES[frameIndex];
94
94
  const lines = [];
95
95
  lines.push(isTTY
96
- ? `${spinner} ${BOLD}waiting for daemon${RESET} ${DIM}(${elapsedSec}s)${RESET}`
97
- : `${spinner} waiting for daemon (${elapsedSec}s)`);
96
+ ? `${spinner} ${BOLD}starting background service${RESET} ${DIM}(${elapsedSec}s)${RESET}`
97
+ : `${spinner} starting background service (${elapsedSec}s)`);
98
98
  if (latestEvent) {
99
- lines.push(isTTY ? ` ${DIM}${latestEvent}${RESET}` : ` ${latestEvent}`);
99
+ const detail = `latest daemon event: ${latestEvent}`;
100
+ lines.push(isTTY ? ` ${DIM}${detail}${RESET}` : ` ${detail}`);
100
101
  }
101
102
  return renderStartupLines(lines, prevLineCount, isTTY);
102
103
  }
@@ -182,7 +183,10 @@ async function pollDaemonStartup(deps) {
182
183
  }
183
184
  // Show what the daemon is doing from its log
184
185
  const latestEvent = deps.readLatestDaemonEvent?.() ?? null;
185
- reportProgress(latestEvent ?? "waiting for daemon");
186
+ reportProgress([
187
+ "waiting for Ouro to answer",
188
+ latestEvent ? `- latest daemon event: ${latestEvent}` : "- background service is still starting",
189
+ ].join("\n"));
186
190
  if (shouldRender) {
187
191
  const output = renderWaitingForDaemon(elapsed, latestEvent, prevLineCount, { isTTY });
188
192
  deps.writeRaw(output);
@@ -222,11 +226,20 @@ async function pollDaemonStartup(deps) {
222
226
  await deps.sleep(POLL_INTERVAL_MS);
223
227
  }
224
228
  }
229
+ function formatStartupWorkerLine(payload) {
230
+ const base = `- ${payload.agent}/${payload.worker}: ${payload.status}`;
231
+ if (payload.status === "crashed" && payload.errorReason) {
232
+ return `${base} (${payload.errorReason})`;
233
+ }
234
+ return base;
235
+ }
225
236
  function formatStartupProgressDetail(payload) {
226
237
  if (payload.workers.length === 0)
227
- return "daemon answered";
228
- const workers = payload.workers.map((worker) => `${worker.agent}/${worker.worker} ${worker.status}`).join(", ");
229
- return `waiting for agents: ${workers}`;
238
+ return "Ouro answered";
239
+ return [
240
+ "Ouro answered",
241
+ ...payload.workers.map((worker) => formatStartupWorkerLine(worker)),
242
+ ].join("\n");
230
243
  }
231
244
  function colorStatus(status) {
232
245
  const statusColor = status === "running" ? GREEN
@@ -21,6 +21,14 @@ const BOLD = "\x1b[1m";
21
21
  const DIM = "\x1b[2m";
22
22
  const GREEN = "\x1b[38;2;46;204;64m";
23
23
  const RED = "\x1b[38;2;255;106;106m";
24
+ function splitDetailLines(detail) {
25
+ if (!detail)
26
+ return [];
27
+ return detail
28
+ .split(/\r?\n/)
29
+ .map((line) => line.trimEnd())
30
+ .filter((line) => line.length > 0);
31
+ }
24
32
  // ── UpProgress class ──
25
33
  class UpProgress {
26
34
  write;
@@ -99,7 +107,9 @@ class UpProgress {
99
107
  this.flushRender();
100
108
  return;
101
109
  }
102
- this.write(` ${detail}\n`);
110
+ for (const line of splitDetailLines(detail)) {
111
+ this.write(` ${line}\n`);
112
+ }
103
113
  }
104
114
  /**
105
115
  * Mark the current phase as done. In non-TTY mode, immediately writes
@@ -198,8 +208,10 @@ class UpProgress {
198
208
  const elapsedSec = (elapsed / 1000).toFixed(1);
199
209
  const frameIndex = Math.floor(elapsed / 80) % SPINNER_FRAMES.length;
200
210
  const spinner = SPINNER_FRAMES[frameIndex];
201
- const detailSuffix = this.currentPhase.detail ? ` \u2014 ${this.currentPhase.detail}` : "";
202
- lines.push(` ${BOLD}${spinner}${RESET} ${this.currentPhase.label} ${DIM}(${elapsedSec}s)${detailSuffix}${RESET}`);
211
+ lines.push(` ${BOLD}${spinner}${RESET} ${this.currentPhase.label} ${DIM}(${elapsedSec}s)${RESET}`);
212
+ for (const detailLine of splitDetailLines(this.currentPhase.detail)) {
213
+ lines.push(` ${DIM}${detailLine}${RESET}`);
214
+ }
203
215
  }
204
216
  let output = "";
205
217
  if (this.prevLineCount > 0) {