@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.
@@ -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 WRAPPER_SCRIPT = `#!/bin/sh
43
- # Check for dev mode — if dev-config.json exists, dispatch to the dev repo
44
- # Skip dev dispatch for "up" command (explicitly returns to production)
45
- DEV_CONFIG="$HOME/.ouro-cli/dev-config.json"
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
- writeFileSync(scriptPath, WRAPPER_SCRIPT, { mode: 0o755 });
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.727",
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.8",
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
  },