@ouro.bot/cli 0.1.0-alpha.2 → 0.1.0-alpha.21

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.
Files changed (58) hide show
  1. package/AdoptionSpecialist.ouro/agent.json +70 -9
  2. package/AdoptionSpecialist.ouro/psyche/SOUL.md +5 -2
  3. package/AdoptionSpecialist.ouro/psyche/identities/monty.md +2 -2
  4. package/assets/ouroboros.png +0 -0
  5. package/dist/heart/config.js +66 -4
  6. package/dist/heart/core.js +75 -2
  7. package/dist/heart/daemon/daemon-cli.js +523 -33
  8. package/dist/heart/daemon/daemon-entry.js +13 -5
  9. package/dist/heart/daemon/daemon-runtime-sync.js +90 -0
  10. package/dist/heart/daemon/daemon.js +42 -9
  11. package/dist/heart/daemon/hatch-animation.js +35 -0
  12. package/dist/heart/daemon/hatch-flow.js +2 -11
  13. package/dist/heart/daemon/hatch-specialist.js +6 -1
  14. package/dist/heart/daemon/ouro-bot-wrapper.js +4 -3
  15. package/dist/heart/daemon/ouro-path-installer.js +178 -0
  16. package/dist/heart/daemon/ouro-uti.js +11 -2
  17. package/dist/heart/daemon/process-manager.js +1 -1
  18. package/dist/heart/daemon/runtime-logging.js +9 -5
  19. package/dist/heart/daemon/runtime-metadata.js +118 -0
  20. package/dist/heart/daemon/sense-manager.js +266 -0
  21. package/dist/heart/daemon/specialist-orchestrator.js +129 -0
  22. package/dist/heart/daemon/specialist-prompt.js +98 -0
  23. package/dist/heart/daemon/specialist-tools.js +237 -0
  24. package/dist/heart/daemon/subagent-installer.js +10 -1
  25. package/dist/heart/daemon/wrapper-publish-guard.js +48 -0
  26. package/dist/heart/identity.js +77 -1
  27. package/dist/heart/providers/anthropic.js +19 -2
  28. package/dist/heart/sense-truth.js +61 -0
  29. package/dist/heart/streaming.js +99 -21
  30. package/dist/mind/bundle-manifest.js +58 -0
  31. package/dist/mind/friends/channel.js +8 -0
  32. package/dist/mind/friends/types.js +1 -1
  33. package/dist/mind/prompt.js +77 -3
  34. package/dist/nerves/cli-logging.js +15 -2
  35. package/dist/repertoire/ado-client.js +4 -2
  36. package/dist/repertoire/coding/feedback.js +134 -0
  37. package/dist/repertoire/coding/index.js +4 -1
  38. package/dist/repertoire/coding/manager.js +61 -2
  39. package/dist/repertoire/coding/spawner.js +3 -3
  40. package/dist/repertoire/coding/tools.js +41 -2
  41. package/dist/repertoire/data/ado-endpoints.json +188 -0
  42. package/dist/repertoire/tools-base.js +69 -5
  43. package/dist/repertoire/tools-teams.js +57 -4
  44. package/dist/repertoire/tools.js +44 -11
  45. package/dist/senses/bluebubbles-client.js +433 -0
  46. package/dist/senses/bluebubbles-entry.js +11 -0
  47. package/dist/senses/bluebubbles-media.js +244 -0
  48. package/dist/senses/bluebubbles-model.js +253 -0
  49. package/dist/senses/bluebubbles-mutation-log.js +76 -0
  50. package/dist/senses/bluebubbles.js +421 -0
  51. package/dist/senses/cli.js +293 -133
  52. package/dist/senses/debug-activity.js +107 -0
  53. package/dist/senses/teams.js +173 -54
  54. package/package.json +11 -4
  55. package/subagents/work-doer.md +26 -24
  56. package/subagents/work-merger.md +24 -30
  57. package/subagents/work-planner.md +34 -25
  58. package/dist/inner-worker-entry.js +0 -4
@@ -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
+ }
@@ -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
+ }