@kingcrab/pi-imessage 0.0.1

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 (75) hide show
  1. package/README.md +120 -0
  2. package/dist/agent.d.ts +22 -0
  3. package/dist/agent.d.ts.map +1 -0
  4. package/dist/agent.js +341 -0
  5. package/dist/agent.js.map +1 -0
  6. package/dist/cli.d.ts +15 -0
  7. package/dist/cli.d.ts.map +1 -0
  8. package/dist/cli.js +179 -0
  9. package/dist/cli.js.map +1 -0
  10. package/dist/imessage.d.ts +32 -0
  11. package/dist/imessage.d.ts.map +1 -0
  12. package/dist/imessage.js +65 -0
  13. package/dist/imessage.js.map +1 -0
  14. package/dist/logger.d.ts +33 -0
  15. package/dist/logger.d.ts.map +1 -0
  16. package/dist/logger.js +103 -0
  17. package/dist/logger.js.map +1 -0
  18. package/dist/main.d.ts +5 -0
  19. package/dist/main.d.ts.map +1 -0
  20. package/dist/main.js +66 -0
  21. package/dist/main.js.map +1 -0
  22. package/dist/pipeline.d.ts +26 -0
  23. package/dist/pipeline.d.ts.map +1 -0
  24. package/dist/pipeline.js +48 -0
  25. package/dist/pipeline.js.map +1 -0
  26. package/dist/queue.d.ts +16 -0
  27. package/dist/queue.d.ts.map +1 -0
  28. package/dist/queue.js +47 -0
  29. package/dist/queue.js.map +1 -0
  30. package/dist/self-echo.d.ts +19 -0
  31. package/dist/self-echo.d.ts.map +1 -0
  32. package/dist/self-echo.js +54 -0
  33. package/dist/self-echo.js.map +1 -0
  34. package/dist/send.d.ts +17 -0
  35. package/dist/send.d.ts.map +1 -0
  36. package/dist/send.js +107 -0
  37. package/dist/send.js.map +1 -0
  38. package/dist/settings.d.ts +39 -0
  39. package/dist/settings.d.ts.map +1 -0
  40. package/dist/settings.js +74 -0
  41. package/dist/settings.js.map +1 -0
  42. package/dist/store.d.ts +41 -0
  43. package/dist/store.d.ts.map +1 -0
  44. package/dist/store.js +72 -0
  45. package/dist/store.js.map +1 -0
  46. package/dist/tasks.d.ts +88 -0
  47. package/dist/tasks.d.ts.map +1 -0
  48. package/dist/tasks.js +260 -0
  49. package/dist/tasks.js.map +1 -0
  50. package/dist/types.d.ts +77 -0
  51. package/dist/types.d.ts.map +1 -0
  52. package/dist/types.js +24 -0
  53. package/dist/types.js.map +1 -0
  54. package/dist/watch.d.ts +21 -0
  55. package/dist/watch.d.ts.map +1 -0
  56. package/dist/watch.js +137 -0
  57. package/dist/watch.js.map +1 -0
  58. package/dist/web/data.d.ts +11 -0
  59. package/dist/web/data.d.ts.map +1 -0
  60. package/dist/web/data.js +56 -0
  61. package/dist/web/data.js.map +1 -0
  62. package/dist/web/html.d.ts +5 -0
  63. package/dist/web/html.d.ts.map +1 -0
  64. package/dist/web/html.js +16 -0
  65. package/dist/web/html.js.map +1 -0
  66. package/dist/web/index.d.ts +15 -0
  67. package/dist/web/index.d.ts.map +1 -0
  68. package/dist/web/index.js +116 -0
  69. package/dist/web/index.js.map +1 -0
  70. package/dist/web/render.d.ts +5 -0
  71. package/dist/web/render.d.ts.map +1 -0
  72. package/dist/web/render.js +50 -0
  73. package/dist/web/render.js.map +1 -0
  74. package/dist/web/templates/page.eta +85 -0
  75. package/package.json +42 -0
package/dist/tasks.js ADDED
@@ -0,0 +1,260 @@
1
+ /**
2
+ * Pipeline task factories — each function creates a task for a specific
3
+ * pipeline phase. Tasks are pure functions with injected dependencies;
4
+ * the bot registers them without containing any business logic itself.
5
+ *
6
+ * before:
7
+ * logIncoming — logs the received message
8
+ * dropSelfEcho — drops messages that are echoes of the bot's own replies
9
+ * storeIncoming — persists the incoming message to log.jsonl
10
+ * checkReplyEnabled — drops messages when reply is disabled by settings
11
+ * downloadImages — downloads image attachments and populates incoming.images
12
+ * resizeImages — resizes oversized images via macOS sips
13
+ *
14
+ * start:
15
+ * commandHandler — intercepts slash commands (/new, /status) before the agent
16
+ * callAgent — sends the message to the agent and yields replies as they arrive
17
+ *
18
+ * end:
19
+ * sendReply — remembers echo, sends reply via BlueBubbles
20
+ * logOutgoing — logs the outgoing reply
21
+ */
22
+ import { readFile } from "node:fs/promises";
23
+ import sharp from "sharp";
24
+ import { isReplyEnabled } from "./settings.js";
25
+ import { formatAgentReply } from "./types.js";
26
+ // ── Helpers ───────────────────────────────────────────────────────────────────
27
+ function messageTypeLabel(msg) {
28
+ if (msg.messageType === "group")
29
+ return "GROUP";
30
+ return msg.messageType === "sms" ? "SMS" : "DM";
31
+ }
32
+ function formatTarget(msg) {
33
+ return msg.messageType === "group" ? `${msg.groupName}|${msg.sender}` : msg.sender;
34
+ }
35
+ // ── before tasks ──────────────────────────────────────────────────────────────
36
+ /**
37
+ * Log the incoming message to console. Always passes through.
38
+ *
39
+ * Examples:
40
+ * [sid] <- [DM] +16501234567: hey what's up
41
+ * [sid] <- [SMS] +16501234567: can you call me
42
+ * [sid] <- [GROUP] Family|+16501234567: dinner at 6? [2 attachment(s)]
43
+ */
44
+ export function createLogIncomingTask(digestLogger) {
45
+ return (incoming, outgoing) => {
46
+ const label = messageTypeLabel(incoming);
47
+ const target = formatTarget(incoming);
48
+ const attachmentNote = incoming.attachments.length > 0 ? ` [${incoming.attachments.length} attachment(s)]` : "";
49
+ digestLogger.log(`[sid] <- [${label}] ${target}: ${(incoming.text ?? "(attachment)").substring(0, 80)}${attachmentNote}`);
50
+ return outgoing;
51
+ };
52
+ }
53
+ /** Persist the incoming message to log.jsonl. Always passes through. */
54
+ export function createStoreIncomingTask(store) {
55
+ return (incoming, outgoing) => {
56
+ store.logIncoming(incoming).catch((error) => {
57
+ console.error(`[sid] failed to store incoming message for ${incoming.chatGuid}:`, error);
58
+ });
59
+ return outgoing;
60
+ };
61
+ }
62
+ /** Drop messages that are echoes of the bot's own replies. */
63
+ export function createDropSelfEchoTask(echoFilter) {
64
+ return (incoming, outgoing) => {
65
+ if (incoming.text && echoFilter.isEcho(incoming.chatGuid, incoming.text)) {
66
+ console.warn(`[sid] drop self-echo ${incoming.chatGuid}: ${incoming.text.substring(0, 40)}`);
67
+ return { ...outgoing, shouldContinue: false };
68
+ }
69
+ return outgoing;
70
+ };
71
+ }
72
+ /**
73
+ * Drop messages when reply is disabled for this chat by settings.
74
+ *
75
+ * Resolution priority (highest to lowest):
76
+ * blacklist["chatGuid"] > whitelist["chatGuid"] > blacklist["*"] > whitelist["*"]
77
+ *
78
+ * Examples:
79
+ * whitelist: ["*"] → reply to everyone
80
+ * whitelist: ["1"] → reply only to "1"
81
+ * whitelist: ["*"], bl: ["2"] → reply to everyone except "2"
82
+ * blacklist: ["*"] → log-only for all
83
+ * whitelist: ["1"], bl: ["*"] → reply only to "1"
84
+ * whitelist: ["1"], bl: ["1"] → no reply (blacklist wins)
85
+ */
86
+ export function createCheckReplyEnabledTask(getSettings) {
87
+ return (incoming, outgoing) => {
88
+ if (!isReplyEnabled(getSettings(), incoming.chatGuid)) {
89
+ console.log(`[sid] reply disabled for ${incoming.chatGuid}, log-only`);
90
+ return { ...outgoing, shouldContinue: false };
91
+ }
92
+ return outgoing;
93
+ };
94
+ }
95
+ // ── command tasks ─────────────────────────────────────────────────────────────
96
+ /**
97
+ * Intercept slash commands (e.g. "/new", "/status") before they reach the agent.
98
+ * Sets shouldContinue=false on the outgoing message to skip subsequent start tasks.
99
+ *
100
+ * Supported commands:
101
+ * /new — reset the agent session for this chat (equivalent to /new in pi coding agent).
102
+ * /status — show session stats: tokens, cost, context usage, model, thinking level.
103
+ */
104
+ export function createCommandHandlerTask(agent) {
105
+ return async (incoming, outgoing, dispatch) => {
106
+ const text = incoming.text?.trim();
107
+ if (text === "/new") {
108
+ await agent.newSession(incoming.chatGuid);
109
+ const newSessionReply = "✓ New session started";
110
+ console.log(`[sid] /new command: ${incoming.chatGuid} → ${newSessionReply}`);
111
+ await dispatch({ ...outgoing, reply: { type: "message", text: newSessionReply } });
112
+ const statusReply = await agent.getSessionStatus(incoming.chatGuid);
113
+ console.log(`[sid] /new status: ${incoming.chatGuid} → ${statusReply}`);
114
+ await dispatch({ ...outgoing, reply: { type: "message", text: statusReply } });
115
+ outgoing.shouldContinue = false;
116
+ return;
117
+ }
118
+ if (text === "/status") {
119
+ const replyText = await agent.getSessionStatus(incoming.chatGuid);
120
+ console.log(`[sid] /status command: ${incoming.chatGuid} → ${replyText}`);
121
+ await dispatch({ ...outgoing, reply: { type: "message", text: replyText } });
122
+ outgoing.shouldContinue = false;
123
+ return;
124
+ }
125
+ };
126
+ }
127
+ // ── start tasks ───────────────────────────────────────────────────────────────
128
+ /**
129
+ * Read image attachments from local disk and populate incoming.images in-place.
130
+ * Non-image attachments are skipped; failed reads are logged and silently skipped.
131
+ */
132
+ export function createDownloadImagesTask() {
133
+ return async (incoming, outgoing) => {
134
+ const images = [];
135
+ for (const attachment of incoming.attachments) {
136
+ const mimeType = attachment.mimeType;
137
+ if (!mimeType?.startsWith("image/")) {
138
+ console.warn(`[sid] skipping non-image attachment ${attachment.path} (mimeType: ${mimeType ?? "null"})`);
139
+ continue;
140
+ }
141
+ try {
142
+ const bytes = await readFile(attachment.path);
143
+ images.push({ type: "image", mimeType, data: bytes.toString("base64") });
144
+ }
145
+ catch (error) {
146
+ console.error(`[sid] failed to read image attachment ${attachment.path}:`, error);
147
+ }
148
+ }
149
+ incoming.images = images;
150
+ return outgoing;
151
+ };
152
+ }
153
+ /**
154
+ * Resize images whose longest edge exceeds MAX_EDGE_PX using sharp.
155
+ * Converts to JPEG at 80% quality to keep size well under Anthropic's 5MB limit.
156
+ *
157
+ * before: raw downloaded image (any size, any format)
158
+ * after: JPEG ≤ MAX_EDGE_PX on longest edge, 80% quality
159
+ *
160
+ * Images that are already within the limit are left untouched.
161
+ * Resize failures are logged and the original image is kept as-is.
162
+ */
163
+ const MAX_EDGE_PX = 1024;
164
+ export function createResizeImagesTask() {
165
+ return async (incoming, outgoing) => {
166
+ const resized = [];
167
+ for (const image of incoming.images) {
168
+ try {
169
+ resized.push(await resizeImageIfNeeded(image));
170
+ }
171
+ catch (error) {
172
+ console.error("[sid] failed to resize image, keeping original:", error);
173
+ resized.push(image);
174
+ }
175
+ }
176
+ incoming.images = resized;
177
+ return outgoing;
178
+ };
179
+ }
180
+ /**
181
+ * Resize a single image if its longest edge exceeds MAX_EDGE_PX.
182
+ * Uses sharp to resample and convert to JPEG at 80% quality.
183
+ * Returns the original image unchanged if already within limits.
184
+ */
185
+ async function resizeImageIfNeeded(image) {
186
+ const originalBytes = Buffer.from(image.data, "base64");
187
+ const originalSizeKB = (originalBytes.length / 1024).toFixed(0);
188
+ const metadata = await sharp(originalBytes).metadata();
189
+ const width = metadata.width ?? 0;
190
+ const height = metadata.height ?? 0;
191
+ const longestEdge = Math.max(width, height);
192
+ if (longestEdge <= MAX_EDGE_PX) {
193
+ return image;
194
+ }
195
+ const resizedBytes = await sharp(originalBytes)
196
+ .resize({ width: MAX_EDGE_PX, height: MAX_EDGE_PX, fit: "inside" })
197
+ .jpeg({ quality: 80 })
198
+ .toBuffer();
199
+ const resizedSizeKB = (resizedBytes.length / 1024).toFixed(0);
200
+ console.log(`[sid] resized image: ${width}x${height} ${originalSizeKB}KB → ${MAX_EDGE_PX}px ${resizedSizeKB}KB`);
201
+ return { type: "image", mimeType: "image/jpeg", data: resizedBytes.toString("base64") };
202
+ }
203
+ /** Send the message to the agent and dispatch a reply for each agent turn. */
204
+ export function createCallAgentTask(agent) {
205
+ return async (incoming, outgoing, dispatch) => {
206
+ await agent.processMessage(incoming, async (agentReply) => {
207
+ const text = formatAgentReply(agentReply);
208
+ await dispatch({ ...outgoing, reply: { type: "message", text } });
209
+ });
210
+ };
211
+ }
212
+ // ── end tasks ─────────────────────────────────────────────────────────────────
213
+ /** Remember echo and send reply via Messages.app AppleScript. */
214
+ export function createSendReplyTask(echoFilter, sender) {
215
+ return async (incoming, outgoing) => {
216
+ const { reply, sendReply } = outgoing;
217
+ if (!sendReply)
218
+ return outgoing;
219
+ if (reply.type === "message") {
220
+ echoFilter.remember(incoming.chatGuid, reply.text);
221
+ await sender.sendMessage(incoming.chatGuid, reply.text);
222
+ }
223
+ return outgoing;
224
+ };
225
+ }
226
+ /**
227
+ * Log the outgoing reply to console.
228
+ *
229
+ * Examples:
230
+ * [sid] -> [DM] +16501234567: sure, I'll check
231
+ * [sid] -> [SMS] +16501234567: got it
232
+ * [sid] -> [GROUP] Family: sounds good!
233
+ * [sid] -> [DM] +16501234567: (reaction: love)
234
+ */
235
+ export function createLogOutgoingTask(digestLogger) {
236
+ return (incoming, outgoing) => {
237
+ const { reply } = outgoing;
238
+ if (reply.type === "message") {
239
+ const label = messageTypeLabel(incoming);
240
+ const target = incoming.messageType === "group" ? incoming.groupName : incoming.sender;
241
+ digestLogger.log(`[sid] -> [${label}] ${target}: ${reply.text.substring(0, 80)}`);
242
+ }
243
+ return outgoing;
244
+ };
245
+ }
246
+ /** Persist the outgoing reply to log.jsonl. */
247
+ export function createStoreOutgoingTask(store) {
248
+ return (incoming, outgoing) => {
249
+ const { reply, sendReply } = outgoing;
250
+ if (reply.type === "message") {
251
+ store
252
+ .logOutgoing(incoming.chatGuid, reply.text, incoming.messageType, incoming.groupName, !sendReply)
253
+ .catch((error) => {
254
+ console.error(`[sid] failed to store outgoing message for ${incoming.chatGuid}:`, error);
255
+ });
256
+ }
257
+ return outgoing;
258
+ };
259
+ }
260
+ //# sourceMappingURL=tasks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tasks.js","sourceRoot":"","sources":["../src/tasks.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Shared types for the iMessage bot pipeline.
3
+ */
4
+ import type { ImageContent } from "@mariozechner/pi-ai";
5
+ /**
6
+ * Message type, derived from service and chatGuid structure:
7
+ *
8
+ * service chatGuid segment MessageType
9
+ * ────────── ─────────────── ───────────
10
+ * "SMS" (any) "sms"
11
+ * "iMessage" ";-;" (DM) "imessage"
12
+ * "iMessage" ";+;" (group) "group"
13
+ */
14
+ export type MessageType = "sms" | "imessage" | "group";
15
+ /** Attachment metadata — local path on disk. */
16
+ export interface Attachment {
17
+ /** Local file path (e.g. ~/Library/Messages/Attachments/...). */
18
+ path: string;
19
+ mimeType: string | null;
20
+ }
21
+ /**
22
+ * Unified incoming message flowing through the entire pipeline.
23
+ *
24
+ * Fully assembled by the watcher — images starts empty and is populated
25
+ * by the downloadImages pipeline task.
26
+ */
27
+ export interface IncomingMessage {
28
+ chatGuid: string;
29
+ text: string | null;
30
+ sender: string;
31
+ messageType: MessageType;
32
+ groupName: string;
33
+ /** Attachment file paths — populated by the watcher from chat.db. */
34
+ attachments: Attachment[];
35
+ /** Image attachments, read and base64-encoded by the downloadImages pipeline task. */
36
+ images: ImageContent[];
37
+ }
38
+ /** Structured reply from the agent — preserves semantic type for formatting. */
39
+ export type AgentReply = {
40
+ kind: "assistant";
41
+ text: string;
42
+ } | {
43
+ kind: "tool_start";
44
+ label: string;
45
+ } | {
46
+ kind: "tool_end";
47
+ toolName: string;
48
+ symbol: string;
49
+ duration: string;
50
+ result: string;
51
+ };
52
+ /** Format an AgentReply into a plain-text iMessage string. */
53
+ export declare function formatAgentReply(reply: AgentReply): string;
54
+ /** What kind of reply the bot should send back. */
55
+ export type ReplyAction = {
56
+ type: "message";
57
+ text: string;
58
+ } | {
59
+ type: "none";
60
+ };
61
+ /**
62
+ * Structured pipeline response, carried as context through all phases.
63
+ *
64
+ * reply — the reply action to perform (text or nothing).
65
+ * shouldContinue — if false, remaining tasks in the current phase and all
66
+ * later phases are skipped.
67
+ * sendReply — if false, the reply is logged but not sent to the user
68
+ * (e.g. agent encountered an error).
69
+ */
70
+ export interface OutgoingMessage {
71
+ reply: ReplyAction;
72
+ shouldContinue: boolean;
73
+ sendReply: boolean;
74
+ }
75
+ /** Create a default OutgoingMessage (no reply, continue processing). */
76
+ export declare function createOutgoingMessage(): OutgoingMessage;
77
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAExD;;;;;;;;GAQG;AACH,MAAM,MAAM,WAAW,GAAG,KAAK,GAAG,UAAU,GAAG,OAAO,CAAC;AAEvD,gDAAgD;AAChD,MAAM,WAAW,UAAU;IAC1B,iEAAiE;IACjE,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;CACxB;AAED;;;;;GAKG;AACH,MAAM,WAAW,eAAe;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,WAAW,CAAC;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,qEAAqE;IACrE,WAAW,EAAE,UAAU,EAAE,CAAC;IAC1B,sFAAsF;IACtF,MAAM,EAAE,YAAY,EAAE,CAAC;CACvB;AAID,gFAAgF;AAChF,MAAM,MAAM,UAAU,GACnB;IAAE,IAAI,EAAE,WAAW,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GACnC;IAAE,IAAI,EAAE,YAAY,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GACrC;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAI5F,8DAA8D;AAC9D,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,CAY1D;AAID,mDAAmD;AACnD,MAAM,MAAM,WAAW,GAAG;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AAE/E;;;;;;;;GAQG;AACH,MAAM,WAAW,eAAe;IAC/B,KAAK,EAAE,WAAW,CAAC;IACnB,cAAc,EAAE,OAAO,CAAC;IACxB,SAAS,EAAE,OAAO,CAAC;CACnB;AAED,wEAAwE;AACxE,wBAAgB,qBAAqB,IAAI,eAAe,CAEvD"}
package/dist/types.js ADDED
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Shared types for the iMessage bot pipeline.
3
+ */
4
+ const MAX_TOOL_RESULT_LINES = 5;
5
+ /** Format an AgentReply into a plain-text iMessage string. */
6
+ export function formatAgentReply(reply) {
7
+ if (reply.kind === "assistant")
8
+ return reply.text;
9
+ if (reply.kind === "tool_start")
10
+ return `→ ${reply.label}`;
11
+ // tool_end: header + result truncated to MAX_TOOL_RESULT_LINES
12
+ const header = `${reply.symbol} ${reply.toolName} (${reply.duration}s)`;
13
+ const lines = reply.result.split("\n");
14
+ if (lines.length > MAX_TOOL_RESULT_LINES) {
15
+ const truncated = lines.slice(0, MAX_TOOL_RESULT_LINES).join("\n");
16
+ return `${header}\n${truncated}\n…`;
17
+ }
18
+ return `${header}\n${reply.result}`;
19
+ }
20
+ /** Create a default OutgoingMessage (no reply, continue processing). */
21
+ export function createOutgoingMessage() {
22
+ return { reply: { type: "none" }, shouldContinue: true, sendReply: true };
23
+ }
24
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAgDH,MAAM,qBAAqB,GAAG,CAAC,CAAC;AAEhC,8DAA8D;AAC9D,MAAM,UAAU,gBAAgB,CAAC,KAAiB;IACjD,IAAI,KAAK,CAAC,IAAI,KAAK,WAAW;QAAE,OAAO,KAAK,CAAC,IAAI,CAAC;IAClD,IAAI,KAAK,CAAC,IAAI,KAAK,YAAY;QAAE,OAAO,KAAK,KAAK,CAAC,KAAK,EAAE,CAAC;IAE3D,+DAA+D;IAC/D,MAAM,MAAM,GAAG,GAAG,KAAK,CAAC,MAAM,IAAI,KAAK,CAAC,QAAQ,KAAK,KAAK,CAAC,QAAQ,IAAI,CAAC;IACxE,MAAM,KAAK,GAAG,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IACvC,IAAI,KAAK,CAAC,MAAM,GAAG,qBAAqB,EAAE,CAAC;QAC1C,MAAM,SAAS,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,qBAAqB,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACnE,OAAO,GAAG,MAAM,KAAK,SAAS,KAAK,CAAC;IACrC,CAAC;IACD,OAAO,GAAG,MAAM,KAAK,KAAK,CAAC,MAAM,EAAE,CAAC;AACrC,CAAC;AAsBD,wEAAwE;AACxE,MAAM,UAAU,qBAAqB;IACpC,OAAO,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,cAAc,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC;AAC3E,CAAC"}
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Watch for new iMessages by polling ~/Library/Messages/chat.db.
3
+ *
4
+ * ┌──────────┐ poll ┌──────────┐ push ┌───────┐
5
+ * │ chat.db │ ──────> │ watch.ts │ ──────> │ queue │
6
+ * └──────────┘ └──────────┘ └───────┘
7
+ *
8
+ * Replaces BlueBubbles webhook monitor — no server, no external dependency.
9
+ * Requires Full Disk Access for the terminal / Node process.
10
+ */
11
+ import type { AsyncQueue } from "./queue.js";
12
+ import type { IncomingMessage } from "./types.js";
13
+ export interface WatcherConfig {
14
+ queue: AsyncQueue<IncomingMessage>;
15
+ pollIntervalMs?: number;
16
+ }
17
+ export declare function createWatcher(config: WatcherConfig): {
18
+ start(): void;
19
+ stop(): void;
20
+ };
21
+ //# sourceMappingURL=watch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"watch.d.ts","sourceRoot":"","sources":["../src/watch.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAKH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAC7C,OAAO,KAAK,EAAc,eAAe,EAAe,MAAM,YAAY,CAAC;AA6D3E,MAAM,WAAW,aAAa;IAC7B,KAAK,EAAE,UAAU,CAAC,eAAe,CAAC,CAAC;IACnC,cAAc,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,wBAAgB,aAAa,CAAC,MAAM,EAAE,aAAa;aAgFxC,IAAI;YAOL,IAAI;EAUb"}
package/dist/watch.js ADDED
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Watch for new iMessages by polling ~/Library/Messages/chat.db.
3
+ *
4
+ * ┌──────────┐ poll ┌──────────┐ push ┌───────┐
5
+ * │ chat.db │ ──────> │ watch.ts │ ──────> │ queue │
6
+ * └──────────┘ └──────────┘ └───────┘
7
+ *
8
+ * Replaces BlueBubbles webhook monitor — no server, no external dependency.
9
+ * Requires Full Disk Access for the terminal / Node process.
10
+ */
11
+ import { homedir } from "node:os";
12
+ import { join } from "node:path";
13
+ import Database from "better-sqlite3";
14
+ // ── Config ────────────────────────────────────────────────────────────────────
15
+ const DEFAULT_POLL_INTERVAL_MS = 2_000;
16
+ /** macOS Core Data epoch: 2001-01-01T00:00:00Z in milliseconds. */
17
+ const MAC_EPOCH_MS = new Date("2001-01-01T00:00:00Z").getTime();
18
+ // ── SQL ───────────────────────────────────────────────────────────────────────
19
+ const MESSAGES_QUERY = `
20
+ SELECT
21
+ message.ROWID AS rowid,
22
+ message.text AS text,
23
+ message.is_from_me AS is_from_me,
24
+ message.service AS service,
25
+ message.associated_message_type AS reaction_type,
26
+ handle.id AS sender,
27
+ chat.guid AS chat_guid,
28
+ chat.display_name AS group_name,
29
+ (SELECT COUNT(*) FROM chat_handle_join WHERE chat_handle_join.chat_id = chat.ROWID) > 1 AS is_group
30
+ FROM message
31
+ LEFT JOIN handle ON message.handle_id = handle.ROWID
32
+ LEFT JOIN chat_message_join ON message.ROWID = chat_message_join.message_id
33
+ LEFT JOIN chat ON chat_message_join.chat_id = chat.ROWID
34
+ WHERE message.date > ?
35
+ AND message.is_from_me = 0
36
+ AND (message.associated_message_type IS NULL OR message.associated_message_type = 0)
37
+ ORDER BY message.date ASC
38
+ `;
39
+ const ATTACHMENTS_QUERY = `
40
+ SELECT
41
+ attachment.filename AS filename,
42
+ attachment.mime_type AS mime_type
43
+ FROM attachment
44
+ INNER JOIN message_attachment_join ON attachment.ROWID = message_attachment_join.attachment_id
45
+ WHERE message_attachment_join.message_id = ?
46
+ `;
47
+ export function createWatcher(config) {
48
+ const { queue, pollIntervalMs = DEFAULT_POLL_INTERVAL_MS } = config;
49
+ const dbPath = join(homedir(), "Library", "Messages", "chat.db");
50
+ let db;
51
+ let intervalId = null;
52
+ let lastTimestamp = (Date.now() - MAC_EPOCH_MS) * 1_000_000; // now, in macOS nanoseconds
53
+ const seenRowIds = new Set();
54
+ function deriveMessageType(service, isGroup) {
55
+ if (service?.toLowerCase().includes("sms"))
56
+ return "sms";
57
+ return isGroup ? "group" : "imessage";
58
+ }
59
+ function expandPath(rawPath) {
60
+ if (rawPath.startsWith("~"))
61
+ return rawPath.replace(/^~/, homedir());
62
+ return rawPath;
63
+ }
64
+ function getAttachments(rowid) {
65
+ const rows = db.prepare(ATTACHMENTS_QUERY).all(rowid);
66
+ return rows
67
+ .filter((r) => r.filename)
68
+ .map((r) => ({
69
+ path: expandPath(r.filename ?? ""),
70
+ mimeType: r.mime_type ?? null,
71
+ }));
72
+ }
73
+ function poll() {
74
+ try {
75
+ const rows = db.prepare(MESSAGES_QUERY).all(lastTimestamp);
76
+ for (const row of rows) {
77
+ if (seenRowIds.has(row.rowid))
78
+ continue;
79
+ if (!row.chat_guid)
80
+ continue;
81
+ const hasText = Boolean(row.text?.trim());
82
+ const attachments = getAttachments(row.rowid);
83
+ if (!hasText && attachments.length === 0)
84
+ continue;
85
+ seenRowIds.add(row.rowid);
86
+ const msg = {
87
+ chatGuid: row.chat_guid,
88
+ text: row.text?.trim() ?? null,
89
+ sender: row.sender ?? "unknown",
90
+ messageType: deriveMessageType(row.service, row.is_group),
91
+ groupName: row.group_name ?? "",
92
+ attachments,
93
+ images: [],
94
+ };
95
+ queue.push(msg);
96
+ }
97
+ // Advance timestamp to latest seen row
98
+ if (rows.length > 0) {
99
+ // Re-query max date to advance cursor
100
+ const maxDate = db
101
+ .prepare(`SELECT MAX(date) AS max_date FROM message WHERE ROWID IN (${rows.map(() => "?").join(",")})`)
102
+ .get(...rows.map((r) => r.rowid));
103
+ if (maxDate?.max_date) {
104
+ lastTimestamp = maxDate.max_date;
105
+ }
106
+ }
107
+ // Prune seenRowIds — keep last 10000
108
+ if (seenRowIds.size > 10_000) {
109
+ const entries = [...seenRowIds];
110
+ for (let i = 0; i < entries.length - 5_000; i++) {
111
+ seenRowIds.delete(entries[i]);
112
+ }
113
+ }
114
+ }
115
+ catch (error) {
116
+ console.error("[watch] poll error:", error);
117
+ }
118
+ }
119
+ return {
120
+ start() {
121
+ db = new Database(dbPath, { readonly: true });
122
+ console.log(`[watch] polling chat.db every ${pollIntervalMs}ms`);
123
+ poll(); // initial poll to set cursor (catches nothing on first run)
124
+ intervalId = setInterval(poll, pollIntervalMs);
125
+ },
126
+ stop() {
127
+ if (intervalId) {
128
+ clearInterval(intervalId);
129
+ intervalId = null;
130
+ }
131
+ db?.close();
132
+ queue.close();
133
+ console.log("[watch] stopped");
134
+ },
135
+ };
136
+ }
137
+ //# sourceMappingURL=watch.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"watch.js","sourceRoot":"","sources":["../src/watch.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ /** Read chat logs from disk and produce ChatBlock summaries. */
2
+ import type { LoggedMessage } from "../store.js";
3
+ export interface ChatBlock {
4
+ guid: string;
5
+ displayName: string;
6
+ messages: LoggedMessage[];
7
+ lastTime: number;
8
+ }
9
+ /** Return chat blocks from the last 7 days, sorted by most-recent message. */
10
+ export declare function getChatBlocks(workingDir: string): ChatBlock[];
11
+ //# sourceMappingURL=data.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"data.d.ts","sourceRoot":"","sources":["../../src/web/data.ts"],"names":[],"mappings":"AAAA,gEAAgE;AAIhE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAEjD,MAAM,WAAW,SAAS;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,aAAa,EAAE,CAAC;IAC1B,QAAQ,EAAE,MAAM,CAAC;CACjB;AAmBD,8EAA8E;AAC9E,wBAAgB,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,EAAE,CAiC7D"}
@@ -0,0 +1,56 @@
1
+ /** Read chat logs from disk and produce ChatBlock summaries. */
2
+ import { existsSync, readFileSync, readdirSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ const SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1000;
5
+ function readMessages(workingDir, chatGuid) {
6
+ const logFile = join(workingDir, chatGuid, "log.jsonl");
7
+ if (!existsSync(logFile))
8
+ return [];
9
+ const lines = readFileSync(logFile, "utf-8").trim().split("\n").filter(Boolean);
10
+ const messages = [];
11
+ for (const line of lines) {
12
+ try {
13
+ messages.push(JSON.parse(line));
14
+ }
15
+ catch {
16
+ // skip malformed lines
17
+ }
18
+ }
19
+ return messages;
20
+ }
21
+ /** Return chat blocks from the last 7 days, sorted by most-recent message. */
22
+ export function getChatBlocks(workingDir) {
23
+ if (!existsSync(workingDir))
24
+ return [];
25
+ const cutoff = Date.now() - SEVEN_DAYS_MS;
26
+ const blocks = [];
27
+ let entries;
28
+ try {
29
+ entries = readdirSync(workingDir, { withFileTypes: true })
30
+ .filter((entry) => entry.isDirectory())
31
+ .map((entry) => entry.name);
32
+ }
33
+ catch {
34
+ return [];
35
+ }
36
+ for (const guid of entries) {
37
+ const messages = readMessages(workingDir, guid);
38
+ if (messages.length === 0)
39
+ continue;
40
+ const lastMessage = messages[messages.length - 1];
41
+ if (!lastMessage)
42
+ continue;
43
+ const lastTime = new Date(lastMessage.date).getTime();
44
+ if (lastTime < cutoff)
45
+ continue;
46
+ const parts = guid.split(";");
47
+ let displayName = parts[parts.length - 1] ?? guid;
48
+ if (lastMessage.messageType === "group" && lastMessage.groupName) {
49
+ displayName = lastMessage.groupName;
50
+ }
51
+ blocks.push({ guid, displayName, messages, lastTime });
52
+ }
53
+ blocks.sort((a, b) => b.lastTime - a.lastTime);
54
+ return blocks;
55
+ }
56
+ //# sourceMappingURL=data.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"data.js","sourceRoot":"","sources":["../../src/web/data.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ /** HTML utility functions. */
2
+ /** Format as "[YYYY-MM-DD HH:MM:SS]" in local time. */
3
+ export declare function formatTime(iso: string): string;
4
+ export declare function anchorId(guid: string): string;
5
+ //# sourceMappingURL=html.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"html.d.ts","sourceRoot":"","sources":["../../src/web/html.ts"],"names":[],"mappings":"AAAA,8BAA8B;AAE9B,uDAAuD;AACvD,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAS9C;AAED,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE7C"}