@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 +14 -0
- package/dist/heart/daemon/bluebubbles-health-diagnostics.js +122 -0
- package/dist/heart/daemon/cli-exec.js +1 -0
- package/dist/heart/daemon/doctor.js +80 -1
- package/dist/heart/outlook/outlook-read.js +26 -1593
- package/dist/heart/outlook/readers/agent-machine.js +355 -0
- package/dist/heart/outlook/readers/continuity-readers.js +332 -0
- package/dist/heart/outlook/readers/runtime-readers.js +660 -0
- package/dist/heart/outlook/readers/sessions.js +231 -0
- package/dist/heart/outlook/readers/shared.js +111 -0
- package/dist/senses/bluebubbles/client.js +12 -9
- package/package.json +1 -1
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) {
|