@ouro.bot/cli 0.1.0-alpha.16 → 0.1.0-alpha.17
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/core.js +1 -1
- package/dist/heart/streaming.js +55 -1
- package/dist/mind/prompt.js +3 -3
- package/dist/repertoire/coding/feedback.js +134 -0
- package/dist/repertoire/coding/index.js +4 -1
- package/dist/repertoire/coding/manager.js +61 -2
- package/dist/repertoire/coding/spawner.js +3 -3
- package/dist/repertoire/coding/tools.js +41 -2
- package/dist/repertoire/tools-base.js +1 -1
- package/dist/repertoire/tools.js +29 -8
- package/dist/senses/bluebubbles-client.js +159 -5
- package/dist/senses/bluebubbles-media.js +244 -0
- package/dist/senses/bluebubbles.js +108 -19
- package/dist/senses/debug-activity.js +107 -0
- package/package.json +1 -1
- package/subagents/work-doer.md +26 -24
- package/subagents/work-merger.md +24 -30
- package/subagents/work-planner.md +34 -25
|
@@ -3,8 +3,10 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.createBlueBubblesClient = createBlueBubblesClient;
|
|
4
4
|
const node_crypto_1 = require("node:crypto");
|
|
5
5
|
const config_1 = require("../heart/config");
|
|
6
|
+
const identity_1 = require("../heart/identity");
|
|
6
7
|
const runtime_1 = require("../nerves/runtime");
|
|
7
8
|
const bluebubbles_model_1 = require("./bluebubbles-model");
|
|
9
|
+
const bluebubbles_media_1 = require("./bluebubbles-media");
|
|
8
10
|
function buildBlueBubblesApiUrl(baseUrl, endpoint, password) {
|
|
9
11
|
const root = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
|
|
10
12
|
const url = new URL(endpoint.replace(/^\//, ""), root);
|
|
@@ -16,6 +18,10 @@ function asRecord(value) {
|
|
|
16
18
|
? value
|
|
17
19
|
: null;
|
|
18
20
|
}
|
|
21
|
+
function readString(record, key) {
|
|
22
|
+
const value = record[key];
|
|
23
|
+
return typeof value === "string" ? value : undefined;
|
|
24
|
+
}
|
|
19
25
|
function extractMessageGuid(payload) {
|
|
20
26
|
if (!payload || typeof payload !== "object")
|
|
21
27
|
return undefined;
|
|
@@ -55,6 +61,75 @@ function buildRepairUrl(baseUrl, messageGuid, password) {
|
|
|
55
61
|
parsed.searchParams.set("with", "attachments,payloadData,chats,messageSummaryInfo");
|
|
56
62
|
return parsed.toString();
|
|
57
63
|
}
|
|
64
|
+
function extractChatIdentifierFromGuid(chatGuid) {
|
|
65
|
+
const parts = chatGuid.split(";");
|
|
66
|
+
return parts.length >= 3 ? parts[2]?.trim() || undefined : undefined;
|
|
67
|
+
}
|
|
68
|
+
function extractChatGuid(value) {
|
|
69
|
+
const record = asRecord(value);
|
|
70
|
+
const candidates = [
|
|
71
|
+
record?.chatGuid,
|
|
72
|
+
record?.guid,
|
|
73
|
+
record?.chat_guid,
|
|
74
|
+
record?.identifier,
|
|
75
|
+
record?.chatIdentifier,
|
|
76
|
+
record?.chat_identifier,
|
|
77
|
+
];
|
|
78
|
+
for (const candidate of candidates) {
|
|
79
|
+
if (typeof candidate === "string" && candidate.trim()) {
|
|
80
|
+
return candidate.trim();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
function extractQueriedChatIdentifier(chat, chatGuid) {
|
|
86
|
+
const explicitIdentifier = readString(chat, "chatIdentifier")
|
|
87
|
+
?? readString(chat, "identifier")
|
|
88
|
+
?? readString(chat, "chat_identifier");
|
|
89
|
+
if (explicitIdentifier) {
|
|
90
|
+
return explicitIdentifier;
|
|
91
|
+
}
|
|
92
|
+
return extractChatIdentifierFromGuid(chatGuid);
|
|
93
|
+
}
|
|
94
|
+
function extractChatQueryRows(payload) {
|
|
95
|
+
const record = asRecord(payload);
|
|
96
|
+
const data = Array.isArray(record?.data) ? record.data : payload;
|
|
97
|
+
if (!Array.isArray(data)) {
|
|
98
|
+
return [];
|
|
99
|
+
}
|
|
100
|
+
return data.map((entry) => asRecord(entry)).filter((entry) => entry !== null);
|
|
101
|
+
}
|
|
102
|
+
async function resolveChatGuidForIdentifier(config, channelConfig, chatIdentifier) {
|
|
103
|
+
const trimmedIdentifier = chatIdentifier.trim();
|
|
104
|
+
if (!trimmedIdentifier)
|
|
105
|
+
return undefined;
|
|
106
|
+
const url = buildBlueBubblesApiUrl(config.serverUrl, "/api/v1/chat/query", config.password);
|
|
107
|
+
const response = await fetch(url, {
|
|
108
|
+
method: "POST",
|
|
109
|
+
headers: { "Content-Type": "application/json" },
|
|
110
|
+
body: JSON.stringify({
|
|
111
|
+
limit: 500,
|
|
112
|
+
offset: 0,
|
|
113
|
+
with: ["participants"],
|
|
114
|
+
}),
|
|
115
|
+
signal: AbortSignal.timeout(channelConfig.requestTimeoutMs),
|
|
116
|
+
});
|
|
117
|
+
if (!response.ok) {
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
const payload = await parseJsonBody(response);
|
|
121
|
+
const rows = extractChatQueryRows(payload);
|
|
122
|
+
for (const row of rows) {
|
|
123
|
+
const guid = extractChatGuid(row);
|
|
124
|
+
if (!guid)
|
|
125
|
+
continue;
|
|
126
|
+
const identifier = extractQueriedChatIdentifier(row, guid);
|
|
127
|
+
if (identifier === trimmedIdentifier || guid === trimmedIdentifier) {
|
|
128
|
+
return guid;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
58
133
|
function collectPreviewStrings(value, out, depth = 0) {
|
|
59
134
|
if (depth > 4 || out.length >= 4)
|
|
60
135
|
return;
|
|
@@ -120,6 +195,13 @@ function extractRepairData(payload) {
|
|
|
120
195
|
const record = asRecord(payload);
|
|
121
196
|
return asRecord(record?.data) ?? record;
|
|
122
197
|
}
|
|
198
|
+
function providerSupportsAudioInput(provider) {
|
|
199
|
+
return provider === "azure" || provider === "openai-codex";
|
|
200
|
+
}
|
|
201
|
+
async function resolveChatGuid(chat, config, channelConfig) {
|
|
202
|
+
return chat.chatGuid
|
|
203
|
+
?? await resolveChatGuidForIdentifier(config, channelConfig, chat.chatIdentifier ?? "");
|
|
204
|
+
}
|
|
123
205
|
function createBlueBubblesClient(config = (0, config_1.getBlueBubblesConfig)(), channelConfig = (0, config_1.getBlueBubblesChannelConfig)()) {
|
|
124
206
|
return {
|
|
125
207
|
async sendText(params) {
|
|
@@ -127,12 +209,13 @@ function createBlueBubblesClient(config = (0, config_1.getBlueBubblesConfig)(),
|
|
|
127
209
|
if (!trimmedText) {
|
|
128
210
|
throw new Error("BlueBubbles send requires non-empty text.");
|
|
129
211
|
}
|
|
130
|
-
|
|
212
|
+
const resolvedChatGuid = await resolveChatGuid(params.chat, config, channelConfig);
|
|
213
|
+
if (!resolvedChatGuid) {
|
|
131
214
|
throw new Error("BlueBubbles send currently requires chat.chatGuid from the inbound event.");
|
|
132
215
|
}
|
|
133
216
|
const url = buildBlueBubblesApiUrl(config.serverUrl, "/api/v1/message/text", config.password);
|
|
134
217
|
const body = {
|
|
135
|
-
chatGuid:
|
|
218
|
+
chatGuid: resolvedChatGuid,
|
|
136
219
|
tempGuid: (0, node_crypto_1.randomUUID)(),
|
|
137
220
|
message: trimmedText,
|
|
138
221
|
};
|
|
@@ -146,7 +229,7 @@ function createBlueBubblesClient(config = (0, config_1.getBlueBubblesConfig)(),
|
|
|
146
229
|
event: "senses.bluebubbles_send_start",
|
|
147
230
|
message: "sending bluebubbles message",
|
|
148
231
|
meta: {
|
|
149
|
-
chatGuid:
|
|
232
|
+
chatGuid: resolvedChatGuid,
|
|
150
233
|
hasReplyTarget: Boolean(params.replyToMessageGuid?.trim()),
|
|
151
234
|
},
|
|
152
235
|
});
|
|
@@ -177,12 +260,68 @@ function createBlueBubblesClient(config = (0, config_1.getBlueBubblesConfig)(),
|
|
|
177
260
|
event: "senses.bluebubbles_send_end",
|
|
178
261
|
message: "bluebubbles message sent",
|
|
179
262
|
meta: {
|
|
180
|
-
chatGuid:
|
|
263
|
+
chatGuid: resolvedChatGuid,
|
|
181
264
|
messageGuid: messageGuid ?? null,
|
|
182
265
|
},
|
|
183
266
|
});
|
|
184
267
|
return { messageGuid };
|
|
185
268
|
},
|
|
269
|
+
async editMessage(params) {
|
|
270
|
+
const messageGuid = params.messageGuid.trim();
|
|
271
|
+
const text = params.text.trim();
|
|
272
|
+
if (!messageGuid) {
|
|
273
|
+
throw new Error("BlueBubbles edit requires messageGuid.");
|
|
274
|
+
}
|
|
275
|
+
if (!text) {
|
|
276
|
+
throw new Error("BlueBubbles edit requires non-empty text.");
|
|
277
|
+
}
|
|
278
|
+
const editTimeoutMs = Math.max(channelConfig.requestTimeoutMs, 120000);
|
|
279
|
+
const url = buildBlueBubblesApiUrl(config.serverUrl, `/api/v1/message/${encodeURIComponent(messageGuid)}/edit`, config.password);
|
|
280
|
+
const response = await fetch(url, {
|
|
281
|
+
method: "POST",
|
|
282
|
+
headers: { "Content-Type": "application/json" },
|
|
283
|
+
body: JSON.stringify({
|
|
284
|
+
editedMessage: text,
|
|
285
|
+
backwardsCompatibilityMessage: params.backwardsCompatibilityMessage ?? `Edited to: ${text}`,
|
|
286
|
+
partIndex: typeof params.partIndex === "number" ? params.partIndex : 0,
|
|
287
|
+
}),
|
|
288
|
+
signal: AbortSignal.timeout(editTimeoutMs),
|
|
289
|
+
});
|
|
290
|
+
if (!response.ok) {
|
|
291
|
+
const errorText = await response.text().catch(() => "");
|
|
292
|
+
throw new Error(`BlueBubbles edit failed (${response.status}): ${errorText || "unknown"}`);
|
|
293
|
+
}
|
|
294
|
+
},
|
|
295
|
+
async setTyping(chat, typing) {
|
|
296
|
+
const resolvedChatGuid = await resolveChatGuid(chat, config, channelConfig);
|
|
297
|
+
if (!resolvedChatGuid) {
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
const url = buildBlueBubblesApiUrl(config.serverUrl, `/api/v1/chat/${encodeURIComponent(resolvedChatGuid)}/typing`, config.password);
|
|
301
|
+
const response = await fetch(url, {
|
|
302
|
+
method: typing ? "POST" : "DELETE",
|
|
303
|
+
signal: AbortSignal.timeout(channelConfig.requestTimeoutMs),
|
|
304
|
+
});
|
|
305
|
+
if (!response.ok) {
|
|
306
|
+
const errorText = await response.text().catch(() => "");
|
|
307
|
+
throw new Error(`BlueBubbles typing failed (${response.status}): ${errorText || "unknown"}`);
|
|
308
|
+
}
|
|
309
|
+
},
|
|
310
|
+
async markChatRead(chat) {
|
|
311
|
+
const resolvedChatGuid = await resolveChatGuid(chat, config, channelConfig);
|
|
312
|
+
if (!resolvedChatGuid) {
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
const url = buildBlueBubblesApiUrl(config.serverUrl, `/api/v1/chat/${encodeURIComponent(resolvedChatGuid)}/read`, config.password);
|
|
316
|
+
const response = await fetch(url, {
|
|
317
|
+
method: "POST",
|
|
318
|
+
signal: AbortSignal.timeout(channelConfig.requestTimeoutMs),
|
|
319
|
+
});
|
|
320
|
+
if (!response.ok) {
|
|
321
|
+
const errorText = await response.text().catch(() => "");
|
|
322
|
+
throw new Error(`BlueBubbles read failed (${response.status}): ${errorText || "unknown"}`);
|
|
323
|
+
}
|
|
324
|
+
},
|
|
186
325
|
async repairEvent(event) {
|
|
187
326
|
if (!event.requiresRepair) {
|
|
188
327
|
(0, runtime_1.emitNervesEvent)({
|
|
@@ -247,7 +386,22 @@ function createBlueBubblesClient(config = (0, config_1.getBlueBubblesConfig)(),
|
|
|
247
386
|
type: event.eventType,
|
|
248
387
|
data,
|
|
249
388
|
});
|
|
250
|
-
|
|
389
|
+
let hydrated = hydrateTextForAgent(normalized, data);
|
|
390
|
+
if (hydrated.kind === "message" &&
|
|
391
|
+
hydrated.balloonBundleId !== "com.apple.messages.URLBalloonProvider" &&
|
|
392
|
+
hydrated.attachments.length > 0) {
|
|
393
|
+
const media = await (0, bluebubbles_media_1.hydrateBlueBubblesAttachments)(hydrated.attachments, config, channelConfig, {
|
|
394
|
+
preferAudioInput: providerSupportsAudioInput((0, identity_1.loadAgentConfig)().provider),
|
|
395
|
+
});
|
|
396
|
+
const transcriptSuffix = media.transcriptAdditions.map((entry) => `[${entry}]`).join("\n");
|
|
397
|
+
const noticeSuffix = media.notices.map((entry) => `[${entry}]`).join("\n");
|
|
398
|
+
const combinedSuffix = [transcriptSuffix, noticeSuffix].filter(Boolean).join("\n");
|
|
399
|
+
hydrated = {
|
|
400
|
+
...hydrated,
|
|
401
|
+
inputPartsForAgent: media.inputParts.length > 0 ? media.inputParts : undefined,
|
|
402
|
+
textForAgent: [hydrated.textForAgent, combinedSuffix].filter(Boolean).join("\n"),
|
|
403
|
+
};
|
|
404
|
+
}
|
|
251
405
|
(0, runtime_1.emitNervesEvent)({
|
|
252
406
|
component: "senses",
|
|
253
407
|
event: "senses.bluebubbles_repair_end",
|
|
@@ -0,0 +1,244 @@
|
|
|
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.hydrateBlueBubblesAttachments = hydrateBlueBubblesAttachments;
|
|
37
|
+
const node_child_process_1 = require("node:child_process");
|
|
38
|
+
const fs = __importStar(require("node:fs/promises"));
|
|
39
|
+
const os = __importStar(require("node:os"));
|
|
40
|
+
const path = __importStar(require("node:path"));
|
|
41
|
+
const runtime_1 = require("../nerves/runtime");
|
|
42
|
+
const MAX_ATTACHMENT_BYTES = 8 * 1024 * 1024;
|
|
43
|
+
const AUDIO_EXTENSIONS = new Set([".mp3", ".wav", ".m4a", ".caf", ".ogg"]);
|
|
44
|
+
const IMAGE_EXTENSIONS = new Set([".jpg", ".jpeg", ".png", ".gif", ".webp", ".heic", ".heif"]);
|
|
45
|
+
const AUDIO_EXTENSION_BY_CONTENT_TYPE = {
|
|
46
|
+
"audio/wav": ".wav",
|
|
47
|
+
"audio/x-wav": ".wav",
|
|
48
|
+
"audio/mp3": ".mp3",
|
|
49
|
+
"audio/mpeg": ".mp3",
|
|
50
|
+
"audio/x-caf": ".caf",
|
|
51
|
+
"audio/caf": ".caf",
|
|
52
|
+
"audio/mp4": ".m4a",
|
|
53
|
+
"audio/x-m4a": ".m4a",
|
|
54
|
+
};
|
|
55
|
+
const AUDIO_INPUT_FORMAT_BY_CONTENT_TYPE = {
|
|
56
|
+
"audio/wav": "wav",
|
|
57
|
+
"audio/x-wav": "wav",
|
|
58
|
+
"audio/mp3": "mp3",
|
|
59
|
+
"audio/mpeg": "mp3",
|
|
60
|
+
};
|
|
61
|
+
const AUDIO_INPUT_FORMAT_BY_EXTENSION = {
|
|
62
|
+
".wav": "wav",
|
|
63
|
+
".mp3": "mp3",
|
|
64
|
+
};
|
|
65
|
+
function buildBlueBubblesApiUrl(baseUrl, endpoint, password) {
|
|
66
|
+
const root = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
|
|
67
|
+
const url = new URL(endpoint.replace(/^\//, ""), root);
|
|
68
|
+
url.searchParams.set("password", password);
|
|
69
|
+
return url.toString();
|
|
70
|
+
}
|
|
71
|
+
function describeAttachment(attachment) {
|
|
72
|
+
return attachment.transferName?.trim() || attachment.guid?.trim() || "attachment";
|
|
73
|
+
}
|
|
74
|
+
function inferContentType(attachment, responseType) {
|
|
75
|
+
const normalizedResponseType = responseType?.split(";")[0]?.trim().toLowerCase();
|
|
76
|
+
if (normalizedResponseType) {
|
|
77
|
+
return normalizedResponseType;
|
|
78
|
+
}
|
|
79
|
+
return attachment.mimeType?.trim().toLowerCase() || undefined;
|
|
80
|
+
}
|
|
81
|
+
function isImageAttachment(attachment, contentType) {
|
|
82
|
+
if (contentType?.startsWith("image/"))
|
|
83
|
+
return true;
|
|
84
|
+
const extension = path.extname(attachment.transferName ?? "").toLowerCase();
|
|
85
|
+
return IMAGE_EXTENSIONS.has(extension);
|
|
86
|
+
}
|
|
87
|
+
function isAudioAttachment(attachment, contentType) {
|
|
88
|
+
if (contentType?.startsWith("audio/"))
|
|
89
|
+
return true;
|
|
90
|
+
const extension = path.extname(attachment.transferName ?? "").toLowerCase();
|
|
91
|
+
return AUDIO_EXTENSIONS.has(extension);
|
|
92
|
+
}
|
|
93
|
+
function sanitizeFilename(name) {
|
|
94
|
+
return path.basename(name).replace(/[\r\n"\\]/g, "_");
|
|
95
|
+
}
|
|
96
|
+
function fileExtensionForAudio(attachment, contentType) {
|
|
97
|
+
const transferExt = path.extname(attachment.transferName ?? "").toLowerCase();
|
|
98
|
+
if (transferExt) {
|
|
99
|
+
return transferExt;
|
|
100
|
+
}
|
|
101
|
+
if (contentType && AUDIO_EXTENSION_BY_CONTENT_TYPE[contentType]) {
|
|
102
|
+
return AUDIO_EXTENSION_BY_CONTENT_TYPE[contentType];
|
|
103
|
+
}
|
|
104
|
+
return ".audio";
|
|
105
|
+
}
|
|
106
|
+
function audioFormatForInput(contentType, attachment) {
|
|
107
|
+
const extension = path.extname(attachment?.transferName ?? "").toLowerCase();
|
|
108
|
+
return AUDIO_INPUT_FORMAT_BY_CONTENT_TYPE[contentType ?? ""] ?? AUDIO_INPUT_FORMAT_BY_EXTENSION[extension];
|
|
109
|
+
}
|
|
110
|
+
async function transcribeAudioWithWhisper(params) {
|
|
111
|
+
const workDir = await fs.mkdtemp(path.join(os.tmpdir(), "ouro-bb-audio-"));
|
|
112
|
+
const filename = sanitizeFilename(describeAttachment(params.attachment));
|
|
113
|
+
const extension = fileExtensionForAudio(params.attachment, params.contentType);
|
|
114
|
+
const audioPath = path.join(workDir, `${path.parse(filename).name}${extension}`);
|
|
115
|
+
try {
|
|
116
|
+
await fs.writeFile(audioPath, params.buffer);
|
|
117
|
+
await new Promise((resolve, reject) => {
|
|
118
|
+
(0, node_child_process_1.execFile)("whisper", [
|
|
119
|
+
audioPath,
|
|
120
|
+
"--model",
|
|
121
|
+
"turbo",
|
|
122
|
+
"--output_dir",
|
|
123
|
+
workDir,
|
|
124
|
+
"--output_format",
|
|
125
|
+
"json",
|
|
126
|
+
"--verbose",
|
|
127
|
+
"False",
|
|
128
|
+
], { timeout: Math.max(params.timeoutMs, 120000) }, (error) => {
|
|
129
|
+
if (error) {
|
|
130
|
+
reject(error);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
resolve();
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
const transcriptPath = path.join(workDir, `${path.parse(audioPath).name}.json`);
|
|
137
|
+
const raw = await fs.readFile(transcriptPath, "utf8");
|
|
138
|
+
const parsed = JSON.parse(raw);
|
|
139
|
+
return typeof parsed.text === "string" ? parsed.text.trim() : "";
|
|
140
|
+
}
|
|
141
|
+
finally {
|
|
142
|
+
await fs.rm(workDir, { recursive: true, force: true }).catch(() => undefined);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
async function downloadAttachment(attachment, config, channelConfig, fetchImpl) {
|
|
146
|
+
const guid = attachment.guid?.trim();
|
|
147
|
+
if (!guid) {
|
|
148
|
+
throw new Error("attachment guid missing");
|
|
149
|
+
}
|
|
150
|
+
if (typeof attachment.totalBytes === "number" && attachment.totalBytes > MAX_ATTACHMENT_BYTES) {
|
|
151
|
+
throw new Error(`attachment exceeds ${MAX_ATTACHMENT_BYTES} byte limit`);
|
|
152
|
+
}
|
|
153
|
+
const url = buildBlueBubblesApiUrl(config.serverUrl, `/api/v1/attachment/${encodeURIComponent(guid)}/download`, config.password);
|
|
154
|
+
const response = await fetchImpl(url, {
|
|
155
|
+
method: "GET",
|
|
156
|
+
signal: AbortSignal.timeout(channelConfig.requestTimeoutMs),
|
|
157
|
+
});
|
|
158
|
+
if (!response.ok) {
|
|
159
|
+
throw new Error(`HTTP ${response.status}`);
|
|
160
|
+
}
|
|
161
|
+
const buffer = Buffer.from(await response.arrayBuffer());
|
|
162
|
+
if (buffer.length > MAX_ATTACHMENT_BYTES) {
|
|
163
|
+
throw new Error(`attachment exceeds ${MAX_ATTACHMENT_BYTES} byte limit`);
|
|
164
|
+
}
|
|
165
|
+
return {
|
|
166
|
+
buffer,
|
|
167
|
+
contentType: inferContentType(attachment, response.headers.get("content-type")),
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
async function hydrateBlueBubblesAttachments(attachments, config, channelConfig, deps = {}) {
|
|
171
|
+
(0, runtime_1.emitNervesEvent)({
|
|
172
|
+
component: "senses",
|
|
173
|
+
event: "senses.bluebubbles_media_hydrate",
|
|
174
|
+
message: "hydrating bluebubbles attachments",
|
|
175
|
+
meta: {
|
|
176
|
+
attachmentCount: attachments.length,
|
|
177
|
+
preferAudioInput: deps.preferAudioInput ?? false,
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
181
|
+
const transcribeAudio = deps.transcribeAudio ?? transcribeAudioWithWhisper;
|
|
182
|
+
const preferAudioInput = deps.preferAudioInput ?? false;
|
|
183
|
+
const inputParts = [];
|
|
184
|
+
const transcriptAdditions = [];
|
|
185
|
+
const notices = [];
|
|
186
|
+
for (const attachment of attachments) {
|
|
187
|
+
const name = describeAttachment(attachment);
|
|
188
|
+
try {
|
|
189
|
+
const downloaded = await downloadAttachment(attachment, config, channelConfig, fetchImpl);
|
|
190
|
+
const base64 = downloaded.buffer.toString("base64");
|
|
191
|
+
if (isImageAttachment(attachment, downloaded.contentType)) {
|
|
192
|
+
inputParts.push({
|
|
193
|
+
type: "image_url",
|
|
194
|
+
image_url: {
|
|
195
|
+
url: `data:${downloaded.contentType ?? "application/octet-stream"};base64,${base64}`,
|
|
196
|
+
detail: "auto",
|
|
197
|
+
},
|
|
198
|
+
});
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
if (isAudioAttachment(attachment, downloaded.contentType)) {
|
|
202
|
+
const audioFormat = audioFormatForInput(downloaded.contentType, attachment);
|
|
203
|
+
if (preferAudioInput && audioFormat) {
|
|
204
|
+
inputParts.push({
|
|
205
|
+
type: "input_audio",
|
|
206
|
+
input_audio: {
|
|
207
|
+
data: base64,
|
|
208
|
+
format: audioFormat,
|
|
209
|
+
},
|
|
210
|
+
});
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
const transcript = (await transcribeAudio({
|
|
214
|
+
attachment,
|
|
215
|
+
buffer: downloaded.buffer,
|
|
216
|
+
contentType: downloaded.contentType,
|
|
217
|
+
timeoutMs: channelConfig.requestTimeoutMs,
|
|
218
|
+
})).trim();
|
|
219
|
+
if (!transcript) {
|
|
220
|
+
notices.push(`attachment hydration failed for ${name}: empty audio transcript`);
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
transcriptAdditions.push(`voice note transcript: ${transcript}`);
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
inputParts.push({
|
|
227
|
+
type: "file",
|
|
228
|
+
file: {
|
|
229
|
+
file_data: base64,
|
|
230
|
+
filename: sanitizeFilename(name),
|
|
231
|
+
},
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
catch (error) {
|
|
235
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
236
|
+
notices.push(`attachment hydration failed for ${name}: ${reason}`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return {
|
|
240
|
+
inputParts,
|
|
241
|
+
transcriptAdditions,
|
|
242
|
+
notices,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
@@ -46,10 +46,12 @@ const tokens_1 = require("../mind/friends/tokens");
|
|
|
46
46
|
const resolver_1 = require("../mind/friends/resolver");
|
|
47
47
|
const store_file_1 = require("../mind/friends/store-file");
|
|
48
48
|
const prompt_1 = require("../mind/prompt");
|
|
49
|
+
const phrases_1 = require("../mind/phrases");
|
|
49
50
|
const runtime_1 = require("../nerves/runtime");
|
|
50
51
|
const bluebubbles_model_1 = require("./bluebubbles-model");
|
|
51
52
|
const bluebubbles_client_1 = require("./bluebubbles-client");
|
|
52
53
|
const bluebubbles_mutation_log_1 = require("./bluebubbles-mutation-log");
|
|
54
|
+
const debug_activity_1 = require("./debug-activity");
|
|
53
55
|
const defaultDeps = {
|
|
54
56
|
getAgentName: identity_1.getAgentName,
|
|
55
57
|
buildSystem: prompt_1.buildSystem,
|
|
@@ -92,10 +94,58 @@ function buildInboundText(event) {
|
|
|
92
94
|
}
|
|
93
95
|
return `${event.sender.displayName}: ${baseText}`;
|
|
94
96
|
}
|
|
97
|
+
function buildInboundContent(event) {
|
|
98
|
+
const text = buildInboundText(event);
|
|
99
|
+
if (event.kind !== "message" || !event.inputPartsForAgent || event.inputPartsForAgent.length === 0) {
|
|
100
|
+
return text;
|
|
101
|
+
}
|
|
102
|
+
return [
|
|
103
|
+
{ type: "text", text },
|
|
104
|
+
...event.inputPartsForAgent,
|
|
105
|
+
];
|
|
106
|
+
}
|
|
95
107
|
function createBlueBubblesCallbacks(client, chat, replyToMessageGuid) {
|
|
96
108
|
let textBuffer = "";
|
|
109
|
+
const phrases = (0, phrases_1.getPhrases)();
|
|
110
|
+
const activity = (0, debug_activity_1.createDebugActivityController)({
|
|
111
|
+
thinkingPhrases: phrases.thinking,
|
|
112
|
+
followupPhrases: phrases.followup,
|
|
113
|
+
transport: {
|
|
114
|
+
sendStatus: async (text) => {
|
|
115
|
+
const sent = await client.sendText({
|
|
116
|
+
chat,
|
|
117
|
+
text,
|
|
118
|
+
replyToMessageGuid,
|
|
119
|
+
});
|
|
120
|
+
return sent.messageGuid;
|
|
121
|
+
},
|
|
122
|
+
editStatus: async (_messageGuid, text) => {
|
|
123
|
+
await client.sendText({
|
|
124
|
+
chat,
|
|
125
|
+
text,
|
|
126
|
+
replyToMessageGuid,
|
|
127
|
+
});
|
|
128
|
+
},
|
|
129
|
+
setTyping: async (active) => {
|
|
130
|
+
await client.setTyping(chat, active);
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
onTransportError: (operation, error) => {
|
|
134
|
+
(0, runtime_1.emitNervesEvent)({
|
|
135
|
+
level: "warn",
|
|
136
|
+
component: "senses",
|
|
137
|
+
event: "senses.bluebubbles_activity_error",
|
|
138
|
+
message: "bluebubbles activity transport failed",
|
|
139
|
+
meta: {
|
|
140
|
+
operation,
|
|
141
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
},
|
|
145
|
+
});
|
|
97
146
|
return {
|
|
98
147
|
onModelStart() {
|
|
148
|
+
activity.onModelStart();
|
|
99
149
|
(0, runtime_1.emitNervesEvent)({
|
|
100
150
|
component: "senses",
|
|
101
151
|
event: "senses.bluebubbles_turn_start",
|
|
@@ -112,10 +162,12 @@ function createBlueBubblesCallbacks(client, chat, replyToMessageGuid) {
|
|
|
112
162
|
});
|
|
113
163
|
},
|
|
114
164
|
onTextChunk(text) {
|
|
165
|
+
activity.onTextChunk(text);
|
|
115
166
|
textBuffer += text;
|
|
116
167
|
},
|
|
117
168
|
onReasoningChunk(_text) { },
|
|
118
169
|
onToolStart(name, _args) {
|
|
170
|
+
activity.onToolStart(name, _args);
|
|
119
171
|
(0, runtime_1.emitNervesEvent)({
|
|
120
172
|
component: "senses",
|
|
121
173
|
event: "senses.bluebubbles_tool_start",
|
|
@@ -124,6 +176,7 @@ function createBlueBubblesCallbacks(client, chat, replyToMessageGuid) {
|
|
|
124
176
|
});
|
|
125
177
|
},
|
|
126
178
|
onToolEnd(name, summary, success) {
|
|
179
|
+
activity.onToolEnd(name, summary, success);
|
|
127
180
|
(0, runtime_1.emitNervesEvent)({
|
|
128
181
|
component: "senses",
|
|
129
182
|
event: "senses.bluebubbles_tool_end",
|
|
@@ -132,6 +185,7 @@ function createBlueBubblesCallbacks(client, chat, replyToMessageGuid) {
|
|
|
132
185
|
});
|
|
133
186
|
},
|
|
134
187
|
onError(error, severity) {
|
|
188
|
+
activity.onError(error);
|
|
135
189
|
(0, runtime_1.emitNervesEvent)({
|
|
136
190
|
level: severity === "terminal" ? "error" : "warn",
|
|
137
191
|
component: "senses",
|
|
@@ -144,8 +198,10 @@ function createBlueBubblesCallbacks(client, chat, replyToMessageGuid) {
|
|
|
144
198
|
textBuffer = "";
|
|
145
199
|
},
|
|
146
200
|
async flush() {
|
|
201
|
+
await activity.drain();
|
|
147
202
|
const trimmed = textBuffer.trim();
|
|
148
203
|
if (!trimmed) {
|
|
204
|
+
await activity.finish();
|
|
149
205
|
return;
|
|
150
206
|
}
|
|
151
207
|
textBuffer = "";
|
|
@@ -154,6 +210,10 @@ function createBlueBubblesCallbacks(client, chat, replyToMessageGuid) {
|
|
|
154
210
|
text: trimmed,
|
|
155
211
|
replyToMessageGuid,
|
|
156
212
|
});
|
|
213
|
+
await activity.finish();
|
|
214
|
+
},
|
|
215
|
+
async finish() {
|
|
216
|
+
await activity.finish();
|
|
157
217
|
},
|
|
158
218
|
};
|
|
159
219
|
}
|
|
@@ -227,6 +287,15 @@ async function handleBlueBubblesEvent(payload, deps = {}) {
|
|
|
227
287
|
friendStore: store,
|
|
228
288
|
summarize: (0, core_1.createSummarize)(),
|
|
229
289
|
context,
|
|
290
|
+
codingFeedback: {
|
|
291
|
+
send: async (message) => {
|
|
292
|
+
await client.sendText({
|
|
293
|
+
chat: event.chat,
|
|
294
|
+
text: message,
|
|
295
|
+
replyToMessageGuid: event.kind === "message" ? event.messageGuid : undefined,
|
|
296
|
+
});
|
|
297
|
+
},
|
|
298
|
+
},
|
|
230
299
|
};
|
|
231
300
|
const friendId = context.friend.id;
|
|
232
301
|
const sessPath = resolvedDeps.sessionPath(friendId, "bluebubbles", event.chat.sessionKey);
|
|
@@ -234,31 +303,51 @@ async function handleBlueBubblesEvent(payload, deps = {}) {
|
|
|
234
303
|
const messages = existing?.messages && existing.messages.length > 0
|
|
235
304
|
? existing.messages
|
|
236
305
|
: [{ role: "system", content: await resolvedDeps.buildSystem("bluebubbles", undefined, context) }];
|
|
237
|
-
messages.push({ role: "user", content:
|
|
306
|
+
messages.push({ role: "user", content: buildInboundContent(event) });
|
|
238
307
|
const callbacks = createBlueBubblesCallbacks(client, event.chat, event.kind === "message" ? event.messageGuid : undefined);
|
|
239
308
|
const controller = new AbortController();
|
|
240
309
|
const agentOptions = {
|
|
241
310
|
toolContext,
|
|
242
311
|
};
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
312
|
+
try {
|
|
313
|
+
const result = await resolvedDeps.runAgent(messages, callbacks, "bluebubbles", controller.signal, agentOptions);
|
|
314
|
+
await callbacks.flush();
|
|
315
|
+
resolvedDeps.postTurn(messages, sessPath, result.usage);
|
|
316
|
+
await resolvedDeps.accumulateFriendTokens(store, friendId, result.usage);
|
|
317
|
+
try {
|
|
318
|
+
await client.markChatRead(event.chat);
|
|
319
|
+
}
|
|
320
|
+
catch (error) {
|
|
321
|
+
(0, runtime_1.emitNervesEvent)({
|
|
322
|
+
level: "warn",
|
|
323
|
+
component: "senses",
|
|
324
|
+
event: "senses.bluebubbles_mark_read_error",
|
|
325
|
+
message: "failed to mark bluebubbles chat as read",
|
|
326
|
+
meta: {
|
|
327
|
+
chatGuid: event.chat.chatGuid ?? null,
|
|
328
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
329
|
+
},
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
(0, runtime_1.emitNervesEvent)({
|
|
333
|
+
component: "senses",
|
|
334
|
+
event: "senses.bluebubbles_turn_end",
|
|
335
|
+
message: "bluebubbles event handled",
|
|
336
|
+
meta: {
|
|
337
|
+
messageGuid: event.messageGuid,
|
|
338
|
+
kind: event.kind,
|
|
339
|
+
sessionKey: event.chat.sessionKey,
|
|
340
|
+
},
|
|
341
|
+
});
|
|
342
|
+
return {
|
|
343
|
+
handled: true,
|
|
344
|
+
notifiedAgent: true,
|
|
253
345
|
kind: event.kind,
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
notifiedAgent: true,
|
|
260
|
-
kind: event.kind,
|
|
261
|
-
};
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
finally {
|
|
349
|
+
await callbacks.finish();
|
|
350
|
+
}
|
|
262
351
|
}
|
|
263
352
|
function createBlueBubblesWebhookHandler(deps = {}) {
|
|
264
353
|
return async (req, res) => {
|