@ouro.bot/cli 0.1.0-alpha.321 → 0.1.0-alpha.322
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
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"_note": "This changelog is maintained as part of the PR/version-bump workflow. Agent-curated, not auto-generated. Agents read this file directly via read_file to understand what changed between versions.",
|
|
3
3
|
"versions": [
|
|
4
|
+
{
|
|
5
|
+
"version": "0.1.0-alpha.322",
|
|
6
|
+
"changes": [
|
|
7
|
+
"fix(bluebubbles): share BlueBubbles health diagnostics between runtime status and doctor so fetch, malformed URL, auth, and upstream server failures produce actionable repair guidance instead of a bare `fetch failed`.",
|
|
8
|
+
"fix(doctor): actively probe enabled BlueBubbles upstreams, validate `bluebubbles.serverUrl` and `bluebubbles.password`, and report exactly whether config, reachability, auth, or upstream health needs repair."
|
|
9
|
+
]
|
|
10
|
+
},
|
|
4
11
|
{
|
|
5
12
|
"version": "0.1.0-alpha.321",
|
|
6
13
|
"changes": [
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.stringifyBlueBubblesHealthError = stringifyBlueBubblesHealthError;
|
|
4
|
+
exports.redactBlueBubblesHealthDetailForNerves = redactBlueBubblesHealthDetailForNerves;
|
|
5
|
+
exports.formatBlueBubblesHealthcheckFailure = formatBlueBubblesHealthcheckFailure;
|
|
6
|
+
exports.probeBlueBubblesHealth = probeBlueBubblesHealth;
|
|
7
|
+
const runtime_1 = require("../../nerves/runtime");
|
|
8
|
+
const error_classification_1 = require("../providers/error-classification");
|
|
9
|
+
function buildBlueBubblesApiUrl(baseUrl, endpoint, password) {
|
|
10
|
+
const root = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
|
|
11
|
+
const url = new URL(endpoint.replace(/^\//, ""), root);
|
|
12
|
+
url.searchParams.set("password", password);
|
|
13
|
+
return url.toString();
|
|
14
|
+
}
|
|
15
|
+
function stringifyBlueBubblesHealthError(error) {
|
|
16
|
+
if (error instanceof Error) {
|
|
17
|
+
const message = error.message.trim();
|
|
18
|
+
if (message)
|
|
19
|
+
return message;
|
|
20
|
+
return error.name || "unknown";
|
|
21
|
+
}
|
|
22
|
+
const value = String(error).trim();
|
|
23
|
+
return value || "unknown";
|
|
24
|
+
}
|
|
25
|
+
function redactBlueBubblesHealthDetailForNerves(detail) {
|
|
26
|
+
return detail
|
|
27
|
+
.replace(/\bbluebubbles\.password\b/gi, "bluebubbles credential")
|
|
28
|
+
.replace(/\bpassword\b/gi, "credential");
|
|
29
|
+
}
|
|
30
|
+
function blueBubblesHealthStatus(error) {
|
|
31
|
+
return error instanceof Error && typeof error.status === "number"
|
|
32
|
+
? error.status
|
|
33
|
+
: null;
|
|
34
|
+
}
|
|
35
|
+
function blueBubblesHealthClassification(error) {
|
|
36
|
+
return error instanceof Error ? (0, error_classification_1.classifyHttpError)(error) : "unknown";
|
|
37
|
+
}
|
|
38
|
+
function formatBlueBubblesHealthcheckFailure(serverUrlInput, error) {
|
|
39
|
+
const serverUrl = serverUrlInput.trim() || "configured BlueBubbles server";
|
|
40
|
+
const rawReason = stringifyBlueBubblesHealthError(error);
|
|
41
|
+
const status = blueBubblesHealthStatus(error);
|
|
42
|
+
if (!(error instanceof Error)) {
|
|
43
|
+
return `BlueBubbles health check failed at ${serverUrl}. Check \`bluebubbles.serverUrl\`, confirm the BlueBubbles app/API is running, and inspect daemon logs. Raw error: ${rawReason}`;
|
|
44
|
+
}
|
|
45
|
+
switch (blueBubblesHealthClassification(error)) {
|
|
46
|
+
case "network-error":
|
|
47
|
+
return `Cannot reach BlueBubbles at ${serverUrl}. Check \`bluebubbles.serverUrl\`, confirm the BlueBubbles app/API is running, and verify this machine can reach it. Raw error: ${rawReason}`;
|
|
48
|
+
case "auth-failure":
|
|
49
|
+
return `BlueBubbles auth failed at ${serverUrl} (HTTP ${status}). Check \`bluebubbles.password\` in secrets.json and confirm the server accepts it. Raw error: ${rawReason}`;
|
|
50
|
+
case "server-error":
|
|
51
|
+
return `BlueBubbles upstream returned HTTP ${status} at ${serverUrl}. Check the BlueBubbles app/server logs and confirm the upstream API is healthy. Raw error: ${rawReason}`;
|
|
52
|
+
default:
|
|
53
|
+
return `BlueBubbles health check failed at ${serverUrl}${status === null ? "" : ` (HTTP ${status})`}. Check \`bluebubbles.serverUrl\`, the BlueBubbles server configuration, and daemon logs. Raw error: ${rawReason}`;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
async function probeBlueBubblesHealth(input) {
|
|
57
|
+
try {
|
|
58
|
+
const url = buildBlueBubblesApiUrl(input.serverUrl, "/api/v1/message/count", input.password);
|
|
59
|
+
const response = await input.fetchImpl(url, {
|
|
60
|
+
method: "GET",
|
|
61
|
+
signal: AbortSignal.timeout(input.requestTimeoutMs),
|
|
62
|
+
});
|
|
63
|
+
if (!response.ok) {
|
|
64
|
+
const errorText = await response.text().catch(() => "");
|
|
65
|
+
const error = new Error(errorText || "unknown");
|
|
66
|
+
error.status = response.status;
|
|
67
|
+
throw error;
|
|
68
|
+
}
|
|
69
|
+
(0, runtime_1.emitNervesEvent)({
|
|
70
|
+
component: "daemon",
|
|
71
|
+
event: "daemon.bluebubbles_health_probe_checked",
|
|
72
|
+
message: "checked bluebubbles upstream health",
|
|
73
|
+
meta: {
|
|
74
|
+
serverUrl: input.serverUrl,
|
|
75
|
+
ok: true,
|
|
76
|
+
status: response.status,
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
return {
|
|
80
|
+
ok: true,
|
|
81
|
+
detail: "upstream reachable",
|
|
82
|
+
reason: null,
|
|
83
|
+
status: response.status,
|
|
84
|
+
classification: null,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
const detail = formatBlueBubblesHealthcheckFailure(input.serverUrl, error);
|
|
89
|
+
const reason = stringifyBlueBubblesHealthError(error);
|
|
90
|
+
const status = blueBubblesHealthStatus(error);
|
|
91
|
+
const classification = blueBubblesHealthClassification(error);
|
|
92
|
+
(0, runtime_1.emitNervesEvent)({
|
|
93
|
+
level: "warn",
|
|
94
|
+
component: "daemon",
|
|
95
|
+
event: "daemon.bluebubbles_health_probe_checked",
|
|
96
|
+
message: "checked bluebubbles upstream health",
|
|
97
|
+
meta: {
|
|
98
|
+
serverUrl: input.serverUrl,
|
|
99
|
+
ok: false,
|
|
100
|
+
status,
|
|
101
|
+
reason,
|
|
102
|
+
classification,
|
|
103
|
+
detail: redactBlueBubblesHealthDetailForNerves(detail),
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
return {
|
|
107
|
+
ok: false,
|
|
108
|
+
detail,
|
|
109
|
+
reason,
|
|
110
|
+
status,
|
|
111
|
+
classification,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/* v8 ignore start -- module load observability event */
|
|
116
|
+
(0, runtime_1.emitNervesEvent)({
|
|
117
|
+
component: "daemon",
|
|
118
|
+
event: "daemon.bluebubbles_health_diagnostics_loaded",
|
|
119
|
+
message: "bluebubbles health diagnostics loaded",
|
|
120
|
+
meta: {},
|
|
121
|
+
});
|
|
122
|
+
/* v8 ignore stop */
|
|
@@ -2189,6 +2189,7 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
|
|
|
2189
2189
|
statSync: (p) => fs.statSync(p),
|
|
2190
2190
|
/* v8 ignore stop */
|
|
2191
2191
|
checkSocketAlive: deps.checkSocketAlive,
|
|
2192
|
+
fetchImpl: deps.fetchImpl ?? fetch,
|
|
2192
2193
|
socketPath: deps.socketPath,
|
|
2193
2194
|
bundlesRoot: deps.bundlesRoot ?? (0, identity_1.getAgentBundlesRoot)(),
|
|
2194
2195
|
secretsRoot: deps.secretsRoot ?? path.join(os.homedir(), ".agentsecrets"),
|
|
@@ -15,6 +15,8 @@ exports.checkSecurity = checkSecurity;
|
|
|
15
15
|
exports.checkDisk = checkDisk;
|
|
16
16
|
exports.runDoctorChecks = runDoctorChecks;
|
|
17
17
|
const runtime_1 = require("../../nerves/runtime");
|
|
18
|
+
const bluebubbles_health_diagnostics_1 = require("./bluebubbles-health-diagnostics");
|
|
19
|
+
const DEFAULT_BLUEBUBBLES_REQUEST_TIMEOUT_MS = 30_000;
|
|
18
20
|
// ── Category checkers ──
|
|
19
21
|
async function checkDaemon(deps) {
|
|
20
22
|
const checks = [];
|
|
@@ -47,6 +49,27 @@ function discoverAgents(deps) {
|
|
|
47
49
|
return [];
|
|
48
50
|
return deps.readdirSync(deps.bundlesRoot).filter((name) => name.endsWith(".ouro"));
|
|
49
51
|
}
|
|
52
|
+
function asRecord(value) {
|
|
53
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
54
|
+
? value
|
|
55
|
+
: null;
|
|
56
|
+
}
|
|
57
|
+
function textField(record, key) {
|
|
58
|
+
const value = record?.[key];
|
|
59
|
+
return typeof value === "string" ? value.trim() : "";
|
|
60
|
+
}
|
|
61
|
+
function numberField(record, key, fallback) {
|
|
62
|
+
const value = record?.[key];
|
|
63
|
+
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
64
|
+
}
|
|
65
|
+
function readJsonObject(deps, filePath) {
|
|
66
|
+
try {
|
|
67
|
+
return asRecord(JSON.parse(deps.readFileSync(filePath)));
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
50
73
|
function checkAgents(deps) {
|
|
51
74
|
const checks = [];
|
|
52
75
|
if (!deps.existsSync(deps.bundlesRoot)) {
|
|
@@ -105,10 +128,11 @@ function checkAgents(deps) {
|
|
|
105
128
|
}
|
|
106
129
|
return { name: "Agents", checks };
|
|
107
130
|
}
|
|
108
|
-
function checkSenses(deps) {
|
|
131
|
+
async function checkSenses(deps) {
|
|
109
132
|
const checks = [];
|
|
110
133
|
const agents = discoverAgents(deps);
|
|
111
134
|
for (const agentDir of agents) {
|
|
135
|
+
const agentName = agentDir.replace(/\.ouro$/, "");
|
|
112
136
|
const configPath = `${deps.bundlesRoot}/${agentDir}/agent.json`;
|
|
113
137
|
if (!deps.existsSync(configPath))
|
|
114
138
|
continue;
|
|
@@ -145,6 +169,61 @@ function checkSenses(deps) {
|
|
|
145
169
|
detail: senseObj.enabled ? "enabled" : "disabled",
|
|
146
170
|
});
|
|
147
171
|
}
|
|
172
|
+
if (sense === "bluebubbles" && senseObj.enabled === true) {
|
|
173
|
+
const secretsPath = `${deps.secretsRoot}/${agentName}/secrets.json`;
|
|
174
|
+
if (!deps.existsSync(secretsPath)) {
|
|
175
|
+
checks.push({
|
|
176
|
+
label: `${agentDir} bluebubbles config`,
|
|
177
|
+
status: "fail",
|
|
178
|
+
detail: "missing secrets.json",
|
|
179
|
+
});
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
const secrets = readJsonObject(deps, secretsPath);
|
|
183
|
+
if (!secrets) {
|
|
184
|
+
checks.push({
|
|
185
|
+
label: `${agentDir} bluebubbles config`,
|
|
186
|
+
status: "fail",
|
|
187
|
+
detail: "secrets.json unparseable",
|
|
188
|
+
});
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
const bluebubbles = asRecord(secrets.bluebubbles);
|
|
192
|
+
const bluebubblesChannel = asRecord(secrets.bluebubblesChannel);
|
|
193
|
+
const serverUrl = textField(bluebubbles, "serverUrl");
|
|
194
|
+
const password = textField(bluebubbles, "password");
|
|
195
|
+
const missing = [];
|
|
196
|
+
if (!serverUrl)
|
|
197
|
+
missing.push("bluebubbles.serverUrl");
|
|
198
|
+
if (!password)
|
|
199
|
+
missing.push("bluebubbles.password");
|
|
200
|
+
if (missing.length > 0) {
|
|
201
|
+
checks.push({
|
|
202
|
+
label: `${agentDir} bluebubbles config`,
|
|
203
|
+
status: "fail",
|
|
204
|
+
detail: `missing ${missing.join("/")}`,
|
|
205
|
+
});
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
checks.push({
|
|
209
|
+
label: `${agentDir} bluebubbles config`,
|
|
210
|
+
status: "pass",
|
|
211
|
+
detail: serverUrl,
|
|
212
|
+
});
|
|
213
|
+
if (deps.fetchImpl) {
|
|
214
|
+
const probe = await (0, bluebubbles_health_diagnostics_1.probeBlueBubblesHealth)({
|
|
215
|
+
serverUrl,
|
|
216
|
+
password,
|
|
217
|
+
requestTimeoutMs: numberField(bluebubblesChannel, "requestTimeoutMs", DEFAULT_BLUEBUBBLES_REQUEST_TIMEOUT_MS),
|
|
218
|
+
fetchImpl: deps.fetchImpl,
|
|
219
|
+
});
|
|
220
|
+
checks.push({
|
|
221
|
+
label: `${agentDir} bluebubbles upstream`,
|
|
222
|
+
status: probe.ok ? "pass" : "fail",
|
|
223
|
+
detail: probe.detail,
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
}
|
|
148
227
|
}
|
|
149
228
|
}
|
|
150
229
|
if (checks.length === 0) {
|
|
@@ -4,6 +4,7 @@ exports.createBlueBubblesClient = createBlueBubblesClient;
|
|
|
4
4
|
const node_crypto_1 = require("node:crypto");
|
|
5
5
|
const config_1 = require("../../heart/config");
|
|
6
6
|
const identity_1 = require("../../heart/identity");
|
|
7
|
+
const bluebubbles_health_diagnostics_1 = require("../../heart/daemon/bluebubbles-health-diagnostics");
|
|
7
8
|
const runtime_1 = require("../../nerves/runtime");
|
|
8
9
|
const minimax_1 = require("../../heart/providers/minimax");
|
|
9
10
|
const minimax_vlm_1 = require("../../heart/providers/minimax-vlm");
|
|
@@ -332,19 +333,19 @@ function createBlueBubblesClient(config = (0, config_1.getBlueBubblesConfig)(),
|
|
|
332
333
|
}
|
|
333
334
|
},
|
|
334
335
|
async checkHealth() {
|
|
335
|
-
const url = buildBlueBubblesApiUrl(config.serverUrl, "/api/v1/message/count", config.password);
|
|
336
336
|
(0, runtime_1.emitNervesEvent)({
|
|
337
337
|
component: "senses",
|
|
338
338
|
event: "senses.bluebubbles_healthcheck_start",
|
|
339
339
|
message: "probing bluebubbles upstream health",
|
|
340
340
|
meta: { serverUrl: config.serverUrl },
|
|
341
341
|
});
|
|
342
|
-
const
|
|
343
|
-
|
|
344
|
-
|
|
342
|
+
const result = await (0, bluebubbles_health_diagnostics_1.probeBlueBubblesHealth)({
|
|
343
|
+
serverUrl: config.serverUrl,
|
|
344
|
+
password: config.password,
|
|
345
|
+
requestTimeoutMs: channelConfig.requestTimeoutMs,
|
|
346
|
+
fetchImpl: fetch,
|
|
345
347
|
});
|
|
346
|
-
if (!
|
|
347
|
-
const errorText = await response.text().catch(() => "");
|
|
348
|
+
if (!result.ok) {
|
|
348
349
|
(0, runtime_1.emitNervesEvent)({
|
|
349
350
|
level: "warn",
|
|
350
351
|
component: "senses",
|
|
@@ -352,11 +353,13 @@ function createBlueBubblesClient(config = (0, config_1.getBlueBubblesConfig)(),
|
|
|
352
353
|
message: "bluebubbles upstream health probe failed",
|
|
353
354
|
meta: {
|
|
354
355
|
serverUrl: config.serverUrl,
|
|
355
|
-
status:
|
|
356
|
-
reason:
|
|
356
|
+
status: result.status,
|
|
357
|
+
reason: result.reason,
|
|
358
|
+
classification: result.classification,
|
|
359
|
+
detail: (0, bluebubbles_health_diagnostics_1.redactBlueBubblesHealthDetailForNerves)(result.detail),
|
|
357
360
|
},
|
|
358
361
|
});
|
|
359
|
-
throw new Error(
|
|
362
|
+
throw new Error(result.detail);
|
|
360
363
|
}
|
|
361
364
|
(0, runtime_1.emitNervesEvent)({
|
|
362
365
|
component: "senses",
|