@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 response = await fetch(url, {
343
- method: "GET",
344
- signal: AbortSignal.timeout(channelConfig.requestTimeoutMs),
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 (!response.ok) {
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: response.status,
356
- reason: errorText || "unknown",
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(`BlueBubbles upstream health check failed (${response.status}): ${errorText || "unknown"}`);
362
+ throw new Error(result.detail);
360
363
  }
361
364
  (0, runtime_1.emitNervesEvent)({
362
365
  component: "senses",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.321",
3
+ "version": "0.1.0-alpha.322",
4
4
  "main": "dist/heart/daemon/ouro-entry.js",
5
5
  "bin": {
6
6
  "cli": "dist/heart/daemon/ouro-bot-entry.js",