@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.
- package/changelog.json +193 -184
- package/dist/heart/daemon/cli-exec.js +170 -30
- package/dist/heart/daemon/daemon-runtime-sync.js +8 -8
- package/dist/heart/daemon/startup-tui.js +20 -7
- package/dist/heart/daemon/up-progress.js +15 -3
- package/dist/heart/runtime-capability-check.js +170 -0
- package/package.json +4 -1
|
@@ -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?.("
|
|
570
|
-
deps.reportDaemonStartupPhase?.("waiting for
|
|
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?.("
|
|
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
|
|
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
|
|
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?.(
|
|
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
|
|
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?.(`
|
|
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?.(`
|
|
1564
|
+
onProgress?.(`waiting for ${agent} to come back\n- current worker state: ${worker.status}`);
|
|
1549
1565
|
}
|
|
1550
1566
|
}
|
|
1551
1567
|
else {
|
|
1552
|
-
onProgress?.(`
|
|
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?.(`
|
|
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
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
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
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
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
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
117
|
-
: `daemon already running (${deps.socketPath}; could not replace the
|
|
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
|
|
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
|
|
154
|
-
: `replaced
|
|
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}
|
|
97
|
-
: `${spinner}
|
|
96
|
+
? `${spinner} ${BOLD}starting background service${RESET} ${DIM}(${elapsedSec}s)${RESET}`
|
|
97
|
+
: `${spinner} starting background service (${elapsedSec}s)`);
|
|
98
98
|
if (latestEvent) {
|
|
99
|
-
|
|
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(
|
|
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 "
|
|
228
|
-
|
|
229
|
-
|
|
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
|
-
|
|
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
|
-
|
|
202
|
-
|
|
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
|
+
}
|