@ouro.bot/cli 0.1.0-alpha.320 → 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,20 @@
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
+ },
11
+ {
12
+ "version": "0.1.0-alpha.321",
13
+ "changes": [
14
+ "refactor(outlook): split `src/heart/outlook/outlook-read.ts` into focused reader modules for agent/machine state, sessions, runtime views, continuity views, and shared helpers while keeping the exported Outlook read API stable through a small composition root.",
15
+ "test(outlook): expand Outlook read verification with composition, continuity catch-path, runtime fallback, and session-activity regressions so the refactor stays covered under the full coverage and nerves-audit gates."
16
+ ]
17
+ },
4
18
  {
5
19
  "version": "0.1.0-alpha.320",
6
20
  "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) {