@ouro.bot/cli 0.1.0-alpha.726 → 0.1.0-alpha.728
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/README.md +14 -1
- package/assets/bluebubbles-host +264 -0
- package/changelog.json +18 -0
- package/dist/heart/config.js +1 -2
- package/dist/heart/core.js +181 -209
- package/dist/heart/daemon/bluebubbles-cli-integration.js +71 -0
- package/dist/heart/daemon/bluebubbles-host-protocol.js +433 -0
- package/dist/heart/daemon/bluebubbles-host.js +273 -0
- package/dist/heart/daemon/cli-defaults.js +8 -1
- package/dist/heart/daemon/cli-exec.js +239 -34
- package/dist/heart/daemon/cli-help.js +17 -6
- package/dist/heart/daemon/cli-parse.js +69 -1
- package/dist/heart/daemon/cli-render.js +7 -0
- package/dist/heart/daemon/daemon.js +79 -14
- package/dist/heart/daemon/doctor.js +21 -1
- package/dist/heart/daemon/mcp-canary.js +160 -29
- package/dist/heart/daemon/process-manager.js +100 -18
- package/dist/heart/daemon/socket-client.js +28 -19
- package/dist/heart/providers/anthropic.js +17 -12
- package/dist/heart/streaming.js +6 -6
- package/dist/heart/versioning/ouro-path-installer.js +30 -24
- package/dist/heart/versioning/ouro-recovery-launcher.js +76 -0
- package/dist/heart/versioning/version-intent.js +114 -0
- package/dist/mind/prompt-budget.js +104 -5
- package/dist/repertoire/tools.js +0 -10
- package/dist/senses/bluebubbles/client.js +43 -57
- package/dist/senses/bluebubbles/context-packet.js +104 -19
- package/dist/senses/bluebubbles/entry.js +14 -1
- package/dist/senses/bluebubbles/index.js +1398 -719
- package/dist/senses/bluebubbles/latest-turn.js +311 -0
- package/dist/senses/bluebubbles/outbound-state.js +4 -0
- package/dist/senses/bluebubbles/reaction-policy.js +4 -12
- package/dist/senses/bluebubbles/semantic-receipts.js +178 -10
- package/dist/senses/bluebubbles/webhook-registration.js +228 -0
- package/package.json +4 -2
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.BLUEBUBBLES_WEBHOOK_RECONCILE_INTERVAL_MS = void 0;
|
|
4
|
+
exports.blueBubblesWebhookOwnerToken = blueBubblesWebhookOwnerToken;
|
|
5
|
+
exports.buildBlueBubblesWebhookCallbackUrl = buildBlueBubblesWebhookCallbackUrl;
|
|
6
|
+
exports.sanitizeBlueBubblesWebhookText = sanitizeBlueBubblesWebhookText;
|
|
7
|
+
exports.inspectBlueBubblesWebhookRegistration = inspectBlueBubblesWebhookRegistration;
|
|
8
|
+
exports.reconcileBlueBubblesWebhookRegistration = reconcileBlueBubblesWebhookRegistration;
|
|
9
|
+
exports.createBlueBubblesWebhookReconciler = createBlueBubblesWebhookReconciler;
|
|
10
|
+
const crypto_1 = require("crypto");
|
|
11
|
+
const runtime_1 = require("../../nerves/runtime");
|
|
12
|
+
exports.BLUEBUBBLES_WEBHOOK_RECONCILE_INTERVAL_MS = 180_000;
|
|
13
|
+
function result(state, detail, counts = {}) {
|
|
14
|
+
return {
|
|
15
|
+
ok: state === "exact",
|
|
16
|
+
state,
|
|
17
|
+
changed: counts.changed ?? false,
|
|
18
|
+
ownedCount: counts.ownedCount ?? 0,
|
|
19
|
+
exactCount: counts.exactCount ?? 0,
|
|
20
|
+
detail,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
function blueBubblesWebhookOwnerToken(agentName, machineId) {
|
|
24
|
+
return `v1_${(0, crypto_1.createHash)("sha256").update(`ouro-bluebubbles-webhook\0${agentName}\0${machineId}`).digest("hex").slice(0, 32)}`;
|
|
25
|
+
}
|
|
26
|
+
function buildBlueBubblesWebhookCallbackUrl(input) {
|
|
27
|
+
const callback = new URL(`http://127.0.0.1:${input.callbackPort}`);
|
|
28
|
+
callback.pathname = input.callbackPath.startsWith("/") ? input.callbackPath : `/${input.callbackPath}`;
|
|
29
|
+
callback.searchParams.set("password", input.password);
|
|
30
|
+
callback.searchParams.set("ouroWebhook", blueBubblesWebhookOwnerToken(input.agentName, input.machineId));
|
|
31
|
+
return callback.toString();
|
|
32
|
+
}
|
|
33
|
+
function apiUrl(input, id) {
|
|
34
|
+
const base = `${input.serverUrl.replace(/\/+$/, "")}/api/v1/webhook${id === undefined ? "" : `/${id}`}`;
|
|
35
|
+
const url = new URL(base);
|
|
36
|
+
url.searchParams.set("password", input.password);
|
|
37
|
+
return url.toString();
|
|
38
|
+
}
|
|
39
|
+
function sanitizeBlueBubblesWebhookText(text, input) {
|
|
40
|
+
const owner = blueBubblesWebhookOwnerToken(input.agentName, input.machineId);
|
|
41
|
+
return text
|
|
42
|
+
.replaceAll(input.password, "[redacted]")
|
|
43
|
+
.replaceAll(owner, "[redacted]")
|
|
44
|
+
.replace(/(https?:\/\/[^\s?]+)\?[^\s]*/g, "$1?[redacted]");
|
|
45
|
+
}
|
|
46
|
+
function isWebhook(value) {
|
|
47
|
+
if (!value || typeof value !== "object")
|
|
48
|
+
return false;
|
|
49
|
+
const row = value;
|
|
50
|
+
return Number.isInteger(row.id)
|
|
51
|
+
&& typeof row.url === "string"
|
|
52
|
+
&& Array.isArray(row.events)
|
|
53
|
+
&& row.events.every((event) => typeof event === "string");
|
|
54
|
+
}
|
|
55
|
+
function ownedBy(urlText, owner) {
|
|
56
|
+
try {
|
|
57
|
+
return new URL(urlText).searchParams.get("ouroWebhook") === owner;
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function exactUnmarked(urlText, desiredUrl) {
|
|
64
|
+
try {
|
|
65
|
+
const candidate = new URL(urlText);
|
|
66
|
+
const desired = new URL(desiredUrl);
|
|
67
|
+
if (candidate.searchParams.has("ouroWebhook"))
|
|
68
|
+
return false;
|
|
69
|
+
desired.searchParams.delete("ouroWebhook");
|
|
70
|
+
return candidate.toString() === desired.toString();
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function exactHook(hook, desiredUrl) {
|
|
77
|
+
return hook.url === desiredUrl && hook.events.length === 1 && hook.events[0] === "*";
|
|
78
|
+
}
|
|
79
|
+
async function request(input, fetchImpl, method, id, body, signal) {
|
|
80
|
+
const timeoutSignal = AbortSignal.timeout(input.requestTimeoutMs);
|
|
81
|
+
return fetchImpl(apiUrl(input, id), {
|
|
82
|
+
method,
|
|
83
|
+
...(body === undefined ? {} : {
|
|
84
|
+
headers: { "content-type": "application/json" },
|
|
85
|
+
body: JSON.stringify(body),
|
|
86
|
+
}),
|
|
87
|
+
signal: signal ? AbortSignal.any([timeoutSignal, signal]) : timeoutSignal,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
async function readHooks(input, fetchImpl, signal) {
|
|
91
|
+
try {
|
|
92
|
+
const response = await request(input, fetchImpl, "GET", undefined, undefined, signal);
|
|
93
|
+
if (response.status === 401 || response.status === 403) {
|
|
94
|
+
return result("auth-failed", `BlueBubbles rejected webhook credentials (HTTP ${response.status})`);
|
|
95
|
+
}
|
|
96
|
+
if (!response.ok)
|
|
97
|
+
return result("api-unreachable", `BlueBubbles webhook API returned HTTP ${response.status}`);
|
|
98
|
+
let payload;
|
|
99
|
+
try {
|
|
100
|
+
payload = await response.json();
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return result("malformed", "BlueBubbles webhook API returned invalid JSON");
|
|
104
|
+
}
|
|
105
|
+
const data = payload && typeof payload === "object" ? payload.data : undefined;
|
|
106
|
+
if (!Array.isArray(data) || !data.every(isWebhook)) {
|
|
107
|
+
return result("malformed", "BlueBubbles webhook API returned an invalid webhook list");
|
|
108
|
+
}
|
|
109
|
+
return { hooks: data };
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
113
|
+
return result("api-unreachable", sanitizeBlueBubblesWebhookText(detail, input));
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
function inspectHooks(hooks, input) {
|
|
117
|
+
const desired = buildBlueBubblesWebhookCallbackUrl(input);
|
|
118
|
+
const owner = blueBubblesWebhookOwnerToken(input.agentName, input.machineId);
|
|
119
|
+
const owned = hooks.filter((hook) => ownedBy(hook.url, owner));
|
|
120
|
+
const exact = owned.filter((hook) => exactHook(hook, desired));
|
|
121
|
+
if (owned.length === 1 && exact.length === 1) {
|
|
122
|
+
return result("exact", "one exact Ouro-owned BlueBubbles webhook is registered", {
|
|
123
|
+
ownedCount: 1,
|
|
124
|
+
exactCount: 1,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
return result(owned.length === 0 ? "missing" : "drifted", owned.length === 0
|
|
128
|
+
? "the Ouro-owned BlueBubbles webhook is missing"
|
|
129
|
+
: "the Ouro-owned BlueBubbles webhook registration is drifted", {
|
|
130
|
+
ownedCount: owned.length,
|
|
131
|
+
exactCount: exact.length,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
async function inspectBlueBubblesWebhookRegistration(input, deps = {}) {
|
|
135
|
+
if (!input.listenerReady)
|
|
136
|
+
return result("listener-not-ready", "the local BlueBubbles listener is not ready");
|
|
137
|
+
const listed = await readHooks(input, deps.fetchImpl ?? fetch, deps.signal);
|
|
138
|
+
return "hooks" in listed ? inspectHooks(listed.hooks, input) : listed;
|
|
139
|
+
}
|
|
140
|
+
async function mutate(input, fetchImpl, method, id, signal) {
|
|
141
|
+
try {
|
|
142
|
+
const response = await request(input, fetchImpl, method, id, method === "POST" ? { url: buildBlueBubblesWebhookCallbackUrl(input), events: ["*"] } : undefined, signal);
|
|
143
|
+
if (!response.ok)
|
|
144
|
+
return { ok: false, detail: `HTTP ${response.status}` };
|
|
145
|
+
return { ok: true };
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
return { ok: false, detail: sanitizeBlueBubblesWebhookText(error instanceof Error ? error.message : String(error), input) };
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
async function reconcileBlueBubblesWebhookRegistration(input, deps = {}) {
|
|
152
|
+
if (!input.listenerReady)
|
|
153
|
+
return result("listener-not-ready", "the local BlueBubbles listener is not ready");
|
|
154
|
+
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
155
|
+
const listed = await readHooks(input, fetchImpl, deps.signal);
|
|
156
|
+
if (!("hooks" in listed))
|
|
157
|
+
return listed;
|
|
158
|
+
const desired = buildBlueBubblesWebhookCallbackUrl(input);
|
|
159
|
+
const owner = blueBubblesWebhookOwnerToken(input.agentName, input.machineId);
|
|
160
|
+
const owned = listed.hooks.filter((hook) => ownedBy(hook.url, owner));
|
|
161
|
+
const exact = owned.filter((hook) => exactHook(hook, desired));
|
|
162
|
+
const keep = exact[0];
|
|
163
|
+
const stale = owned.filter((hook) => hook !== keep);
|
|
164
|
+
const adopt = keep ? [] : listed.hooks.filter((hook) => exactUnmarked(hook.url, desired)).slice(0, 1);
|
|
165
|
+
let changed = false;
|
|
166
|
+
if (!keep) {
|
|
167
|
+
const created = await mutate(input, fetchImpl, "POST", undefined, deps.signal);
|
|
168
|
+
if (!created.ok) {
|
|
169
|
+
return result(owned.length === 0 ? "missing" : "drifted", `could not create the desired webhook: ${created.detail}`, {
|
|
170
|
+
ownedCount: owned.length,
|
|
171
|
+
exactCount: exact.length,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
changed = true;
|
|
175
|
+
}
|
|
176
|
+
for (const hook of [...stale, ...adopt]) {
|
|
177
|
+
const deleted = await mutate(input, fetchImpl, "DELETE", hook.id, deps.signal);
|
|
178
|
+
if (!deleted.ok) {
|
|
179
|
+
return result("drifted", `could not remove 1 stale owned registration: ${deleted.detail}`, {
|
|
180
|
+
ownedCount: owned.length,
|
|
181
|
+
exactCount: exact.length,
|
|
182
|
+
changed,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
changed = true;
|
|
186
|
+
}
|
|
187
|
+
if (!changed)
|
|
188
|
+
return inspectHooks(listed.hooks, input);
|
|
189
|
+
const verified = await readHooks(input, fetchImpl, deps.signal);
|
|
190
|
+
if (!("hooks" in verified))
|
|
191
|
+
return { ...verified, changed };
|
|
192
|
+
return { ...inspectHooks(verified.hooks, input), changed };
|
|
193
|
+
}
|
|
194
|
+
function createBlueBubblesWebhookReconciler(input, deps = {}) {
|
|
195
|
+
const setIntervalImpl = deps.setIntervalImpl ?? ((callback, intervalMs) => setInterval(callback, intervalMs));
|
|
196
|
+
const clearIntervalImpl = deps.clearIntervalImpl ?? ((timer) => clearInterval(timer));
|
|
197
|
+
let closed = false;
|
|
198
|
+
let active = null;
|
|
199
|
+
const abortController = new AbortController();
|
|
200
|
+
const reconcileNow = () => {
|
|
201
|
+
if (closed)
|
|
202
|
+
return Promise.resolve(result("listener-not-ready", "the BlueBubbles webhook reconciler is closed"));
|
|
203
|
+
if (active)
|
|
204
|
+
return active;
|
|
205
|
+
active = reconcileBlueBubblesWebhookRegistration(input, { ...deps, signal: abortController.signal }).finally(() => { active = null; });
|
|
206
|
+
return active;
|
|
207
|
+
};
|
|
208
|
+
const timer = setIntervalImpl(() => { void reconcileNow(); }, exports.BLUEBUBBLES_WEBHOOK_RECONCILE_INTERVAL_MS);
|
|
209
|
+
void reconcileNow().then((outcome) => {
|
|
210
|
+
(0, runtime_1.emitNervesEvent)({
|
|
211
|
+
component: "senses",
|
|
212
|
+
event: "senses.bluebubbles_webhook_reconciled",
|
|
213
|
+
level: outcome.ok ? "info" : "warn",
|
|
214
|
+
message: "reconciled BlueBubbles webhook registration",
|
|
215
|
+
meta: { state: outcome.state, changed: outcome.changed },
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
return {
|
|
219
|
+
reconcileNow,
|
|
220
|
+
close: () => {
|
|
221
|
+
if (closed)
|
|
222
|
+
return;
|
|
223
|
+
closed = true;
|
|
224
|
+
clearIntervalImpl(timer);
|
|
225
|
+
abortController.abort(new Error("BlueBubbles webhook reconciliation closed"));
|
|
226
|
+
},
|
|
227
|
+
};
|
|
228
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ouro.bot/cli",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.728",
|
|
4
4
|
"engines": {
|
|
5
5
|
"node": ">=22"
|
|
6
6
|
},
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
"test": "vitest run",
|
|
35
35
|
"test:integration": "npm run build && vitest run --config vitest.integration.config.ts",
|
|
36
36
|
"test:e2e:package": "npm run build && node scripts/package-e2e.cjs",
|
|
37
|
+
"test:e2e:ouro-bot-package": "node scripts/ouro-bot-package-e2e.cjs",
|
|
37
38
|
"test:e2e:real-smoke": "npm run build && node scripts/nightly-real-smoke.cjs",
|
|
38
39
|
"typecheck:mailbox-ui": "tsc --noEmit -p packages/mailbox-ui/tsconfig.json",
|
|
39
40
|
"test:mailbox-ui": "npm test --prefix packages/mailbox-ui",
|
|
@@ -67,7 +68,7 @@
|
|
|
67
68
|
"better-sqlite3": "13.0.2",
|
|
68
69
|
"fast-glob": "^3.3.3",
|
|
69
70
|
"ink": "^3.2.0",
|
|
70
|
-
"mailparser": "^3.9.
|
|
71
|
+
"mailparser": "^3.9.15",
|
|
71
72
|
"openai": "^6.27.0",
|
|
72
73
|
"react": "^17.0.2",
|
|
73
74
|
"semver": "^7.7.4",
|
|
@@ -93,6 +94,7 @@
|
|
|
93
94
|
"vitest": "^4.1.8"
|
|
94
95
|
},
|
|
95
96
|
"overrides": {
|
|
97
|
+
"deepmerge-ts": "^8.0.1",
|
|
96
98
|
"@microsoft/teams.apps": {
|
|
97
99
|
"@azure/msal-node": "^5.2.2"
|
|
98
100
|
},
|