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

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.
@@ -97,6 +97,7 @@ const up_progress_1 = require("./up-progress");
97
97
  const provider_ping_1 = require("../provider-ping");
98
98
  const agent_discovery_1 = require("./agent-discovery");
99
99
  const connect_bay_1 = require("./connect-bay");
100
+ const runtime_capability_check_1 = require("../runtime-capability-check");
100
101
  // ── ensureDaemonRunning ──
101
102
  const DEFAULT_DAEMON_STARTUP_TIMEOUT_MS = 10_000;
102
103
  const DEFAULT_DAEMON_STARTUP_POLL_INTERVAL_MS = 500;
@@ -386,6 +387,17 @@ function renderCommandBoard(deps, options) {
386
387
  sections: options.sections,
387
388
  }).trimEnd();
388
389
  }
390
+ function writeConnectorIntro(deps, options) {
391
+ const text = ttyBoardEnabled(deps)
392
+ ? renderCommandBoard(deps, {
393
+ title: options.title,
394
+ subtitle: options.subtitle,
395
+ summary: options.summary,
396
+ sections: options.sections,
397
+ })
398
+ : options.fallbackLines.join("\n");
399
+ deps.writeStdout(text);
400
+ }
389
401
  async function promptForNamedAgent(title, subtitle, agents, deps) {
390
402
  if (!deps.promptInput)
391
403
  throw new Error("agent selection requires interactive input");
@@ -566,8 +578,8 @@ async function ensureDaemonRunning(deps, options = {}) {
566
578
  };
567
579
  let lastPid = null;
568
580
  for (let attempt = 0; attempt <= retryLimit; attempt += 1) {
569
- deps.reportDaemonStartupPhase?.("launching daemon process");
570
- deps.reportDaemonStartupPhase?.("waiting for daemon socket");
581
+ deps.reportDaemonStartupPhase?.("starting a fresh background service");
582
+ deps.reportDaemonStartupPhase?.("waiting for the new background service to answer");
571
583
  deps.cleanupStaleSocket(deps.socketPath);
572
584
  const bootStartedAtMs = (deps.now ?? Date.now)();
573
585
  const started = await deps.startDaemonProcess(deps.socketPath);
@@ -605,7 +617,7 @@ async function ensureDaemonRunning(deps, options = {}) {
605
617
  if (!startupFailure.retryable || attempt >= retryLimit) {
606
618
  break;
607
619
  }
608
- deps.reportDaemonStartupPhase?.("daemon startup lost stability; retrying once");
620
+ deps.reportDaemonStartupPhase?.("background service startup went sideways once; trying one more time");
609
621
  }
610
622
  return {
611
623
  alreadyRunning: false,
@@ -1427,14 +1439,17 @@ const DEFAULT_DAEMON_STATUS_TIMEOUT_MS = 4_000;
1427
1439
  const DEFAULT_AGENT_RESTART_TIMEOUT_MS = 8_000;
1428
1440
  const CONNECT_PROVIDER_CHOICES = ["openai-codex", "anthropic", "minimax", "azure", "github-copilot"];
1429
1441
  function hasRuntimeConfigValue(config, key) {
1442
+ return readRuntimeConfigString(config, key) !== null;
1443
+ }
1444
+ function readRuntimeConfigString(config, key) {
1430
1445
  const segments = key.split(".");
1431
1446
  let cursor = config;
1432
1447
  for (const segment of segments) {
1433
1448
  if (!cursor || typeof cursor !== "object" || Array.isArray(cursor))
1434
- return false;
1449
+ return null;
1435
1450
  cursor = cursor[segment];
1436
1451
  }
1437
- return typeof cursor === "string" && cursor.trim().length > 0;
1452
+ return typeof cursor === "string" && cursor.trim().length > 0 ? cursor : null;
1438
1453
  }
1439
1454
  function runtimeConfigReadStatus(runtime) {
1440
1455
  if (runtime.reason === "missing")
@@ -1514,46 +1529,47 @@ function readRuntimeApplyWorker(payload, agent) {
1514
1529
  }
1515
1530
  async function applyRuntimeChangeToRunningAgent(agent, deps, onProgress) {
1516
1531
  try {
1517
- onProgress?.("checking daemon socket");
1532
+ onProgress?.("checking whether Ouro is already running");
1518
1533
  const alive = await deps.checkSocketAlive(deps.socketPath);
1519
1534
  if (!alive)
1520
1535
  return "daemon is not running; next `ouro up` will load the change";
1521
- onProgress?.("requesting restart from daemon");
1536
+ onProgress?.(`asking Ouro to reload ${agent}`);
1522
1537
  const response = await withCliTimeout(DEFAULT_AGENT_RESTART_TIMEOUT_MS, "daemon restart request timed out", () => deps.sendCommand(deps.socketPath, { kind: "agent.restart", agent }));
1523
1538
  if (!response.ok)
1524
1539
  return `daemon restart skipped: ${response.error ?? response.message ?? "unknown daemon error"}`;
1525
1540
  const deadline = cliNowMs(deps) + DEFAULT_RUNTIME_APPLY_TIMEOUT_MS;
1526
- onProgress?.(`waiting for ${agent} to report running state`);
1541
+ onProgress?.(`waiting for ${agent} to come back\n- reload request accepted`);
1527
1542
  while (cliNowMs(deps) < deadline) {
1528
1543
  try {
1529
1544
  const statusResponse = await withCliTimeout(DEFAULT_DAEMON_STATUS_TIMEOUT_MS, "daemon status timed out", () => deps.sendCommand(deps.socketPath, { kind: "daemon.status" }));
1530
1545
  if (statusResponse.ok) {
1531
1546
  const payload = (0, cli_render_1.parseStatusPayload)(statusResponse.data);
1532
1547
  if (!payload) {
1533
- onProgress?.("daemon status did not include structured worker state");
1548
+ onProgress?.("waiting for Ouro to confirm the reload\n- daemon status did not include structured worker state");
1534
1549
  return "restart requested; daemon status is unavailable, so verify with `ouro status` if needed";
1535
1550
  }
1536
1551
  const worker = readRuntimeApplyWorker(payload, agent);
1537
1552
  if (!worker) {
1538
- onProgress?.(`still waiting: ${agent} is not listed by daemon`);
1553
+ onProgress?.(`waiting for ${agent} to come back\n- ${agent} is not listed by daemon yet`);
1539
1554
  }
1540
1555
  else if (worker.status === "running") {
1541
- onProgress?.(`daemon reports ${agent}/${worker.worker} running`);
1556
+ onProgress?.(`waiting for ${agent} to come back\n- daemon reports ${agent}/${worker.worker} running`);
1542
1557
  return `restarted ${agent} and the daemon reports it running`;
1543
1558
  }
1544
1559
  else if (worker.status === "crashed") {
1560
+ onProgress?.(`waiting for ${agent} to come back\n- daemon reports ${agent}/${worker.worker} crashed`);
1545
1561
  return `restart requested, but ${agent}/${worker.worker} crashed before reporting running${worker.errorReason ? `: ${worker.errorReason}` : worker.fixHint ? `: ${worker.fixHint}` : ""}`;
1546
1562
  }
1547
1563
  else {
1548
- onProgress?.(`still waiting: ${agent}/${worker.worker} is ${worker.status}`);
1564
+ onProgress?.(`waiting for ${agent} to come back\n- current worker state: ${worker.status}`);
1549
1565
  }
1550
1566
  }
1551
1567
  else {
1552
- onProgress?.(`still waiting: daemon status returned ${statusResponse.error ?? statusResponse.message ?? "unknown error"}`);
1568
+ onProgress?.(`waiting for ${agent} to come back\n- latest status check: ${statusResponse.error ?? statusResponse.message ?? "unknown error"}`);
1553
1569
  }
1554
1570
  }
1555
1571
  catch (error) {
1556
- onProgress?.(`still waiting: ${error instanceof Error ? error.message : String(error)}`);
1572
+ onProgress?.(`waiting for ${agent} to come back\n- latest status check: ${error instanceof Error ? error.message : String(error)}`);
1557
1573
  }
1558
1574
  if (cliNowMs(deps) >= deadline - DEFAULT_RUNTIME_APPLY_POLL_INTERVAL_MS)
1559
1575
  break;
@@ -1730,12 +1746,44 @@ async function buildConnectMenu(agent, deps, onProgress) {
1730
1746
  onProgress?.("loading this machine's settings");
1731
1747
  const machineRuntime = await (0, runtime_credentials_1.refreshMachineRuntimeCredentialConfig)(agent, currentMachineId(deps), { preserveCachedOnFailure: true });
1732
1748
  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);
1749
+ let perplexityStatus;
1750
+ let perplexityDetailLines;
1751
+ const perplexityApiKey = runtimeConfig.ok
1752
+ ? readRuntimeConfigString(runtimeConfig.config, "integrations.perplexityApiKey")
1753
+ : null;
1754
+ if (!runtimeConfig.ok) {
1755
+ perplexityStatus = runtimeConfigReadStatus(runtimeConfig);
1756
+ perplexityDetailLines = [];
1757
+ }
1758
+ else if (!perplexityApiKey) {
1759
+ perplexityStatus = "missing";
1760
+ perplexityDetailLines = ["no API key saved yet"];
1761
+ }
1762
+ else {
1763
+ onProgress?.("verifying Perplexity search");
1764
+ const verification = await (0, runtime_capability_check_1.verifyPerplexityCapability)(perplexityApiKey);
1765
+ perplexityStatus = verification.ok ? "ready" : "needs attention";
1766
+ perplexityDetailLines = [verification.ok ? "verified live just now" : `live check failed: ${verification.summary}`];
1767
+ }
1768
+ let embeddingsStatus;
1769
+ let embeddingsDetailLines;
1770
+ const embeddingsApiKey = runtimeConfig.ok
1771
+ ? readRuntimeConfigString(runtimeConfig.config, "integrations.openaiEmbeddingsApiKey")
1772
+ : null;
1773
+ if (!runtimeConfig.ok) {
1774
+ embeddingsStatus = runtimeConfigReadStatus(runtimeConfig);
1775
+ embeddingsDetailLines = [];
1776
+ }
1777
+ else if (!embeddingsApiKey) {
1778
+ embeddingsStatus = "missing";
1779
+ embeddingsDetailLines = ["no API key saved yet"];
1780
+ }
1781
+ else {
1782
+ onProgress?.("verifying memory embeddings");
1783
+ const verification = await (0, runtime_capability_check_1.verifyEmbeddingsCapability)(embeddingsApiKey);
1784
+ embeddingsStatus = verification.ok ? "ready" : "needs attention";
1785
+ embeddingsDetailLines = [verification.ok ? "verified live just now" : `live check failed: ${verification.summary}`];
1786
+ }
1739
1787
  const teamsStatus = runtimeConfig.ok
1740
1788
  ? hasRuntimeConfigValue(runtimeConfig.config, "teams.clientId")
1741
1789
  && hasRuntimeConfigValue(runtimeConfig.config, "teams.clientSecret")
@@ -1768,6 +1816,7 @@ async function buildConnectMenu(agent, deps, onProgress) {
1768
1816
  section: "Portable",
1769
1817
  status: perplexityStatus,
1770
1818
  description: "Web search via Perplexity.",
1819
+ detailLines: perplexityDetailLines,
1771
1820
  nextAction: (0, connect_bay_1.connectEntryNeedsAttention)({
1772
1821
  option: "2",
1773
1822
  name: "Perplexity search",
@@ -1781,6 +1830,7 @@ async function buildConnectMenu(agent, deps, onProgress) {
1781
1830
  section: "Portable",
1782
1831
  status: embeddingsStatus,
1783
1832
  description: "Memory retrieval and note search.",
1833
+ detailLines: embeddingsDetailLines,
1784
1834
  nextAction: (0, connect_bay_1.connectEntryNeedsAttention)({
1785
1835
  option: "3",
1786
1836
  name: "Memory embeddings",
@@ -1828,16 +1878,44 @@ async function executeConnectPerplexity(agent, deps) {
1828
1878
  throw new Error("SerpentGuide has no persistent runtime credentials. Connect Perplexity on the hatchling agent instead.");
1829
1879
  }
1830
1880
  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"));
1881
+ writeConnectorIntro(deps, {
1882
+ title: "Connect Perplexity",
1883
+ subtitle: `${agent} gets portable web search.`,
1884
+ summary: "Add one hidden API key, verify it live, and make web search travel with this agent.",
1885
+ sections: [
1886
+ {
1887
+ title: "Unlocks",
1888
+ lines: [
1889
+ "Portable web search inside Ouro.",
1890
+ ],
1891
+ },
1892
+ {
1893
+ title: "What you need",
1894
+ lines: [
1895
+ "One Perplexity API key.",
1896
+ "It stays hidden while you type.",
1897
+ ],
1898
+ },
1899
+ {
1900
+ title: "Where it lives",
1901
+ lines: [
1902
+ `${agent}'s vault runtime/config item.`,
1903
+ "It travels with the agent across machines.",
1904
+ ],
1905
+ },
1906
+ ],
1907
+ fallbackLines: [
1908
+ `Connect Perplexity for ${agent}`,
1909
+ "The API key stays hidden while you type.",
1910
+ `Ouro stores it in ${agent}'s vault runtime/config item.`,
1911
+ ],
1912
+ });
1836
1913
  const key = (await promptSecret("Perplexity API key: ")).trim();
1837
1914
  if (!key)
1838
1915
  throw new Error("Perplexity API key cannot be blank");
1839
1916
  const progress = createHumanCommandProgress(deps, "connect perplexity");
1840
1917
  let stored;
1918
+ let verification;
1841
1919
  let reload;
1842
1920
  try {
1843
1921
  stored = await runCommandProgressPhase(progress, "saving Perplexity search", () => storeRuntimeConfigKey({
@@ -1848,6 +1926,23 @@ async function executeConnectPerplexity(agent, deps) {
1848
1926
  deps,
1849
1927
  onProgress: (message) => progress.updateDetail(message),
1850
1928
  }), () => "secret stored");
1929
+ progress.startPhase("verifying Perplexity search");
1930
+ verification = await (0, runtime_capability_check_1.verifyPerplexityCapability)(key);
1931
+ if (!verification.ok) {
1932
+ progress.failPhase("verifying Perplexity search", verification.summary);
1933
+ progress.end();
1934
+ const message = [
1935
+ `Perplexity key was saved for ${agent}, but the live check failed.`,
1936
+ `stored: ${stored.itemPath}`,
1937
+ `live check: ${verification.summary}`,
1938
+ "secret was not printed",
1939
+ "",
1940
+ `Next: rerun \`ouro connect perplexity --agent ${agent}\` with a working key.`,
1941
+ ].join("\n");
1942
+ deps.writeStdout(message);
1943
+ return message;
1944
+ }
1945
+ progress.completePhase("verifying Perplexity search", verification.summary);
1851
1946
  reload = await runCommandProgressPhase(progress, `applying change to running ${agent}`, () => applyRuntimeChangeToRunningAgent(agent, deps, (message) => progress.updateDetail(message)), (result) => result);
1852
1947
  }
1853
1948
  finally {
@@ -1870,16 +1965,44 @@ async function executeConnectEmbeddings(agent, deps) {
1870
1965
  throw new Error("SerpentGuide has no persistent runtime credentials. Connect embeddings on the hatchling agent instead.");
1871
1966
  }
1872
1967
  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"));
1968
+ writeConnectorIntro(deps, {
1969
+ title: "Connect Embeddings",
1970
+ subtitle: `${agent} gets portable note and memory search.`,
1971
+ summary: "Add one hidden API key, verify it live, and let semantic memory travel with this agent.",
1972
+ sections: [
1973
+ {
1974
+ title: "Unlocks",
1975
+ lines: [
1976
+ "Portable note search and memory retrieval.",
1977
+ ],
1978
+ },
1979
+ {
1980
+ title: "What you need",
1981
+ lines: [
1982
+ "One OpenAI embeddings API key.",
1983
+ "It stays hidden while you type.",
1984
+ ],
1985
+ },
1986
+ {
1987
+ title: "Where it lives",
1988
+ lines: [
1989
+ `${agent}'s vault runtime/config item.`,
1990
+ "It travels with the agent across machines.",
1991
+ ],
1992
+ },
1993
+ ],
1994
+ fallbackLines: [
1995
+ `Connect embeddings for ${agent}`,
1996
+ "The API key stays hidden while you type.",
1997
+ `Ouro stores it in ${agent}'s vault runtime/config item.`,
1998
+ ],
1999
+ });
1878
2000
  const key = (await promptSecret("OpenAI embeddings API key: ")).trim();
1879
2001
  if (!key)
1880
2002
  throw new Error("OpenAI embeddings API key cannot be blank");
1881
2003
  const progress = createHumanCommandProgress(deps, "connect embeddings");
1882
2004
  let stored;
2005
+ let verification;
1883
2006
  let reload;
1884
2007
  try {
1885
2008
  stored = await runCommandProgressPhase(progress, "saving memory embeddings", () => storeRuntimeConfigKey({
@@ -1890,6 +2013,23 @@ async function executeConnectEmbeddings(agent, deps) {
1890
2013
  deps,
1891
2014
  onProgress: (message) => progress.updateDetail(message),
1892
2015
  }), () => "secret stored");
2016
+ progress.startPhase("verifying memory embeddings");
2017
+ verification = await (0, runtime_capability_check_1.verifyEmbeddingsCapability)(key);
2018
+ if (!verification.ok) {
2019
+ progress.failPhase("verifying memory embeddings", verification.summary);
2020
+ progress.end();
2021
+ const message = [
2022
+ `Embeddings key was saved for ${agent}, but the live check failed.`,
2023
+ `stored: ${stored.itemPath}`,
2024
+ `live check: ${verification.summary}`,
2025
+ "secret was not printed",
2026
+ "",
2027
+ `Next: rerun \`ouro connect embeddings --agent ${agent}\` with a working key.`,
2028
+ ].join("\n");
2029
+ deps.writeStdout(message);
2030
+ return message;
2031
+ }
2032
+ progress.completePhase("verifying memory embeddings", verification.summary);
1893
2033
  reload = await runCommandProgressPhase(progress, `applying change to running ${agent}`, () => applyRuntimeChangeToRunningAgent(agent, deps, (message) => progress.updateDetail(message)), (result) => result);
1894
2034
  }
1895
2035
  finally {
@@ -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) {
@@ -0,0 +1,170 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.verifyPerplexityCapability = verifyPerplexityCapability;
4
+ exports.verifyEmbeddingsCapability = verifyEmbeddingsCapability;
5
+ const runtime_1 = require("../nerves/runtime");
6
+ function sanitizeCapabilityError(message) {
7
+ const statusMatch = message.match(/^(\d{3})\s/);
8
+ if (!statusMatch)
9
+ return message;
10
+ const status = statusMatch[1];
11
+ const body = message.slice(status.length).trim();
12
+ if (body.startsWith("<") || body.includes("<!DOCTYPE") || body.includes("<html")) {
13
+ return `HTTP ${status}`;
14
+ }
15
+ if (body.startsWith("{")) {
16
+ try {
17
+ const json = JSON.parse(body);
18
+ const inner = json?.error?.message;
19
+ if (typeof inner === "string" && inner && inner !== "Error") {
20
+ return `${status} ${inner}`;
21
+ }
22
+ }
23
+ catch {
24
+ // Keep the HTTP status fallback below.
25
+ }
26
+ return `HTTP ${status}`;
27
+ }
28
+ return message;
29
+ }
30
+ async function readHttpFailureSummary(response) {
31
+ let detail = `${response.status} ${response.statusText}`.trim();
32
+ try {
33
+ const json = await response.json();
34
+ if (typeof json.error === "string" && json.error.trim()) {
35
+ detail = `${response.status} ${json.error}`;
36
+ }
37
+ else if (typeof json.error === "object" && json.error !== null) {
38
+ const errObj = json.error;
39
+ if (typeof errObj.message === "string" && errObj.message.trim()) {
40
+ detail = `${response.status} ${errObj.message}`;
41
+ }
42
+ }
43
+ else if (typeof json.message === "string" && json.message.trim()) {
44
+ detail = `${response.status} ${json.message}`;
45
+ }
46
+ }
47
+ catch {
48
+ // Keep the HTTP status summary if the body is not JSON.
49
+ }
50
+ return sanitizeCapabilityError(detail);
51
+ }
52
+ function reportRuntimeCapabilityCheckStart(capability, url) {
53
+ (0, runtime_1.emitNervesEvent)({
54
+ component: "daemon",
55
+ event: "daemon.runtime_capability_check_start",
56
+ message: "starting runtime capability check",
57
+ meta: { capability, url },
58
+ });
59
+ }
60
+ function reportRuntimeCapabilityCheckEnd(capability, url) {
61
+ (0, runtime_1.emitNervesEvent)({
62
+ component: "daemon",
63
+ event: "daemon.runtime_capability_check_end",
64
+ message: "runtime capability check passed",
65
+ meta: { capability, url },
66
+ });
67
+ }
68
+ function reportRuntimeCapabilityCheckError(capability, url, summary) {
69
+ (0, runtime_1.emitNervesEvent)({
70
+ level: "warn",
71
+ component: "daemon",
72
+ event: "daemon.runtime_capability_check_error",
73
+ message: "runtime capability check failed",
74
+ meta: { capability, url, summary },
75
+ });
76
+ }
77
+ async function verifyPerplexityCapability(apiKey, fetchImpl = fetch) {
78
+ const url = "https://api.perplexity.ai/search";
79
+ reportRuntimeCapabilityCheckStart("perplexity-search", url);
80
+ try {
81
+ const response = await fetchImpl(url, {
82
+ method: "POST",
83
+ headers: {
84
+ Authorization: `Bearer ${apiKey}`,
85
+ "Content-Type": "application/json",
86
+ },
87
+ body: JSON.stringify({
88
+ query: "ping",
89
+ max_results: 1,
90
+ }),
91
+ });
92
+ if (!response.ok) {
93
+ const summary = await readHttpFailureSummary(response);
94
+ reportRuntimeCapabilityCheckError("perplexity-search", url, summary);
95
+ return {
96
+ ok: false,
97
+ summary,
98
+ };
99
+ }
100
+ const payload = await response.json();
101
+ if (!Array.isArray(payload.results) || payload.results.length === 0) {
102
+ const summary = "Perplexity returned no search results";
103
+ reportRuntimeCapabilityCheckError("perplexity-search", url, summary);
104
+ return {
105
+ ok: false,
106
+ summary,
107
+ };
108
+ }
109
+ reportRuntimeCapabilityCheckEnd("perplexity-search", url);
110
+ return {
111
+ ok: true,
112
+ summary: "live check passed",
113
+ };
114
+ }
115
+ catch (error) {
116
+ const summary = sanitizeCapabilityError(error instanceof Error ? error.message : String(error));
117
+ reportRuntimeCapabilityCheckError("perplexity-search", url, summary);
118
+ return {
119
+ ok: false,
120
+ summary,
121
+ };
122
+ }
123
+ }
124
+ async function verifyEmbeddingsCapability(apiKey, fetchImpl = fetch) {
125
+ const url = "https://api.openai.com/v1/embeddings";
126
+ reportRuntimeCapabilityCheckStart("memory-embeddings", url);
127
+ try {
128
+ const response = await fetchImpl(url, {
129
+ method: "POST",
130
+ headers: {
131
+ Authorization: `Bearer ${apiKey}`,
132
+ "Content-Type": "application/json",
133
+ },
134
+ body: JSON.stringify({
135
+ model: "text-embedding-3-small",
136
+ input: ["ping"],
137
+ }),
138
+ });
139
+ if (!response.ok) {
140
+ const summary = await readHttpFailureSummary(response);
141
+ reportRuntimeCapabilityCheckError("memory-embeddings", url, summary);
142
+ return {
143
+ ok: false,
144
+ summary,
145
+ };
146
+ }
147
+ const payload = await response.json();
148
+ if (!Array.isArray(payload.data) || payload.data.length !== 1 || !Array.isArray(payload.data[0]?.embedding) || payload.data[0].embedding.length === 0) {
149
+ const summary = "embeddings response missing expected vectors";
150
+ reportRuntimeCapabilityCheckError("memory-embeddings", url, summary);
151
+ return {
152
+ ok: false,
153
+ summary,
154
+ };
155
+ }
156
+ reportRuntimeCapabilityCheckEnd("memory-embeddings", url);
157
+ return {
158
+ ok: true,
159
+ summary: "live check passed",
160
+ };
161
+ }
162
+ catch (error) {
163
+ const summary = sanitizeCapabilityError(error instanceof Error ? error.message : String(error));
164
+ reportRuntimeCapabilityCheckError("memory-embeddings", url, summary);
165
+ return {
166
+ ok: false,
167
+ summary,
168
+ };
169
+ }
170
+ }