@ouro.bot/cli 0.1.0-alpha.727 → 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 +9 -0
- package/dist/heart/config.js +1 -2
- 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/doctor.js +21 -1
- package/dist/heart/daemon/mcp-canary.js +160 -29
- package/dist/heart/daemon/socket-client.js +28 -19
- 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/senses/bluebubbles/index.js +16 -0
- package/dist/senses/bluebubbles/webhook-registration.js +228 -0
- package/package.json +4 -2
|
@@ -39,28 +39,20 @@ const fs = __importStar(require("fs"));
|
|
|
39
39
|
const os = __importStar(require("os"));
|
|
40
40
|
const path = __importStar(require("path"));
|
|
41
41
|
const runtime_1 = require("../../nerves/runtime");
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
if [ -f "$DEV_CONFIG" ] && [ "$1" != "up" ]; then
|
|
47
|
-
DEV_REPO=$(node -e "try{console.log(JSON.parse(require('fs').readFileSync('$DEV_CONFIG','utf-8')).repoPath)}catch{}" 2>/dev/null)
|
|
48
|
-
DEV_ENTRY="$DEV_REPO/dist/heart/daemon/ouro-entry.js"
|
|
49
|
-
if [ -n "$DEV_REPO" ] && [ -e "$DEV_ENTRY" ]; then
|
|
50
|
-
exec node "$DEV_ENTRY" "$@"
|
|
51
|
-
fi
|
|
52
|
-
fi
|
|
53
|
-
# Fall back to installed version
|
|
54
|
-
ENTRY="$HOME/.ouro-cli/CurrentVersion/node_modules/@ouro.bot/cli/dist/heart/daemon/ouro-entry.js"
|
|
55
|
-
if [ ! -e "$ENTRY" ]; then
|
|
56
|
-
echo "ouro not installed. Run: npx ouro.bot@latest" >&2
|
|
57
|
-
exit 1
|
|
58
|
-
fi
|
|
59
|
-
exec node "$ENTRY" "$@"
|
|
60
|
-
`;
|
|
61
|
-
function writeWrapperScript(scriptPath, mkdirSync, writeFileSync, chmodSync) {
|
|
42
|
+
const ouro_recovery_launcher_1 = require("./ouro-recovery-launcher");
|
|
43
|
+
const WRAPPER_SCRIPT = ouro_recovery_launcher_1.OURO_WRAPPER_SCRIPT;
|
|
44
|
+
function writeWrapperScript(scriptPath, mkdirSync, writeFileSync, chmodSync, renameSync, existsSync, readFileSync) {
|
|
45
|
+
const assets = (0, ouro_recovery_launcher_1.resolveOuroRecoveryLauncherAssets)();
|
|
62
46
|
mkdirSync(path.dirname(scriptPath), { recursive: true });
|
|
63
|
-
|
|
47
|
+
const launcherPath = path.join(path.dirname(scriptPath), "ouro-launcher.js");
|
|
48
|
+
const launcherTempPath = `${launcherPath}.next-${process.pid}`;
|
|
49
|
+
writeFileSync(launcherTempPath, assets.launcherScript, { mode: 0o755 });
|
|
50
|
+
chmodSync(launcherTempPath, 0o755);
|
|
51
|
+
renameSync(launcherTempPath, launcherPath);
|
|
52
|
+
if (existsSync(launcherPath) && readFileSync(launcherPath, "utf8") !== assets.launcherScript) {
|
|
53
|
+
throw new Error("failed to verify installed Ouro recovery launcher");
|
|
54
|
+
}
|
|
55
|
+
writeFileSync(scriptPath, assets.wrapperScript, { mode: 0o755 });
|
|
64
56
|
chmodSync(scriptPath, 0o755);
|
|
65
57
|
}
|
|
66
58
|
function detectShellProfile(homeDir, shell, platform) {
|
|
@@ -106,6 +98,16 @@ function isWrapperCurrent(scriptPath, existsSync, readFileSync) {
|
|
|
106
98
|
return false;
|
|
107
99
|
}
|
|
108
100
|
}
|
|
101
|
+
function isRecoveryLauncherCurrent(scriptPath, existsSync, readFileSync) {
|
|
102
|
+
if (!existsSync(scriptPath))
|
|
103
|
+
return false;
|
|
104
|
+
try {
|
|
105
|
+
return readFileSync(scriptPath, "utf-8") === ouro_recovery_launcher_1.OURO_RECOVERY_LAUNCHER_SCRIPT;
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
109
111
|
function isOwnedOuroLauncherPath(resolvedPath) {
|
|
110
112
|
const normalized = path.normalize(resolvedPath);
|
|
111
113
|
return (normalized.includes(`${path.sep}node_modules${path.sep}@ouro.bot${path.sep}cli${path.sep}`) ||
|
|
@@ -116,7 +118,8 @@ function isOwnedOuroLauncherContent(content) {
|
|
|
116
118
|
content.includes("ouro.bot@latest") ||
|
|
117
119
|
content.includes("ouro.bot@alpha") ||
|
|
118
120
|
content.includes('exec npx --yes ouro.bot "$@"') ||
|
|
119
|
-
content.includes("CurrentVersion/node_modules/@ouro.bot/cli")
|
|
121
|
+
content.includes("CurrentVersion/node_modules/@ouro.bot/cli") ||
|
|
122
|
+
content.includes(".ouro-cli/bin/ouro-launcher.js"));
|
|
120
123
|
}
|
|
121
124
|
function canRepairShadowedLauncher(shadowPath, existsSync, readFileSync, realpathSync) {
|
|
122
125
|
if (!existsSync(shadowPath))
|
|
@@ -236,6 +239,7 @@ function installOuroCommand(deps = {}) {
|
|
|
236
239
|
const realpathSync = deps.realpathSync ?? fs.realpathSync;
|
|
237
240
|
const lstatSync = deps.lstatSync ?? fs.lstatSync;
|
|
238
241
|
const unlinkSync = deps.unlinkSync ?? fs.unlinkSync;
|
|
242
|
+
const renameSync = deps.renameSync ?? fs.renameSync;
|
|
239
243
|
const envPath = deps.envPath ?? process.env.PATH ?? "";
|
|
240
244
|
const shell = deps.shell ?? process.env.SHELL;
|
|
241
245
|
/* v8 ignore stop */
|
|
@@ -254,9 +258,11 @@ function installOuroCommand(deps = {}) {
|
|
|
254
258
|
}
|
|
255
259
|
const binDir = path.join(homeDir, ".ouro-cli", "bin");
|
|
256
260
|
const scriptPath = path.join(binDir, "ouro");
|
|
261
|
+
const recoveryLauncherPath = path.join(binDir, "ouro-launcher.js");
|
|
257
262
|
const oldScriptPath = path.join(homeDir, ".local", "bin", "ouro");
|
|
258
263
|
const resolvePath = () => diagnoseOuroPath({ homeDir, envPath, existsSync, readFileSync });
|
|
259
|
-
const modernCurrent = isWrapperCurrent(scriptPath, existsSync, readFileSync)
|
|
264
|
+
const modernCurrent = isWrapperCurrent(scriptPath, existsSync, readFileSync)
|
|
265
|
+
&& isRecoveryLauncherCurrent(recoveryLauncherPath, existsSync, readFileSync);
|
|
260
266
|
const oldExists = existsSync(oldScriptPath);
|
|
261
267
|
const oldCurrent = oldExists && isWrapperCurrent(oldScriptPath, existsSync, readFileSync);
|
|
262
268
|
// ── Repair old ~/.local/bin/ouro launcher ──
|
|
@@ -332,7 +338,7 @@ function installOuroCommand(deps = {}) {
|
|
|
332
338
|
});
|
|
333
339
|
try {
|
|
334
340
|
if (!modernCurrent) {
|
|
335
|
-
writeWrapperScript(scriptPath, mkdirSync, writeFileSync, chmodSync);
|
|
341
|
+
writeWrapperScript(scriptPath, mkdirSync, writeFileSync, chmodSync, renameSync, existsSync, readFileSync);
|
|
336
342
|
}
|
|
337
343
|
}
|
|
338
344
|
catch (error) {
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.OURO_WRAPPER_SCRIPT = exports.OURO_RECOVERY_LAUNCHER_SCRIPT = void 0;
|
|
4
|
+
exports.resolveOuroRecoveryLauncherAssets = resolveOuroRecoveryLauncherAssets;
|
|
5
|
+
const runtime_1 = require("../../nerves/runtime");
|
|
6
|
+
exports.OURO_RECOVERY_LAUNCHER_SCRIPT = `#!/usr/bin/env node
|
|
7
|
+
"use strict";
|
|
8
|
+
const fs = require("fs");
|
|
9
|
+
const os = require("os");
|
|
10
|
+
const path = require("path");
|
|
11
|
+
const cp = require("child_process");
|
|
12
|
+
const home = path.join(os.homedir(), ".ouro-cli");
|
|
13
|
+
const current = path.join(home, "CurrentVersion");
|
|
14
|
+
const intentPath = path.join(home, "version-intent.json");
|
|
15
|
+
const entryRel = "node_modules/@ouro.bot/cli/dist/heart/daemon/ouro-entry.js";
|
|
16
|
+
let intent = null;
|
|
17
|
+
try {
|
|
18
|
+
intent = JSON.parse(fs.readFileSync(intentPath, "utf8"));
|
|
19
|
+
if (intent.schemaVersion !== 1 || !["pinned", "latest"].includes(intent.mode) || !intent.targetVersion) throw new Error("invalid fields");
|
|
20
|
+
} catch (error) {
|
|
21
|
+
if (error && error.code !== "ENOENT") {
|
|
22
|
+
process.stderr.write("invalid Ouro version intent: " + error.message + "\\n");
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
if (intent) {
|
|
27
|
+
const target = path.join(home, "versions", intent.targetVersion);
|
|
28
|
+
const targetEntry = path.join(target, entryRel);
|
|
29
|
+
if (!fs.existsSync(targetEntry)) {
|
|
30
|
+
process.stderr.write("Ouro version intent target is not installed: " + intent.targetVersion + "\\n");
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
let active = null;
|
|
34
|
+
try { active = path.basename(fs.readlinkSync(current)); } catch {}
|
|
35
|
+
if (active !== intent.targetVersion) {
|
|
36
|
+
const next = current + ".next-" + process.pid;
|
|
37
|
+
try { fs.unlinkSync(next); } catch {}
|
|
38
|
+
fs.symlinkSync(target, next);
|
|
39
|
+
fs.renameSync(next, current);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const entry = path.join(current, entryRel);
|
|
43
|
+
if (!fs.existsSync(entry)) {
|
|
44
|
+
process.stderr.write("ouro not installed. Run: npx ouro.bot@latest\\n");
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
const result = cp.spawnSync(process.execPath, [entry, ...process.argv.slice(2)], { stdio: "inherit" });
|
|
48
|
+
if (result.error) throw result.error;
|
|
49
|
+
if (result.signal) process.kill(process.pid, result.signal);
|
|
50
|
+
process.exit(result.status == null ? 1 : result.status);
|
|
51
|
+
`;
|
|
52
|
+
exports.OURO_WRAPPER_SCRIPT = `#!/bin/sh
|
|
53
|
+
# Check for dev mode — if dev-config.json exists, dispatch to the dev repo.
|
|
54
|
+
# Skip dev dispatch for "up" command (explicitly returns to production).
|
|
55
|
+
DEV_CONFIG="$HOME/.ouro-cli/dev-config.json"
|
|
56
|
+
if [ -f "$DEV_CONFIG" ] && [ "$1" != "up" ]; then
|
|
57
|
+
DEV_REPO=$(node -e "try{console.log(JSON.parse(require('fs').readFileSync('$DEV_CONFIG','utf-8')).repoPath)}catch{}" 2>/dev/null)
|
|
58
|
+
DEV_ENTRY="$DEV_REPO/dist/heart/daemon/ouro-entry.js"
|
|
59
|
+
if [ -n "$DEV_REPO" ] && [ -e "$DEV_ENTRY" ]; then
|
|
60
|
+
exec node "$DEV_ENTRY" "$@"
|
|
61
|
+
fi
|
|
62
|
+
fi
|
|
63
|
+
exec node "$HOME/.ouro-cli/bin/ouro-launcher.js" "$@"
|
|
64
|
+
`;
|
|
65
|
+
function resolveOuroRecoveryLauncherAssets() {
|
|
66
|
+
(0, runtime_1.emitNervesEvent)({
|
|
67
|
+
component: "daemon",
|
|
68
|
+
event: "daemon.ouro_recovery_launcher_assets_resolved",
|
|
69
|
+
message: "resolved stable CLI recovery launcher assets",
|
|
70
|
+
meta: { launcher: "ouro-launcher.js", wrapper: "ouro" },
|
|
71
|
+
});
|
|
72
|
+
return {
|
|
73
|
+
launcherScript: exports.OURO_RECOVERY_LAUNCHER_SCRIPT,
|
|
74
|
+
wrapperScript: exports.OURO_WRAPPER_SCRIPT,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.versionIntentPath = versionIntentPath;
|
|
37
|
+
exports.readVersionIntent = readVersionIntent;
|
|
38
|
+
exports.writeVersionIntent = writeVersionIntent;
|
|
39
|
+
const fs = __importStar(require("fs"));
|
|
40
|
+
const os = __importStar(require("os"));
|
|
41
|
+
const path = __importStar(require("path"));
|
|
42
|
+
const runtime_1 = require("../../nerves/runtime");
|
|
43
|
+
function versionIntentPath(homeDir = os.homedir()) {
|
|
44
|
+
return path.join(homeDir, ".ouro-cli", "version-intent.json");
|
|
45
|
+
}
|
|
46
|
+
function parseVersionIntent(raw) {
|
|
47
|
+
let value;
|
|
48
|
+
try {
|
|
49
|
+
value = JSON.parse(raw);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
throw new Error("invalid version intent: malformed JSON");
|
|
53
|
+
}
|
|
54
|
+
if (!value
|
|
55
|
+
|| typeof value !== "object"
|
|
56
|
+
|| value.schemaVersion !== 1
|
|
57
|
+
|| !["pinned", "latest"].includes(String(value.mode))
|
|
58
|
+
|| typeof value.targetVersion !== "string"
|
|
59
|
+
|| !value.targetVersion) {
|
|
60
|
+
throw new Error("invalid version intent: expected schemaVersion=1, mode, and targetVersion");
|
|
61
|
+
}
|
|
62
|
+
return value;
|
|
63
|
+
}
|
|
64
|
+
function readVersionIntent(deps = {}) {
|
|
65
|
+
const readFileSync = deps.readFileSync ?? fs.readFileSync;
|
|
66
|
+
try {
|
|
67
|
+
return parseVersionIntent(readFileSync(versionIntentPath(deps.homeDir), "utf8"));
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
if (error.code === "ENOENT")
|
|
71
|
+
return null;
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function writeVersionIntent(intent, deps = {}) {
|
|
76
|
+
const mkdirSync = deps.mkdirSync ?? fs.mkdirSync;
|
|
77
|
+
const writeFileSync = deps.writeFileSync ?? fs.writeFileSync;
|
|
78
|
+
const renameSync = deps.renameSync ?? fs.renameSync;
|
|
79
|
+
const unlinkSync = deps.unlinkSync ?? fs.unlinkSync;
|
|
80
|
+
const randomUUID = deps.randomUUID ?? (() => `${process.pid}-${Date.now()}`);
|
|
81
|
+
const destination = versionIntentPath(deps.homeDir);
|
|
82
|
+
const temporary = `${destination}.${randomUUID()}.tmp`;
|
|
83
|
+
(0, runtime_1.emitNervesEvent)({
|
|
84
|
+
component: "daemon",
|
|
85
|
+
event: "daemon.version_intent_write_start",
|
|
86
|
+
message: "writing CLI version intent",
|
|
87
|
+
meta: { mode: intent.mode, targetVersion: intent.targetVersion },
|
|
88
|
+
});
|
|
89
|
+
try {
|
|
90
|
+
mkdirSync(path.dirname(destination), { recursive: true });
|
|
91
|
+
writeFileSync(temporary, `${JSON.stringify(intent, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
|
|
92
|
+
renameSync(temporary, destination);
|
|
93
|
+
(0, runtime_1.emitNervesEvent)({
|
|
94
|
+
component: "daemon",
|
|
95
|
+
event: "daemon.version_intent_write_end",
|
|
96
|
+
message: "CLI version intent written",
|
|
97
|
+
meta: { mode: intent.mode, targetVersion: intent.targetVersion },
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
try {
|
|
102
|
+
unlinkSync(temporary);
|
|
103
|
+
}
|
|
104
|
+
catch { /* best-effort temporary cleanup */ }
|
|
105
|
+
(0, runtime_1.emitNervesEvent)({
|
|
106
|
+
component: "daemon",
|
|
107
|
+
event: "daemon.version_intent_write_error",
|
|
108
|
+
level: "error",
|
|
109
|
+
message: "failed to write CLI version intent",
|
|
110
|
+
meta: { mode: intent.mode, targetVersion: intent.targetVersion, error: error instanceof Error ? error.message : String(error) },
|
|
111
|
+
});
|
|
112
|
+
throw error;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -63,6 +63,7 @@ const node_crypto_1 = require("node:crypto");
|
|
|
63
63
|
const core_1 = require("../../heart/core");
|
|
64
64
|
const config_1 = require("../../heart/config");
|
|
65
65
|
const identity_1 = require("../../heart/identity");
|
|
66
|
+
const machine_identity_1 = require("../../heart/machine-identity");
|
|
66
67
|
const runtime_cwd_1 = require("../../heart/runtime-cwd");
|
|
67
68
|
const turn_coordinator_1 = require("../../heart/turn-coordinator");
|
|
68
69
|
const context_1 = require("../../mind/context");
|
|
@@ -76,6 +77,7 @@ const proactive_content_guard_1 = require("../proactive-content-guard");
|
|
|
76
77
|
const bluebubbles_meta_guard_1 = require("../bluebubbles-meta-guard");
|
|
77
78
|
const model_1 = require("./model");
|
|
78
79
|
const client_1 = require("./client");
|
|
80
|
+
const webhook_registration_1 = require("./webhook-registration");
|
|
79
81
|
const inbound_log_1 = require("./inbound-log");
|
|
80
82
|
const mutation_log_1 = require("./mutation-log");
|
|
81
83
|
const processed_log_1 = require("./processed-log");
|
|
@@ -3718,6 +3720,7 @@ function startBlueBubblesApp(deps = {}) {
|
|
|
3718
3720
|
let recoveryPassRunning = false;
|
|
3719
3721
|
let runtimeSyncRunning = false;
|
|
3720
3722
|
let recoveryDelayTimer = null;
|
|
3723
|
+
let webhookReconciler = null;
|
|
3721
3724
|
let closed = false;
|
|
3722
3725
|
function triggerRecoveryPass() {
|
|
3723
3726
|
/* v8 ignore next -- close clears both recovery timers; guard covers an already-queued host callback @preserve */
|
|
@@ -3791,6 +3794,8 @@ function startBlueBubblesApp(deps = {}) {
|
|
|
3791
3794
|
const recoveryTimer = setInterval(triggerRecoveryPass, BLUEBUBBLES_RECOVERY_PASS_INTERVAL_MS);
|
|
3792
3795
|
server.on?.("close", () => {
|
|
3793
3796
|
closed = true;
|
|
3797
|
+
webhookReconciler?.close();
|
|
3798
|
+
webhookReconciler = null;
|
|
3794
3799
|
lifecycleController.abort(new Error("bluebubbles_runtime_shutdown"));
|
|
3795
3800
|
clearInterval(runtimeTimer);
|
|
3796
3801
|
clearInterval(recoveryTimer);
|
|
@@ -3800,6 +3805,17 @@ function startBlueBubblesApp(deps = {}) {
|
|
|
3800
3805
|
}
|
|
3801
3806
|
});
|
|
3802
3807
|
server.listen(channelConfig.port, () => {
|
|
3808
|
+
const blueBubblesConfig = (0, config_1.getBlueBubblesConfig)();
|
|
3809
|
+
webhookReconciler = (0, webhook_registration_1.createBlueBubblesWebhookReconciler)({
|
|
3810
|
+
serverUrl: blueBubblesConfig.serverUrl,
|
|
3811
|
+
password: blueBubblesConfig.password,
|
|
3812
|
+
callbackPort: channelConfig.port,
|
|
3813
|
+
callbackPath: channelConfig.webhookPath,
|
|
3814
|
+
agentName: resolvedDeps.getAgentName(),
|
|
3815
|
+
machineId: (0, machine_identity_1.loadOrCreateMachineIdentity)().machineId,
|
|
3816
|
+
requestTimeoutMs: channelConfig.requestTimeoutMs,
|
|
3817
|
+
listenerReady: true,
|
|
3818
|
+
});
|
|
3803
3819
|
(0, runtime_1.emitNervesEvent)({
|
|
3804
3820
|
component: "channels",
|
|
3805
3821
|
event: "channel.app_started",
|
|
@@ -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
|
},
|