@ouro.bot/cli 0.1.0-alpha.5 → 0.1.0-alpha.7
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/dist/heart/config.js +34 -0
- package/dist/heart/daemon/daemon-cli.js +88 -1
- package/dist/mind/friends/channel.js +8 -0
- package/dist/mind/friends/types.js +1 -1
- package/dist/mind/prompt.js +3 -0
- package/dist/repertoire/tools.js +3 -3
- package/dist/senses/bluebubbles-client.js +279 -0
- package/dist/senses/bluebubbles-entry.js +11 -0
- package/dist/senses/bluebubbles-model.js +253 -0
- package/dist/senses/bluebubbles-mutation-log.js +76 -0
- package/dist/senses/bluebubbles.js +332 -0
- package/package.json +2 -1
package/dist/heart/config.js
CHANGED
|
@@ -44,6 +44,8 @@ exports.getTeamsConfig = getTeamsConfig;
|
|
|
44
44
|
exports.getContextConfig = getContextConfig;
|
|
45
45
|
exports.getOAuthConfig = getOAuthConfig;
|
|
46
46
|
exports.getTeamsChannelConfig = getTeamsChannelConfig;
|
|
47
|
+
exports.getBlueBubblesConfig = getBlueBubblesConfig;
|
|
48
|
+
exports.getBlueBubblesChannelConfig = getBlueBubblesChannelConfig;
|
|
47
49
|
exports.getIntegrationsConfig = getIntegrationsConfig;
|
|
48
50
|
exports.getOpenAIEmbeddingsApiKey = getOpenAIEmbeddingsApiKey;
|
|
49
51
|
exports.getLogsDir = getLogsDir;
|
|
@@ -92,6 +94,16 @@ const DEFAULT_SECRETS_TEMPLATE = {
|
|
|
92
94
|
skipConfirmation: true,
|
|
93
95
|
port: 3978,
|
|
94
96
|
},
|
|
97
|
+
bluebubbles: {
|
|
98
|
+
serverUrl: "",
|
|
99
|
+
password: "",
|
|
100
|
+
accountId: "default",
|
|
101
|
+
},
|
|
102
|
+
bluebubblesChannel: {
|
|
103
|
+
port: 18790,
|
|
104
|
+
webhookPath: "/bluebubbles-webhook",
|
|
105
|
+
requestTimeoutMs: 30000,
|
|
106
|
+
},
|
|
95
107
|
integrations: {
|
|
96
108
|
perplexityApiKey: "",
|
|
97
109
|
openaiEmbeddingsApiKey: "",
|
|
@@ -109,6 +121,8 @@ function defaultRuntimeConfig() {
|
|
|
109
121
|
oauth: { ...DEFAULT_SECRETS_TEMPLATE.oauth },
|
|
110
122
|
context: { ...identity_1.DEFAULT_AGENT_CONTEXT },
|
|
111
123
|
teamsChannel: { ...DEFAULT_SECRETS_TEMPLATE.teamsChannel },
|
|
124
|
+
bluebubbles: { ...DEFAULT_SECRETS_TEMPLATE.bluebubbles },
|
|
125
|
+
bluebubblesChannel: { ...DEFAULT_SECRETS_TEMPLATE.bluebubblesChannel },
|
|
112
126
|
integrations: { ...DEFAULT_SECRETS_TEMPLATE.integrations },
|
|
113
127
|
};
|
|
114
128
|
}
|
|
@@ -273,6 +287,26 @@ function getTeamsChannelConfig() {
|
|
|
273
287
|
const { skipConfirmation, flushIntervalMs, port } = config.teamsChannel;
|
|
274
288
|
return { skipConfirmation, flushIntervalMs, port };
|
|
275
289
|
}
|
|
290
|
+
function getBlueBubblesConfig() {
|
|
291
|
+
const config = loadConfig();
|
|
292
|
+
const { serverUrl, password, accountId } = config.bluebubbles;
|
|
293
|
+
if (!serverUrl.trim()) {
|
|
294
|
+
throw new Error("bluebubbles.serverUrl is required in secrets.json to run the BlueBubbles sense.");
|
|
295
|
+
}
|
|
296
|
+
if (!password.trim()) {
|
|
297
|
+
throw new Error("bluebubbles.password is required in secrets.json to run the BlueBubbles sense.");
|
|
298
|
+
}
|
|
299
|
+
return {
|
|
300
|
+
serverUrl: serverUrl.trim(),
|
|
301
|
+
password: password.trim(),
|
|
302
|
+
accountId: accountId.trim() || "default",
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
function getBlueBubblesChannelConfig() {
|
|
306
|
+
const config = loadConfig();
|
|
307
|
+
const { port, webhookPath, requestTimeoutMs } = config.bluebubblesChannel;
|
|
308
|
+
return { port, webhookPath, requestTimeoutMs };
|
|
309
|
+
}
|
|
276
310
|
function getIntegrationsConfig() {
|
|
277
311
|
const config = loadConfig();
|
|
278
312
|
return { ...config.integrations };
|
|
@@ -49,6 +49,7 @@ const types_1 = require("../../mind/friends/types");
|
|
|
49
49
|
const ouro_uti_1 = require("./ouro-uti");
|
|
50
50
|
const subagent_installer_1 = require("./subagent-installer");
|
|
51
51
|
const hatch_flow_1 = require("./hatch-flow");
|
|
52
|
+
const specialist_orchestrator_1 = require("./specialist-orchestrator");
|
|
52
53
|
async function ensureDaemonRunning(deps) {
|
|
53
54
|
const alive = await deps.checkSocketAlive(deps.socketPath);
|
|
54
55
|
if (alive) {
|
|
@@ -457,6 +458,65 @@ async function defaultLinkFriendIdentity(command) {
|
|
|
457
458
|
});
|
|
458
459
|
return `linked ${command.provider}:${command.externalId} to ${command.friendId}`;
|
|
459
460
|
}
|
|
461
|
+
/* v8 ignore next 49 -- integration: interactive terminal specialist session @preserve */
|
|
462
|
+
async function defaultRunAdoptionSpecialist() {
|
|
463
|
+
const readline = await Promise.resolve().then(() => __importStar(require("readline/promises")));
|
|
464
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
465
|
+
const prompt = async (q) => {
|
|
466
|
+
const answer = await rl.question(q);
|
|
467
|
+
return answer.trim();
|
|
468
|
+
};
|
|
469
|
+
try {
|
|
470
|
+
const humanName = await prompt("Your name: ");
|
|
471
|
+
const providerRaw = await prompt("Provider (azure|anthropic|minimax|openai-codex): ");
|
|
472
|
+
if (!humanName || !isAgentProvider(providerRaw)) {
|
|
473
|
+
process.stdout.write("Invalid input. Run `ouro hatch` to try again.\n");
|
|
474
|
+
return null;
|
|
475
|
+
}
|
|
476
|
+
const credentials = {};
|
|
477
|
+
if (providerRaw === "anthropic")
|
|
478
|
+
credentials.setupToken = await prompt("Anthropic API key: ");
|
|
479
|
+
if (providerRaw === "openai-codex")
|
|
480
|
+
credentials.oauthAccessToken = await prompt("OpenAI Codex OAuth token: ");
|
|
481
|
+
if (providerRaw === "minimax")
|
|
482
|
+
credentials.apiKey = await prompt("MiniMax API key: ");
|
|
483
|
+
if (providerRaw === "azure") {
|
|
484
|
+
credentials.apiKey = await prompt("Azure API key: ");
|
|
485
|
+
credentials.endpoint = await prompt("Azure endpoint: ");
|
|
486
|
+
credentials.deployment = await prompt("Azure deployment: ");
|
|
487
|
+
}
|
|
488
|
+
rl.close();
|
|
489
|
+
// Locate the bundled AdoptionSpecialist.ouro shipped with the npm package
|
|
490
|
+
const bundleSourceDir = path.resolve(__dirname, "..", "..", "..", "AdoptionSpecialist.ouro");
|
|
491
|
+
const bundlesRoot = (0, identity_1.getAgentBundlesRoot)();
|
|
492
|
+
const secretsRoot = path.join(os.homedir(), ".agentsecrets");
|
|
493
|
+
return await (0, specialist_orchestrator_1.runAdoptionSpecialist)({
|
|
494
|
+
bundleSourceDir,
|
|
495
|
+
bundlesRoot,
|
|
496
|
+
secretsRoot,
|
|
497
|
+
provider: providerRaw,
|
|
498
|
+
credentials,
|
|
499
|
+
humanName,
|
|
500
|
+
createReadline: () => {
|
|
501
|
+
const rl2 = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
502
|
+
return { question: (q) => rl2.question(q), close: () => rl2.close() };
|
|
503
|
+
},
|
|
504
|
+
callbacks: {
|
|
505
|
+
onModelStart: () => { },
|
|
506
|
+
onModelStreamStart: () => { },
|
|
507
|
+
onTextChunk: (text) => process.stdout.write(text),
|
|
508
|
+
onReasoningChunk: () => { },
|
|
509
|
+
onToolStart: () => { },
|
|
510
|
+
onToolEnd: () => { },
|
|
511
|
+
onError: (err) => process.stderr.write(`error: ${err.message}\n`),
|
|
512
|
+
},
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
catch {
|
|
516
|
+
rl.close();
|
|
517
|
+
return null;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
460
520
|
function createDefaultOuroCliDeps(socketPath = "/tmp/ouroboros-daemon.sock") {
|
|
461
521
|
return {
|
|
462
522
|
socketPath,
|
|
@@ -471,6 +531,7 @@ function createDefaultOuroCliDeps(socketPath = "/tmp/ouroboros-daemon.sock") {
|
|
|
471
531
|
listDiscoveredAgents: defaultListDiscoveredAgents,
|
|
472
532
|
runHatchFlow: hatch_flow_1.runHatchFlow,
|
|
473
533
|
promptInput: defaultPromptInput,
|
|
534
|
+
runAdoptionSpecialist: defaultRunAdoptionSpecialist,
|
|
474
535
|
registerOuroBundleType: ouro_uti_1.registerOuroBundleUti,
|
|
475
536
|
/* v8 ignore next 3 -- integration: launches interactive CLI session @preserve */
|
|
476
537
|
startChat: async (agentName) => {
|
|
@@ -648,6 +709,32 @@ async function runOuroCli(args, deps = createDefaultOuroCliDeps()) {
|
|
|
648
709
|
return message;
|
|
649
710
|
}
|
|
650
711
|
if (command.kind === "hatch.start") {
|
|
712
|
+
// Route through adoption specialist when no explicit hatch args were provided
|
|
713
|
+
const hasExplicitHatchArgs = !!(command.agentName || command.humanName || command.provider || command.credentials);
|
|
714
|
+
if (deps.runAdoptionSpecialist && !hasExplicitHatchArgs) {
|
|
715
|
+
const hatchlingName = await deps.runAdoptionSpecialist();
|
|
716
|
+
if (!hatchlingName) {
|
|
717
|
+
return "";
|
|
718
|
+
}
|
|
719
|
+
try {
|
|
720
|
+
await deps.installSubagents();
|
|
721
|
+
}
|
|
722
|
+
catch (error) {
|
|
723
|
+
(0, runtime_1.emitNervesEvent)({
|
|
724
|
+
level: "warn",
|
|
725
|
+
component: "daemon",
|
|
726
|
+
event: "daemon.subagent_install_error",
|
|
727
|
+
message: "subagent auto-install failed",
|
|
728
|
+
meta: { error: error instanceof Error ? error.message : /* v8 ignore next -- defensive: non-Error catch branch @preserve */ String(error) },
|
|
729
|
+
});
|
|
730
|
+
}
|
|
731
|
+
await registerOuroBundleTypeNonBlocking(deps);
|
|
732
|
+
await ensureDaemonRunning(deps);
|
|
733
|
+
if (deps.startChat) {
|
|
734
|
+
await deps.startChat(hatchlingName);
|
|
735
|
+
}
|
|
736
|
+
return "";
|
|
737
|
+
}
|
|
651
738
|
const hatchRunner = deps.runHatchFlow;
|
|
652
739
|
if (!hatchRunner) {
|
|
653
740
|
const response = await deps.sendCommand(deps.socketPath, { kind: "hatch.start" });
|
|
@@ -666,7 +753,7 @@ async function runOuroCli(args, deps = createDefaultOuroCliDeps()) {
|
|
|
666
753
|
component: "daemon",
|
|
667
754
|
event: "daemon.subagent_install_error",
|
|
668
755
|
message: "subagent auto-install failed",
|
|
669
|
-
meta: { error: error instanceof Error ? error.message : String(error) },
|
|
756
|
+
meta: { error: error instanceof Error ? error.message : /* v8 ignore next -- defensive: non-Error catch branch @preserve */ String(error) },
|
|
670
757
|
});
|
|
671
758
|
}
|
|
672
759
|
await registerOuroBundleTypeNonBlocking(deps);
|
|
@@ -21,6 +21,14 @@ const CHANNEL_CAPABILITIES = {
|
|
|
21
21
|
supportsRichCards: true,
|
|
22
22
|
maxMessageLength: Infinity,
|
|
23
23
|
},
|
|
24
|
+
bluebubbles: {
|
|
25
|
+
channel: "bluebubbles",
|
|
26
|
+
availableIntegrations: [],
|
|
27
|
+
supportsMarkdown: false,
|
|
28
|
+
supportsStreaming: false,
|
|
29
|
+
supportsRichCards: false,
|
|
30
|
+
maxMessageLength: Infinity,
|
|
31
|
+
},
|
|
24
32
|
};
|
|
25
33
|
const DEFAULT_CAPABILITIES = {
|
|
26
34
|
channel: "cli",
|
|
@@ -5,7 +5,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
5
5
|
exports.isIdentityProvider = isIdentityProvider;
|
|
6
6
|
exports.isIntegration = isIntegration;
|
|
7
7
|
const runtime_1 = require("../../nerves/runtime");
|
|
8
|
-
const IDENTITY_PROVIDERS = new Set(["aad", "local", "teams-conversation"]);
|
|
8
|
+
const IDENTITY_PROVIDERS = new Set(["aad", "local", "teams-conversation", "imessage-handle"]);
|
|
9
9
|
function isIdentityProvider(value) {
|
|
10
10
|
(0, runtime_1.emitNervesEvent)({
|
|
11
11
|
component: "friends",
|
package/dist/mind/prompt.js
CHANGED
|
@@ -198,6 +198,9 @@ function runtimeInfoSection(channel) {
|
|
|
198
198
|
if (channel === "cli") {
|
|
199
199
|
lines.push("i introduce myself on boot with a fun random greeting.");
|
|
200
200
|
}
|
|
201
|
+
else if (channel === "bluebubbles") {
|
|
202
|
+
lines.push("i am responding in iMessage through BlueBubbles. i keep replies short and phone-native. i do not use markdown. i do not introduce myself on boot.");
|
|
203
|
+
}
|
|
201
204
|
else {
|
|
202
205
|
lines.push("i am responding in Microsoft Teams. i keep responses concise. i use markdown formatting. i do not introduce myself on boot.");
|
|
203
206
|
}
|
package/dist/repertoire/tools.js
CHANGED
|
@@ -20,7 +20,7 @@ Object.defineProperty(exports, "teamsTools", { enumerable: true, get: function (
|
|
|
20
20
|
const allDefinitions = [...tools_base_1.baseToolDefinitions, ...tools_teams_1.teamsToolDefinitions, ...ado_semantic_1.adoSemanticToolDefinitions, ...tools_github_1.githubToolDefinitions];
|
|
21
21
|
const REMOTE_BLOCKED_LOCAL_TOOLS = new Set(["shell", "read_file", "write_file", "git_commit", "gh_cli"]);
|
|
22
22
|
function baseToolsForCapabilities(capabilities) {
|
|
23
|
-
const isRemoteChannel = capabilities?.channel === "teams";
|
|
23
|
+
const isRemoteChannel = capabilities?.channel === "teams" || capabilities?.channel === "bluebubbles";
|
|
24
24
|
if (!isRemoteChannel)
|
|
25
25
|
return tools_base_1.tools;
|
|
26
26
|
return tools_base_1.tools.filter((tool) => !REMOTE_BLOCKED_LOCAL_TOOLS.has(tool.function.name));
|
|
@@ -87,7 +87,7 @@ async function execTool(name, args, ctx) {
|
|
|
87
87
|
});
|
|
88
88
|
return `unknown: ${name}`;
|
|
89
89
|
}
|
|
90
|
-
const isRemoteChannel = ctx?.context?.channel?.channel === "teams";
|
|
90
|
+
const isRemoteChannel = ctx?.context?.channel?.channel === "teams" || ctx?.context?.channel?.channel === "bluebubbles";
|
|
91
91
|
if (isRemoteChannel && REMOTE_BLOCKED_LOCAL_TOOLS.has(name)) {
|
|
92
92
|
const message = "I can't do that from here because I'm talking to multiple people in a shared remote channel, and local shell/file/git/gh operations could let conversations interfere with each other. Ask me for a remote-safe alternative (Graph/ADO/web), or run that operation from CLI.";
|
|
93
93
|
(0, runtime_1.emitNervesEvent)({
|
|
@@ -95,7 +95,7 @@ async function execTool(name, args, ctx) {
|
|
|
95
95
|
event: "tool.error",
|
|
96
96
|
component: "tools",
|
|
97
97
|
message: "blocked local tool in remote channel",
|
|
98
|
-
meta: { name, channel:
|
|
98
|
+
meta: { name, channel: ctx?.context?.channel?.channel },
|
|
99
99
|
});
|
|
100
100
|
return message;
|
|
101
101
|
}
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createBlueBubblesClient = createBlueBubblesClient;
|
|
4
|
+
const node_crypto_1 = require("node:crypto");
|
|
5
|
+
const config_1 = require("../heart/config");
|
|
6
|
+
const runtime_1 = require("../nerves/runtime");
|
|
7
|
+
const bluebubbles_model_1 = require("./bluebubbles-model");
|
|
8
|
+
function buildBlueBubblesApiUrl(baseUrl, endpoint, password) {
|
|
9
|
+
const root = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
|
|
10
|
+
const url = new URL(endpoint.replace(/^\//, ""), root);
|
|
11
|
+
url.searchParams.set("password", password);
|
|
12
|
+
return url.toString();
|
|
13
|
+
}
|
|
14
|
+
function asRecord(value) {
|
|
15
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
16
|
+
? value
|
|
17
|
+
: null;
|
|
18
|
+
}
|
|
19
|
+
function extractMessageGuid(payload) {
|
|
20
|
+
if (!payload || typeof payload !== "object")
|
|
21
|
+
return undefined;
|
|
22
|
+
const record = payload;
|
|
23
|
+
const data = record.data && typeof record.data === "object" && !Array.isArray(record.data)
|
|
24
|
+
? record.data
|
|
25
|
+
: null;
|
|
26
|
+
const candidates = [
|
|
27
|
+
record.messageGuid,
|
|
28
|
+
record.messageId,
|
|
29
|
+
record.guid,
|
|
30
|
+
data?.messageGuid,
|
|
31
|
+
data?.messageId,
|
|
32
|
+
data?.guid,
|
|
33
|
+
];
|
|
34
|
+
for (const candidate of candidates) {
|
|
35
|
+
if (typeof candidate === "string" && candidate.trim()) {
|
|
36
|
+
return candidate.trim();
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
async function parseJsonBody(response) {
|
|
42
|
+
const raw = await response.text();
|
|
43
|
+
if (!raw.trim())
|
|
44
|
+
return null;
|
|
45
|
+
try {
|
|
46
|
+
return JSON.parse(raw);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function buildRepairUrl(baseUrl, messageGuid, password) {
|
|
53
|
+
const url = buildBlueBubblesApiUrl(baseUrl, `/api/v1/message/${encodeURIComponent(messageGuid)}`, password);
|
|
54
|
+
const parsed = new URL(url);
|
|
55
|
+
parsed.searchParams.set("with", "attachments,payloadData,chats,messageSummaryInfo");
|
|
56
|
+
return parsed.toString();
|
|
57
|
+
}
|
|
58
|
+
function collectPreviewStrings(value, out, depth = 0) {
|
|
59
|
+
if (depth > 4 || out.length >= 4)
|
|
60
|
+
return;
|
|
61
|
+
if (typeof value === "string") {
|
|
62
|
+
const trimmed = value.trim();
|
|
63
|
+
if (trimmed)
|
|
64
|
+
out.push(trimmed);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if (Array.isArray(value)) {
|
|
68
|
+
for (const entry of value)
|
|
69
|
+
collectPreviewStrings(entry, out, depth + 1);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const record = asRecord(value);
|
|
73
|
+
if (!record)
|
|
74
|
+
return;
|
|
75
|
+
const preferredKeys = ["title", "summary", "subtitle", "previewText", "siteName", "host", "url"];
|
|
76
|
+
for (const key of preferredKeys) {
|
|
77
|
+
if (out.length >= 4)
|
|
78
|
+
break;
|
|
79
|
+
collectPreviewStrings(record[key], out, depth + 1);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function extractLinkPreviewText(data) {
|
|
83
|
+
const values = [];
|
|
84
|
+
collectPreviewStrings(data.payloadData, values);
|
|
85
|
+
collectPreviewStrings(data.messageSummaryInfo, values);
|
|
86
|
+
const unique = [...new Set(values.map((value) => value.trim()).filter(Boolean))];
|
|
87
|
+
if (unique.length === 0)
|
|
88
|
+
return undefined;
|
|
89
|
+
return unique.slice(0, 2).join(" — ");
|
|
90
|
+
}
|
|
91
|
+
function applyRepairNotice(event, notice) {
|
|
92
|
+
return {
|
|
93
|
+
...event,
|
|
94
|
+
requiresRepair: false,
|
|
95
|
+
repairNotice: notice,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
function hydrateTextForAgent(event, rawData) {
|
|
99
|
+
if (event.kind !== "message") {
|
|
100
|
+
return { ...event, requiresRepair: false };
|
|
101
|
+
}
|
|
102
|
+
if (event.balloonBundleId !== "com.apple.messages.URLBalloonProvider") {
|
|
103
|
+
return { ...event, requiresRepair: false };
|
|
104
|
+
}
|
|
105
|
+
const previewText = extractLinkPreviewText(rawData);
|
|
106
|
+
if (!previewText) {
|
|
107
|
+
return { ...event, requiresRepair: false };
|
|
108
|
+
}
|
|
109
|
+
const base = event.text.trim();
|
|
110
|
+
const textForAgent = base
|
|
111
|
+
? `${base}\n[link preview: ${previewText}]`
|
|
112
|
+
: `[link preview: ${previewText}]`;
|
|
113
|
+
return {
|
|
114
|
+
...event,
|
|
115
|
+
textForAgent,
|
|
116
|
+
requiresRepair: false,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
function extractRepairData(payload) {
|
|
120
|
+
const record = asRecord(payload);
|
|
121
|
+
return asRecord(record?.data) ?? record;
|
|
122
|
+
}
|
|
123
|
+
function createBlueBubblesClient(config = (0, config_1.getBlueBubblesConfig)(), channelConfig = (0, config_1.getBlueBubblesChannelConfig)()) {
|
|
124
|
+
return {
|
|
125
|
+
async sendText(params) {
|
|
126
|
+
const trimmedText = params.text.trim();
|
|
127
|
+
if (!trimmedText) {
|
|
128
|
+
throw new Error("BlueBubbles send requires non-empty text.");
|
|
129
|
+
}
|
|
130
|
+
if (!params.chat.chatGuid) {
|
|
131
|
+
throw new Error("BlueBubbles send currently requires chat.chatGuid from the inbound event.");
|
|
132
|
+
}
|
|
133
|
+
const url = buildBlueBubblesApiUrl(config.serverUrl, "/api/v1/message/text", config.password);
|
|
134
|
+
const body = {
|
|
135
|
+
chatGuid: params.chat.chatGuid,
|
|
136
|
+
tempGuid: (0, node_crypto_1.randomUUID)(),
|
|
137
|
+
message: trimmedText,
|
|
138
|
+
};
|
|
139
|
+
if (params.replyToMessageGuid?.trim()) {
|
|
140
|
+
body.method = "private-api";
|
|
141
|
+
body.selectedMessageGuid = params.replyToMessageGuid.trim();
|
|
142
|
+
body.partIndex = 0;
|
|
143
|
+
}
|
|
144
|
+
(0, runtime_1.emitNervesEvent)({
|
|
145
|
+
component: "senses",
|
|
146
|
+
event: "senses.bluebubbles_send_start",
|
|
147
|
+
message: "sending bluebubbles message",
|
|
148
|
+
meta: {
|
|
149
|
+
chatGuid: params.chat.chatGuid,
|
|
150
|
+
hasReplyTarget: Boolean(params.replyToMessageGuid?.trim()),
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
const response = await fetch(url, {
|
|
154
|
+
method: "POST",
|
|
155
|
+
headers: { "Content-Type": "application/json" },
|
|
156
|
+
body: JSON.stringify(body),
|
|
157
|
+
signal: AbortSignal.timeout(channelConfig.requestTimeoutMs),
|
|
158
|
+
});
|
|
159
|
+
if (!response.ok) {
|
|
160
|
+
const errorText = await response.text().catch(() => "");
|
|
161
|
+
(0, runtime_1.emitNervesEvent)({
|
|
162
|
+
level: "error",
|
|
163
|
+
component: "senses",
|
|
164
|
+
event: "senses.bluebubbles_send_error",
|
|
165
|
+
message: "bluebubbles send failed",
|
|
166
|
+
meta: {
|
|
167
|
+
status: response.status,
|
|
168
|
+
reason: errorText || "unknown",
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
throw new Error(`BlueBubbles send failed (${response.status}): ${errorText || "unknown"}`);
|
|
172
|
+
}
|
|
173
|
+
const payload = await parseJsonBody(response);
|
|
174
|
+
const messageGuid = extractMessageGuid(payload);
|
|
175
|
+
(0, runtime_1.emitNervesEvent)({
|
|
176
|
+
component: "senses",
|
|
177
|
+
event: "senses.bluebubbles_send_end",
|
|
178
|
+
message: "bluebubbles message sent",
|
|
179
|
+
meta: {
|
|
180
|
+
chatGuid: params.chat.chatGuid,
|
|
181
|
+
messageGuid: messageGuid ?? null,
|
|
182
|
+
},
|
|
183
|
+
});
|
|
184
|
+
return { messageGuid };
|
|
185
|
+
},
|
|
186
|
+
async repairEvent(event) {
|
|
187
|
+
if (!event.requiresRepair) {
|
|
188
|
+
(0, runtime_1.emitNervesEvent)({
|
|
189
|
+
component: "senses",
|
|
190
|
+
event: "senses.bluebubbles_repair_skipped",
|
|
191
|
+
message: "bluebubbles event repair skipped",
|
|
192
|
+
meta: {
|
|
193
|
+
kind: event.kind,
|
|
194
|
+
messageGuid: event.messageGuid,
|
|
195
|
+
},
|
|
196
|
+
});
|
|
197
|
+
return event;
|
|
198
|
+
}
|
|
199
|
+
(0, runtime_1.emitNervesEvent)({
|
|
200
|
+
component: "senses",
|
|
201
|
+
event: "senses.bluebubbles_repair_start",
|
|
202
|
+
message: "repairing bluebubbles event by guid",
|
|
203
|
+
meta: {
|
|
204
|
+
kind: event.kind,
|
|
205
|
+
messageGuid: event.messageGuid,
|
|
206
|
+
eventType: event.eventType,
|
|
207
|
+
},
|
|
208
|
+
});
|
|
209
|
+
const url = buildRepairUrl(config.serverUrl, event.messageGuid, config.password);
|
|
210
|
+
try {
|
|
211
|
+
const response = await fetch(url, {
|
|
212
|
+
method: "GET",
|
|
213
|
+
signal: AbortSignal.timeout(channelConfig.requestTimeoutMs),
|
|
214
|
+
});
|
|
215
|
+
if (!response.ok) {
|
|
216
|
+
const errorText = await response.text().catch(() => "");
|
|
217
|
+
const repaired = applyRepairNotice(event, `BlueBubbles repair failed: ${errorText || `HTTP ${response.status}`}`);
|
|
218
|
+
(0, runtime_1.emitNervesEvent)({
|
|
219
|
+
level: "warn",
|
|
220
|
+
component: "senses",
|
|
221
|
+
event: "senses.bluebubbles_repair_error",
|
|
222
|
+
message: "bluebubbles repair request failed",
|
|
223
|
+
meta: {
|
|
224
|
+
messageGuid: event.messageGuid,
|
|
225
|
+
status: response.status,
|
|
226
|
+
reason: errorText || "unknown",
|
|
227
|
+
},
|
|
228
|
+
});
|
|
229
|
+
return repaired;
|
|
230
|
+
}
|
|
231
|
+
const payload = await parseJsonBody(response);
|
|
232
|
+
const data = extractRepairData(payload);
|
|
233
|
+
if (!data || typeof data.guid !== "string") {
|
|
234
|
+
const repaired = applyRepairNotice(event, "BlueBubbles repair failed: invalid message payload");
|
|
235
|
+
(0, runtime_1.emitNervesEvent)({
|
|
236
|
+
level: "warn",
|
|
237
|
+
component: "senses",
|
|
238
|
+
event: "senses.bluebubbles_repair_error",
|
|
239
|
+
message: "bluebubbles repair returned unusable payload",
|
|
240
|
+
meta: {
|
|
241
|
+
messageGuid: event.messageGuid,
|
|
242
|
+
},
|
|
243
|
+
});
|
|
244
|
+
return repaired;
|
|
245
|
+
}
|
|
246
|
+
const normalized = (0, bluebubbles_model_1.normalizeBlueBubblesEvent)({
|
|
247
|
+
type: event.eventType,
|
|
248
|
+
data,
|
|
249
|
+
});
|
|
250
|
+
const hydrated = hydrateTextForAgent(normalized, data);
|
|
251
|
+
(0, runtime_1.emitNervesEvent)({
|
|
252
|
+
component: "senses",
|
|
253
|
+
event: "senses.bluebubbles_repair_end",
|
|
254
|
+
message: "bluebubbles event repaired",
|
|
255
|
+
meta: {
|
|
256
|
+
kind: hydrated.kind,
|
|
257
|
+
messageGuid: hydrated.messageGuid,
|
|
258
|
+
repairedFrom: event.kind,
|
|
259
|
+
},
|
|
260
|
+
});
|
|
261
|
+
return hydrated;
|
|
262
|
+
}
|
|
263
|
+
catch (error) {
|
|
264
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
265
|
+
(0, runtime_1.emitNervesEvent)({
|
|
266
|
+
level: "warn",
|
|
267
|
+
component: "senses",
|
|
268
|
+
event: "senses.bluebubbles_repair_error",
|
|
269
|
+
message: "bluebubbles repair threw",
|
|
270
|
+
meta: {
|
|
271
|
+
messageGuid: event.messageGuid,
|
|
272
|
+
reason,
|
|
273
|
+
},
|
|
274
|
+
});
|
|
275
|
+
return applyRepairNotice(event, `BlueBubbles repair failed: ${reason}`);
|
|
276
|
+
}
|
|
277
|
+
},
|
|
278
|
+
};
|
|
279
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Thin entrypoint for `npm run bluebubbles` / `node dist/senses/bluebubbles-entry.js --agent <name>`.
|
|
3
|
+
// Separated from bluebubbles.ts so the BlueBubbles adapter stays testable.
|
|
4
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
|
+
if (!process.argv.includes("--agent")) {
|
|
6
|
+
// eslint-disable-next-line no-console -- pre-boot guard: --agent check before imports
|
|
7
|
+
console.error("Missing required --agent <name> argument.\nUsage: node dist/senses/bluebubbles-entry.js --agent ouroboros");
|
|
8
|
+
process.exit(1);
|
|
9
|
+
}
|
|
10
|
+
const bluebubbles_1 = require("./bluebubbles");
|
|
11
|
+
(0, bluebubbles_1.startBlueBubblesApp)();
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.normalizeBlueBubblesEvent = normalizeBlueBubblesEvent;
|
|
4
|
+
const runtime_1 = require("../nerves/runtime");
|
|
5
|
+
function asRecord(value) {
|
|
6
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
7
|
+
? value
|
|
8
|
+
: null;
|
|
9
|
+
}
|
|
10
|
+
function readString(record, key) {
|
|
11
|
+
if (!record)
|
|
12
|
+
return undefined;
|
|
13
|
+
const value = record[key];
|
|
14
|
+
return typeof value === "string" ? value : undefined;
|
|
15
|
+
}
|
|
16
|
+
function readNumber(record, key) {
|
|
17
|
+
if (!record)
|
|
18
|
+
return undefined;
|
|
19
|
+
const value = record[key];
|
|
20
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
21
|
+
}
|
|
22
|
+
function readBoolean(record, key) {
|
|
23
|
+
const value = record[key];
|
|
24
|
+
return typeof value === "boolean" ? value : undefined;
|
|
25
|
+
}
|
|
26
|
+
function normalizeHandle(raw) {
|
|
27
|
+
const trimmed = raw.trim();
|
|
28
|
+
if (!trimmed)
|
|
29
|
+
return "";
|
|
30
|
+
if (trimmed.includes("@"))
|
|
31
|
+
return trimmed.toLowerCase();
|
|
32
|
+
const compact = trimmed.replace(/[^\d+]/g, "");
|
|
33
|
+
return compact || trimmed;
|
|
34
|
+
}
|
|
35
|
+
function extractChatIdentifierFromGuid(chatGuid) {
|
|
36
|
+
if (!chatGuid)
|
|
37
|
+
return undefined;
|
|
38
|
+
const parts = chatGuid.split(";");
|
|
39
|
+
return parts.length >= 3 ? parts[2]?.trim() || undefined : undefined;
|
|
40
|
+
}
|
|
41
|
+
function buildChatRef(data, threadOriginatorGuid) {
|
|
42
|
+
const chats = Array.isArray(data.chats) ? data.chats : [];
|
|
43
|
+
const chat = asRecord(chats[0]) ?? null;
|
|
44
|
+
const chatGuid = readString(chat, "guid");
|
|
45
|
+
const chatIdentifier = readString(chat, "chatIdentifier") ??
|
|
46
|
+
readString(chat, "identifier") ??
|
|
47
|
+
extractChatIdentifierFromGuid(chatGuid);
|
|
48
|
+
const displayName = readString(chat, "displayName")?.trim() || undefined;
|
|
49
|
+
const style = readNumber(chat, "style");
|
|
50
|
+
const isGroup = style === 43 || (chatGuid?.includes(";+;") ?? false) || Boolean(displayName);
|
|
51
|
+
const baseKey = chatGuid?.trim()
|
|
52
|
+
? `chat:${chatGuid.trim()}`
|
|
53
|
+
: `chat_identifier:${(chatIdentifier ?? "unknown").trim()}`;
|
|
54
|
+
const sessionKey = threadOriginatorGuid?.trim()
|
|
55
|
+
? `${baseKey}:thread:${threadOriginatorGuid.trim()}`
|
|
56
|
+
: baseKey;
|
|
57
|
+
return {
|
|
58
|
+
chatGuid: chatGuid?.trim() || undefined,
|
|
59
|
+
chatIdentifier: chatIdentifier?.trim() || undefined,
|
|
60
|
+
displayName,
|
|
61
|
+
isGroup,
|
|
62
|
+
sessionKey,
|
|
63
|
+
sendTarget: chatGuid?.trim()
|
|
64
|
+
? { kind: "chat_guid", value: chatGuid.trim() }
|
|
65
|
+
: { kind: "chat_identifier", value: (chatIdentifier ?? "unknown").trim() },
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function extractSender(data, chat) {
|
|
69
|
+
const handle = asRecord(data.handle) ?? asRecord(data.sender) ?? null;
|
|
70
|
+
const rawId = readString(handle, "address") ??
|
|
71
|
+
readString(handle, "id") ??
|
|
72
|
+
readString(data, "senderId") ??
|
|
73
|
+
chat.chatIdentifier ??
|
|
74
|
+
chat.chatGuid ??
|
|
75
|
+
"unknown";
|
|
76
|
+
const externalId = normalizeHandle(rawId);
|
|
77
|
+
const displayName = externalId || rawId || "Unknown";
|
|
78
|
+
return {
|
|
79
|
+
provider: "imessage-handle",
|
|
80
|
+
externalId,
|
|
81
|
+
rawId,
|
|
82
|
+
displayName,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
function extractAttachments(data) {
|
|
86
|
+
const raw = Array.isArray(data.attachments) ? data.attachments : [];
|
|
87
|
+
return raw
|
|
88
|
+
.map((entry) => asRecord(entry))
|
|
89
|
+
.filter((entry) => entry !== null)
|
|
90
|
+
.map((entry) => ({
|
|
91
|
+
guid: readString(entry, "guid"),
|
|
92
|
+
mimeType: readString(entry, "mimeType"),
|
|
93
|
+
transferName: readString(entry, "transferName"),
|
|
94
|
+
totalBytes: readNumber(entry, "totalBytes"),
|
|
95
|
+
height: readNumber(entry, "height"),
|
|
96
|
+
width: readNumber(entry, "width"),
|
|
97
|
+
}));
|
|
98
|
+
}
|
|
99
|
+
function formatAttachmentText(attachments) {
|
|
100
|
+
if (attachments.length === 0)
|
|
101
|
+
return "";
|
|
102
|
+
const [first] = attachments;
|
|
103
|
+
const mime = first.mimeType ?? "";
|
|
104
|
+
const label = mime.startsWith("image/")
|
|
105
|
+
? "image attachment"
|
|
106
|
+
: mime.startsWith("audio/")
|
|
107
|
+
? "audio attachment"
|
|
108
|
+
: "attachment";
|
|
109
|
+
const name = first.transferName ? `: ${first.transferName}` : "";
|
|
110
|
+
const dimensions = typeof first.width === "number" && typeof first.height === "number" && first.width > 0 && first.height > 0
|
|
111
|
+
? ` (${first.width}x${first.height})`
|
|
112
|
+
: "";
|
|
113
|
+
return `[${label}${name}${dimensions}]`;
|
|
114
|
+
}
|
|
115
|
+
function formatMessageText(data, attachments) {
|
|
116
|
+
const text = readString(data, "text")?.trim() ?? "";
|
|
117
|
+
const balloonBundleId = readString(data, "balloonBundleId")?.trim();
|
|
118
|
+
if (text) {
|
|
119
|
+
if (balloonBundleId === "com.apple.messages.URLBalloonProvider") {
|
|
120
|
+
return `${text}\n[link preview attached]`;
|
|
121
|
+
}
|
|
122
|
+
return text;
|
|
123
|
+
}
|
|
124
|
+
return formatAttachmentText(attachments);
|
|
125
|
+
}
|
|
126
|
+
function normalizeReactionName(value) {
|
|
127
|
+
if (typeof value !== "string")
|
|
128
|
+
return undefined;
|
|
129
|
+
const trimmed = value.trim();
|
|
130
|
+
return trimmed ? trimmed.toLowerCase() : undefined;
|
|
131
|
+
}
|
|
132
|
+
function stripPartPrefix(guid) {
|
|
133
|
+
if (!guid)
|
|
134
|
+
return undefined;
|
|
135
|
+
const trimmed = guid.trim();
|
|
136
|
+
const marker = trimmed.lastIndexOf("/");
|
|
137
|
+
return marker >= 0 ? trimmed.slice(marker + 1) : trimmed;
|
|
138
|
+
}
|
|
139
|
+
function buildMutationText(mutationType, data, reactionName) {
|
|
140
|
+
if (mutationType === "reaction") {
|
|
141
|
+
return `reacted with ${reactionName}`;
|
|
142
|
+
}
|
|
143
|
+
if (mutationType === "edit") {
|
|
144
|
+
const editedText = readString(data, "text")?.trim() ?? "";
|
|
145
|
+
return editedText ? `edited message: ${editedText}` : "edited a message";
|
|
146
|
+
}
|
|
147
|
+
if (mutationType === "unsend") {
|
|
148
|
+
return "unsent a message";
|
|
149
|
+
}
|
|
150
|
+
if (mutationType === "read") {
|
|
151
|
+
return "message marked as read";
|
|
152
|
+
}
|
|
153
|
+
return "message marked as delivered";
|
|
154
|
+
}
|
|
155
|
+
function detectMutationType(eventType, data, reactionName) {
|
|
156
|
+
if (reactionName)
|
|
157
|
+
return "reaction";
|
|
158
|
+
if (eventType === "updated-message") {
|
|
159
|
+
if (readNumber(data, "dateRetracted"))
|
|
160
|
+
return "unsend";
|
|
161
|
+
if (readNumber(data, "dateEdited"))
|
|
162
|
+
return "edit";
|
|
163
|
+
if (readNumber(data, "dateRead"))
|
|
164
|
+
return "read";
|
|
165
|
+
if (readBoolean(data, "isDelivered") || readNumber(data, "dateDelivered"))
|
|
166
|
+
return "delivery";
|
|
167
|
+
}
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
function normalizeBlueBubblesEvent(payload) {
|
|
171
|
+
const envelope = asRecord(payload);
|
|
172
|
+
const eventType = readString(envelope, "type")?.trim() ?? "";
|
|
173
|
+
const data = asRecord(envelope?.data);
|
|
174
|
+
if (!eventType || !data) {
|
|
175
|
+
(0, runtime_1.emitNervesEvent)({
|
|
176
|
+
level: "warn",
|
|
177
|
+
component: "senses",
|
|
178
|
+
event: "senses.bluebubbles_event_ignored",
|
|
179
|
+
message: "ignored invalid bluebubbles payload",
|
|
180
|
+
meta: { hasEnvelope: Boolean(envelope), eventType },
|
|
181
|
+
});
|
|
182
|
+
throw new Error("Invalid BlueBubbles payload");
|
|
183
|
+
}
|
|
184
|
+
const messageGuid = readString(data, "guid")?.trim();
|
|
185
|
+
if (!messageGuid) {
|
|
186
|
+
(0, runtime_1.emitNervesEvent)({
|
|
187
|
+
level: "warn",
|
|
188
|
+
component: "senses",
|
|
189
|
+
event: "senses.bluebubbles_event_ignored",
|
|
190
|
+
message: "ignored bluebubbles payload without guid",
|
|
191
|
+
meta: { eventType },
|
|
192
|
+
});
|
|
193
|
+
throw new Error("BlueBubbles payload is missing data.guid");
|
|
194
|
+
}
|
|
195
|
+
const threadOriginatorGuid = readString(data, "threadOriginatorGuid")?.trim() || undefined;
|
|
196
|
+
const chat = buildChatRef(data, threadOriginatorGuid);
|
|
197
|
+
const sender = extractSender(data, chat);
|
|
198
|
+
const timestamp = readNumber(data, "dateCreated") ?? Date.now();
|
|
199
|
+
const fromMe = readBoolean(data, "isFromMe") ?? false;
|
|
200
|
+
const attachments = extractAttachments(data);
|
|
201
|
+
const reactionName = normalizeReactionName(data.associatedMessageType);
|
|
202
|
+
const mutationType = detectMutationType(eventType, data, reactionName);
|
|
203
|
+
const requiresRepair = (readBoolean(data, "hasPayloadData") ?? false) ||
|
|
204
|
+
attachments.length > 0 ||
|
|
205
|
+
eventType === "updated-message";
|
|
206
|
+
const result = mutationType
|
|
207
|
+
? {
|
|
208
|
+
kind: "mutation",
|
|
209
|
+
eventType,
|
|
210
|
+
mutationType,
|
|
211
|
+
messageGuid,
|
|
212
|
+
targetMessageGuid: mutationType === "reaction"
|
|
213
|
+
? stripPartPrefix(readString(data, "associatedMessageGuid"))
|
|
214
|
+
: undefined,
|
|
215
|
+
timestamp,
|
|
216
|
+
fromMe,
|
|
217
|
+
sender,
|
|
218
|
+
chat,
|
|
219
|
+
shouldNotifyAgent: mutationType === "reaction" || mutationType === "edit" || mutationType === "unsend",
|
|
220
|
+
textForAgent: buildMutationText(mutationType, data, reactionName),
|
|
221
|
+
requiresRepair,
|
|
222
|
+
}
|
|
223
|
+
: {
|
|
224
|
+
kind: "message",
|
|
225
|
+
eventType,
|
|
226
|
+
messageGuid,
|
|
227
|
+
timestamp,
|
|
228
|
+
fromMe,
|
|
229
|
+
sender,
|
|
230
|
+
chat,
|
|
231
|
+
text: readString(data, "text")?.trim() ?? "",
|
|
232
|
+
textForAgent: formatMessageText(data, attachments),
|
|
233
|
+
attachments,
|
|
234
|
+
balloonBundleId: readString(data, "balloonBundleId")?.trim() || undefined,
|
|
235
|
+
hasPayloadData: readBoolean(data, "hasPayloadData") ?? false,
|
|
236
|
+
requiresRepair,
|
|
237
|
+
threadOriginatorGuid,
|
|
238
|
+
replyToGuid: threadOriginatorGuid,
|
|
239
|
+
};
|
|
240
|
+
(0, runtime_1.emitNervesEvent)({
|
|
241
|
+
component: "senses",
|
|
242
|
+
event: "senses.bluebubbles_event_normalized",
|
|
243
|
+
message: "normalized bluebubbles event",
|
|
244
|
+
meta: {
|
|
245
|
+
eventType,
|
|
246
|
+
kind: result.kind,
|
|
247
|
+
mutationType: result.kind === "mutation" ? result.mutationType : null,
|
|
248
|
+
sessionKey: result.chat.sessionKey,
|
|
249
|
+
fromMe,
|
|
250
|
+
},
|
|
251
|
+
});
|
|
252
|
+
return result;
|
|
253
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
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.getBlueBubblesMutationLogPath = getBlueBubblesMutationLogPath;
|
|
37
|
+
exports.recordBlueBubblesMutation = recordBlueBubblesMutation;
|
|
38
|
+
const fs = __importStar(require("node:fs"));
|
|
39
|
+
const os = __importStar(require("node:os"));
|
|
40
|
+
const path = __importStar(require("node:path"));
|
|
41
|
+
const runtime_1 = require("../nerves/runtime");
|
|
42
|
+
function sanitizeKey(key) {
|
|
43
|
+
return key.replace(/[/:]/g, "_");
|
|
44
|
+
}
|
|
45
|
+
function getBlueBubblesMutationLogPath(agentName, sessionKey) {
|
|
46
|
+
return path.join(os.homedir(), ".agentstate", agentName, "senses", "bluebubbles", "mutations", `${sanitizeKey(sessionKey)}.ndjson`);
|
|
47
|
+
}
|
|
48
|
+
function recordBlueBubblesMutation(agentName, event) {
|
|
49
|
+
const filePath = getBlueBubblesMutationLogPath(agentName, event.chat.sessionKey);
|
|
50
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
51
|
+
fs.appendFileSync(filePath, JSON.stringify({
|
|
52
|
+
recordedAt: new Date(event.timestamp).toISOString(),
|
|
53
|
+
eventType: event.eventType,
|
|
54
|
+
mutationType: event.mutationType,
|
|
55
|
+
messageGuid: event.messageGuid,
|
|
56
|
+
targetMessageGuid: event.targetMessageGuid ?? null,
|
|
57
|
+
chatGuid: event.chat.chatGuid ?? null,
|
|
58
|
+
chatIdentifier: event.chat.chatIdentifier ?? null,
|
|
59
|
+
sessionKey: event.chat.sessionKey,
|
|
60
|
+
shouldNotifyAgent: event.shouldNotifyAgent,
|
|
61
|
+
textForAgent: event.textForAgent,
|
|
62
|
+
fromMe: event.fromMe,
|
|
63
|
+
}) + "\n", "utf-8");
|
|
64
|
+
(0, runtime_1.emitNervesEvent)({
|
|
65
|
+
component: "senses",
|
|
66
|
+
event: "senses.bluebubbles_mutation_logged",
|
|
67
|
+
message: "recorded bluebubbles mutation to sidecar log",
|
|
68
|
+
meta: {
|
|
69
|
+
agentName,
|
|
70
|
+
mutationType: event.mutationType,
|
|
71
|
+
messageGuid: event.messageGuid,
|
|
72
|
+
path: filePath,
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
return filePath;
|
|
76
|
+
}
|
|
@@ -0,0 +1,332 @@
|
|
|
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.handleBlueBubblesEvent = handleBlueBubblesEvent;
|
|
37
|
+
exports.createBlueBubblesWebhookHandler = createBlueBubblesWebhookHandler;
|
|
38
|
+
exports.startBlueBubblesApp = startBlueBubblesApp;
|
|
39
|
+
const http = __importStar(require("node:http"));
|
|
40
|
+
const path = __importStar(require("node:path"));
|
|
41
|
+
const core_1 = require("../heart/core");
|
|
42
|
+
const config_1 = require("../heart/config");
|
|
43
|
+
const identity_1 = require("../heart/identity");
|
|
44
|
+
const context_1 = require("../mind/context");
|
|
45
|
+
const tokens_1 = require("../mind/friends/tokens");
|
|
46
|
+
const resolver_1 = require("../mind/friends/resolver");
|
|
47
|
+
const store_file_1 = require("../mind/friends/store-file");
|
|
48
|
+
const prompt_1 = require("../mind/prompt");
|
|
49
|
+
const runtime_1 = require("../nerves/runtime");
|
|
50
|
+
const bluebubbles_model_1 = require("./bluebubbles-model");
|
|
51
|
+
const bluebubbles_client_1 = require("./bluebubbles-client");
|
|
52
|
+
const bluebubbles_mutation_log_1 = require("./bluebubbles-mutation-log");
|
|
53
|
+
const defaultDeps = {
|
|
54
|
+
getAgentName: identity_1.getAgentName,
|
|
55
|
+
buildSystem: prompt_1.buildSystem,
|
|
56
|
+
runAgent: core_1.runAgent,
|
|
57
|
+
loadSession: context_1.loadSession,
|
|
58
|
+
postTurn: context_1.postTurn,
|
|
59
|
+
sessionPath: config_1.sessionPath,
|
|
60
|
+
accumulateFriendTokens: tokens_1.accumulateFriendTokens,
|
|
61
|
+
createClient: () => (0, bluebubbles_client_1.createBlueBubblesClient)(),
|
|
62
|
+
recordMutation: bluebubbles_mutation_log_1.recordBlueBubblesMutation,
|
|
63
|
+
createFriendStore: () => new store_file_1.FileFriendStore(path.join((0, identity_1.getAgentRoot)(), "friends")),
|
|
64
|
+
createFriendResolver: (store, params) => new resolver_1.FriendResolver(store, params),
|
|
65
|
+
createServer: http.createServer,
|
|
66
|
+
};
|
|
67
|
+
function resolveFriendParams(event) {
|
|
68
|
+
if (event.chat.isGroup) {
|
|
69
|
+
const groupKey = event.chat.chatGuid ?? event.chat.chatIdentifier ?? event.sender.externalId;
|
|
70
|
+
return {
|
|
71
|
+
provider: "imessage-handle",
|
|
72
|
+
externalId: `group:${groupKey}`,
|
|
73
|
+
displayName: event.chat.displayName ?? "Unknown Group",
|
|
74
|
+
channel: "bluebubbles",
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
provider: "imessage-handle",
|
|
79
|
+
externalId: event.sender.externalId || event.sender.rawId,
|
|
80
|
+
displayName: event.sender.displayName || "Unknown",
|
|
81
|
+
channel: "bluebubbles",
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
function buildInboundText(event) {
|
|
85
|
+
const baseText = event.repairNotice?.trim()
|
|
86
|
+
? `${event.textForAgent}\n[${event.repairNotice.trim()}]`
|
|
87
|
+
: event.textForAgent;
|
|
88
|
+
if (!event.chat.isGroup)
|
|
89
|
+
return baseText;
|
|
90
|
+
if (event.kind === "mutation") {
|
|
91
|
+
return `${event.sender.displayName} ${baseText}`;
|
|
92
|
+
}
|
|
93
|
+
return `${event.sender.displayName}: ${baseText}`;
|
|
94
|
+
}
|
|
95
|
+
function createBlueBubblesCallbacks(client, chat, replyToMessageGuid) {
|
|
96
|
+
let textBuffer = "";
|
|
97
|
+
return {
|
|
98
|
+
onModelStart() {
|
|
99
|
+
(0, runtime_1.emitNervesEvent)({
|
|
100
|
+
component: "senses",
|
|
101
|
+
event: "senses.bluebubbles_turn_start",
|
|
102
|
+
message: "bluebubbles turn started",
|
|
103
|
+
meta: { chatGuid: chat.chatGuid ?? null },
|
|
104
|
+
});
|
|
105
|
+
},
|
|
106
|
+
onModelStreamStart() {
|
|
107
|
+
(0, runtime_1.emitNervesEvent)({
|
|
108
|
+
component: "senses",
|
|
109
|
+
event: "senses.bluebubbles_stream_start",
|
|
110
|
+
message: "bluebubbles non-streaming response started",
|
|
111
|
+
meta: {},
|
|
112
|
+
});
|
|
113
|
+
},
|
|
114
|
+
onTextChunk(text) {
|
|
115
|
+
textBuffer += text;
|
|
116
|
+
},
|
|
117
|
+
onReasoningChunk(_text) { },
|
|
118
|
+
onToolStart(name, _args) {
|
|
119
|
+
(0, runtime_1.emitNervesEvent)({
|
|
120
|
+
component: "senses",
|
|
121
|
+
event: "senses.bluebubbles_tool_start",
|
|
122
|
+
message: "bluebubbles tool execution started",
|
|
123
|
+
meta: { name },
|
|
124
|
+
});
|
|
125
|
+
},
|
|
126
|
+
onToolEnd(name, summary, success) {
|
|
127
|
+
(0, runtime_1.emitNervesEvent)({
|
|
128
|
+
component: "senses",
|
|
129
|
+
event: "senses.bluebubbles_tool_end",
|
|
130
|
+
message: "bluebubbles tool execution completed",
|
|
131
|
+
meta: { name, success, summary },
|
|
132
|
+
});
|
|
133
|
+
},
|
|
134
|
+
onError(error, severity) {
|
|
135
|
+
(0, runtime_1.emitNervesEvent)({
|
|
136
|
+
level: severity === "terminal" ? "error" : "warn",
|
|
137
|
+
component: "senses",
|
|
138
|
+
event: "senses.bluebubbles_turn_error",
|
|
139
|
+
message: "bluebubbles turn callback error",
|
|
140
|
+
meta: { severity, reason: error.message },
|
|
141
|
+
});
|
|
142
|
+
},
|
|
143
|
+
onClearText() {
|
|
144
|
+
textBuffer = "";
|
|
145
|
+
},
|
|
146
|
+
async flush() {
|
|
147
|
+
const trimmed = textBuffer.trim();
|
|
148
|
+
if (!trimmed) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
textBuffer = "";
|
|
152
|
+
await client.sendText({
|
|
153
|
+
chat,
|
|
154
|
+
text: trimmed,
|
|
155
|
+
replyToMessageGuid,
|
|
156
|
+
});
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
async function readRequestBody(req) {
|
|
161
|
+
let body = "";
|
|
162
|
+
for await (const chunk of req) {
|
|
163
|
+
body += chunk.toString();
|
|
164
|
+
}
|
|
165
|
+
return body;
|
|
166
|
+
}
|
|
167
|
+
function writeJson(res, statusCode, payload) {
|
|
168
|
+
res.statusCode = statusCode;
|
|
169
|
+
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
170
|
+
res.end(JSON.stringify(payload));
|
|
171
|
+
}
|
|
172
|
+
function isWebhookPasswordValid(url, expectedPassword) {
|
|
173
|
+
const provided = url.searchParams.get("password");
|
|
174
|
+
return !provided || provided === expectedPassword;
|
|
175
|
+
}
|
|
176
|
+
async function handleBlueBubblesEvent(payload, deps = {}) {
|
|
177
|
+
const resolvedDeps = { ...defaultDeps, ...deps };
|
|
178
|
+
const client = resolvedDeps.createClient();
|
|
179
|
+
const event = await client.repairEvent((0, bluebubbles_model_1.normalizeBlueBubblesEvent)(payload));
|
|
180
|
+
if (event.fromMe) {
|
|
181
|
+
(0, runtime_1.emitNervesEvent)({
|
|
182
|
+
component: "senses",
|
|
183
|
+
event: "senses.bluebubbles_from_me_ignored",
|
|
184
|
+
message: "ignored from-me bluebubbles event",
|
|
185
|
+
meta: {
|
|
186
|
+
messageGuid: event.messageGuid,
|
|
187
|
+
kind: event.kind,
|
|
188
|
+
},
|
|
189
|
+
});
|
|
190
|
+
return { handled: true, notifiedAgent: false, kind: event.kind, reason: "from_me" };
|
|
191
|
+
}
|
|
192
|
+
if (event.kind === "mutation") {
|
|
193
|
+
try {
|
|
194
|
+
resolvedDeps.recordMutation(resolvedDeps.getAgentName(), event);
|
|
195
|
+
}
|
|
196
|
+
catch (error) {
|
|
197
|
+
(0, runtime_1.emitNervesEvent)({
|
|
198
|
+
level: "error",
|
|
199
|
+
component: "senses",
|
|
200
|
+
event: "senses.bluebubbles_mutation_log_error",
|
|
201
|
+
message: "failed recording bluebubbles mutation sidecar",
|
|
202
|
+
meta: {
|
|
203
|
+
messageGuid: event.messageGuid,
|
|
204
|
+
mutationType: event.mutationType,
|
|
205
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
206
|
+
},
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
if (event.kind === "mutation" && !event.shouldNotifyAgent) {
|
|
211
|
+
(0, runtime_1.emitNervesEvent)({
|
|
212
|
+
component: "senses",
|
|
213
|
+
event: "senses.bluebubbles_state_mutation_recorded",
|
|
214
|
+
message: "recorded non-notify bluebubbles mutation",
|
|
215
|
+
meta: {
|
|
216
|
+
messageGuid: event.messageGuid,
|
|
217
|
+
mutationType: event.mutationType,
|
|
218
|
+
},
|
|
219
|
+
});
|
|
220
|
+
return { handled: true, notifiedAgent: false, kind: event.kind, reason: "mutation_state_only" };
|
|
221
|
+
}
|
|
222
|
+
const store = resolvedDeps.createFriendStore();
|
|
223
|
+
const resolver = resolvedDeps.createFriendResolver(store, resolveFriendParams(event));
|
|
224
|
+
const context = await resolver.resolve();
|
|
225
|
+
const toolContext = {
|
|
226
|
+
signin: async () => undefined,
|
|
227
|
+
friendStore: store,
|
|
228
|
+
summarize: (0, core_1.createSummarize)(),
|
|
229
|
+
context,
|
|
230
|
+
};
|
|
231
|
+
const friendId = context.friend.id;
|
|
232
|
+
const sessPath = resolvedDeps.sessionPath(friendId, "bluebubbles", event.chat.sessionKey);
|
|
233
|
+
const existing = resolvedDeps.loadSession(sessPath);
|
|
234
|
+
const messages = existing?.messages && existing.messages.length > 0
|
|
235
|
+
? existing.messages
|
|
236
|
+
: [{ role: "system", content: await resolvedDeps.buildSystem("bluebubbles", undefined, context) }];
|
|
237
|
+
messages.push({ role: "user", content: buildInboundText(event) });
|
|
238
|
+
const callbacks = createBlueBubblesCallbacks(client, event.chat, event.kind === "message" ? event.messageGuid : undefined);
|
|
239
|
+
const controller = new AbortController();
|
|
240
|
+
const agentOptions = {
|
|
241
|
+
toolContext,
|
|
242
|
+
};
|
|
243
|
+
const result = await resolvedDeps.runAgent(messages, callbacks, "bluebubbles", controller.signal, agentOptions);
|
|
244
|
+
await callbacks.flush();
|
|
245
|
+
resolvedDeps.postTurn(messages, sessPath, result.usage);
|
|
246
|
+
await resolvedDeps.accumulateFriendTokens(store, friendId, result.usage);
|
|
247
|
+
(0, runtime_1.emitNervesEvent)({
|
|
248
|
+
component: "senses",
|
|
249
|
+
event: "senses.bluebubbles_turn_end",
|
|
250
|
+
message: "bluebubbles event handled",
|
|
251
|
+
meta: {
|
|
252
|
+
messageGuid: event.messageGuid,
|
|
253
|
+
kind: event.kind,
|
|
254
|
+
sessionKey: event.chat.sessionKey,
|
|
255
|
+
},
|
|
256
|
+
});
|
|
257
|
+
return {
|
|
258
|
+
handled: true,
|
|
259
|
+
notifiedAgent: true,
|
|
260
|
+
kind: event.kind,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
function createBlueBubblesWebhookHandler(deps = {}) {
|
|
264
|
+
return async (req, res) => {
|
|
265
|
+
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
266
|
+
const channelConfig = (0, config_1.getBlueBubblesChannelConfig)();
|
|
267
|
+
const runtimeConfig = (0, config_1.getBlueBubblesConfig)();
|
|
268
|
+
if (url.pathname !== channelConfig.webhookPath) {
|
|
269
|
+
writeJson(res, 404, { error: "Not found" });
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
if (req.method !== "POST") {
|
|
273
|
+
writeJson(res, 405, { error: "Method not allowed" });
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
if (!isWebhookPasswordValid(url, runtimeConfig.password)) {
|
|
277
|
+
writeJson(res, 401, { error: "Unauthorized" });
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
let payload;
|
|
281
|
+
try {
|
|
282
|
+
const rawBody = await readRequestBody(req);
|
|
283
|
+
payload = JSON.parse(rawBody);
|
|
284
|
+
}
|
|
285
|
+
catch (error) {
|
|
286
|
+
(0, runtime_1.emitNervesEvent)({
|
|
287
|
+
level: "warn",
|
|
288
|
+
component: "senses",
|
|
289
|
+
event: "senses.bluebubbles_webhook_bad_json",
|
|
290
|
+
message: "failed to parse bluebubbles webhook body",
|
|
291
|
+
meta: {
|
|
292
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
293
|
+
},
|
|
294
|
+
});
|
|
295
|
+
writeJson(res, 400, { error: "Invalid JSON body" });
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
try {
|
|
299
|
+
const result = await handleBlueBubblesEvent(payload, deps);
|
|
300
|
+
writeJson(res, 200, result);
|
|
301
|
+
}
|
|
302
|
+
catch (error) {
|
|
303
|
+
(0, runtime_1.emitNervesEvent)({
|
|
304
|
+
level: "error",
|
|
305
|
+
component: "senses",
|
|
306
|
+
event: "senses.bluebubbles_webhook_error",
|
|
307
|
+
message: "bluebubbles webhook handling failed",
|
|
308
|
+
meta: {
|
|
309
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
310
|
+
},
|
|
311
|
+
});
|
|
312
|
+
writeJson(res, 500, {
|
|
313
|
+
error: error instanceof Error ? error.message : String(error),
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
function startBlueBubblesApp(deps = {}) {
|
|
319
|
+
const resolvedDeps = { ...defaultDeps, ...deps };
|
|
320
|
+
resolvedDeps.createClient();
|
|
321
|
+
const channelConfig = (0, config_1.getBlueBubblesChannelConfig)();
|
|
322
|
+
const server = resolvedDeps.createServer(createBlueBubblesWebhookHandler(deps));
|
|
323
|
+
server.listen(channelConfig.port, () => {
|
|
324
|
+
(0, runtime_1.emitNervesEvent)({
|
|
325
|
+
component: "channels",
|
|
326
|
+
event: "channel.app_started",
|
|
327
|
+
message: "BlueBubbles sense started",
|
|
328
|
+
meta: { port: channelConfig.port, webhookPath: channelConfig.webhookPath },
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
return server;
|
|
332
|
+
}
|
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.7",
|
|
4
4
|
"main": "dist/heart/daemon/ouro-entry.js",
|
|
5
5
|
"bin": {
|
|
6
6
|
"ouro": "dist/heart/daemon/ouro-entry.js",
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
"daemon": "tsc && node dist/heart/daemon/daemon-entry.js",
|
|
21
21
|
"ouro": "tsc && node dist/heart/daemon/ouro-entry.js",
|
|
22
22
|
"teams": "tsc && node dist/senses/teams-entry.js --agent ouroboros",
|
|
23
|
+
"bluebubbles": "tsc && node dist/senses/bluebubbles-entry.js --agent ouroboros",
|
|
23
24
|
"test": "vitest run",
|
|
24
25
|
"test:coverage:vitest": "vitest run --coverage",
|
|
25
26
|
"test:coverage": "node scripts/run-coverage-gate.cjs",
|