@ouro.bot/cli 0.1.0-alpha.15 → 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.
@@ -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
- if (!params.chat.chatGuid) {
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: params.chat.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: params.chat.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: params.chat.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
- const hydrated = hydrateTextForAgent(normalized, data);
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: buildInboundText(event) });
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
- 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,
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
- sessionKey: event.chat.sessionKey,
255
- },
256
- });
257
- return {
258
- handled: true,
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) => {