@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.
- package/README.md +120 -0
- package/dist/agent.d.ts +22 -0
- package/dist/agent.d.ts.map +1 -0
- package/dist/agent.js +341 -0
- package/dist/agent.js.map +1 -0
- package/dist/cli.d.ts +15 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +179 -0
- package/dist/cli.js.map +1 -0
- package/dist/imessage.d.ts +32 -0
- package/dist/imessage.d.ts.map +1 -0
- package/dist/imessage.js +65 -0
- package/dist/imessage.js.map +1 -0
- package/dist/logger.d.ts +33 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +103 -0
- package/dist/logger.js.map +1 -0
- package/dist/main.d.ts +5 -0
- package/dist/main.d.ts.map +1 -0
- package/dist/main.js +66 -0
- package/dist/main.js.map +1 -0
- package/dist/pipeline.d.ts +26 -0
- package/dist/pipeline.d.ts.map +1 -0
- package/dist/pipeline.js +48 -0
- package/dist/pipeline.js.map +1 -0
- package/dist/queue.d.ts +16 -0
- package/dist/queue.d.ts.map +1 -0
- package/dist/queue.js +47 -0
- package/dist/queue.js.map +1 -0
- package/dist/self-echo.d.ts +19 -0
- package/dist/self-echo.d.ts.map +1 -0
- package/dist/self-echo.js +54 -0
- package/dist/self-echo.js.map +1 -0
- package/dist/send.d.ts +17 -0
- package/dist/send.d.ts.map +1 -0
- package/dist/send.js +107 -0
- package/dist/send.js.map +1 -0
- package/dist/settings.d.ts +39 -0
- package/dist/settings.d.ts.map +1 -0
- package/dist/settings.js +74 -0
- package/dist/settings.js.map +1 -0
- package/dist/store.d.ts +41 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +72 -0
- package/dist/store.js.map +1 -0
- package/dist/tasks.d.ts +88 -0
- package/dist/tasks.d.ts.map +1 -0
- package/dist/tasks.js +260 -0
- package/dist/tasks.js.map +1 -0
- package/dist/types.d.ts +77 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +24 -0
- package/dist/types.js.map +1 -0
- package/dist/watch.d.ts +21 -0
- package/dist/watch.d.ts.map +1 -0
- package/dist/watch.js +137 -0
- package/dist/watch.js.map +1 -0
- package/dist/web/data.d.ts +11 -0
- package/dist/web/data.d.ts.map +1 -0
- package/dist/web/data.js +56 -0
- package/dist/web/data.js.map +1 -0
- package/dist/web/html.d.ts +5 -0
- package/dist/web/html.d.ts.map +1 -0
- package/dist/web/html.js +16 -0
- package/dist/web/html.js.map +1 -0
- package/dist/web/index.d.ts +15 -0
- package/dist/web/index.d.ts.map +1 -0
- package/dist/web/index.js +116 -0
- package/dist/web/index.js.map +1 -0
- package/dist/web/render.d.ts +5 -0
- package/dist/web/render.d.ts.map +1 -0
- package/dist/web/render.js +50 -0
- package/dist/web/render.js.map +1 -0
- package/dist/web/templates/page.eta +85 -0
- package/package.json +42 -0
package/dist/queue.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic unbounded async queue.
|
|
3
|
+
*
|
|
4
|
+
* push() never blocks; pull() returns a promise that resolves when an item
|
|
5
|
+
* is available. close() rejects all pending waiters and future pull() calls.
|
|
6
|
+
*/
|
|
7
|
+
export class QueueClosedError extends Error {
|
|
8
|
+
constructor() {
|
|
9
|
+
super("Queue closed");
|
|
10
|
+
this.name = "QueueClosedError";
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export function createAsyncQueue() {
|
|
14
|
+
const buffer = [];
|
|
15
|
+
const waiters = [];
|
|
16
|
+
let closed = false;
|
|
17
|
+
return {
|
|
18
|
+
push(item) {
|
|
19
|
+
if (closed)
|
|
20
|
+
return;
|
|
21
|
+
const waiter = waiters.shift();
|
|
22
|
+
if (waiter) {
|
|
23
|
+
waiter.resolve(item);
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
buffer.push(item);
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
pull() {
|
|
30
|
+
if (closed)
|
|
31
|
+
return Promise.reject(new QueueClosedError());
|
|
32
|
+
const item = buffer.shift();
|
|
33
|
+
if (item !== undefined)
|
|
34
|
+
return Promise.resolve(item);
|
|
35
|
+
return new Promise((resolve, reject) => waiters.push({ resolve, reject }));
|
|
36
|
+
},
|
|
37
|
+
close() {
|
|
38
|
+
closed = true;
|
|
39
|
+
for (const waiter of waiters) {
|
|
40
|
+
waiter.reject(new QueueClosedError());
|
|
41
|
+
}
|
|
42
|
+
waiters.length = 0;
|
|
43
|
+
buffer.length = 0;
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=queue.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"queue.js","sourceRoot":"","sources":["../src/queue.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,MAAM,OAAO,gBAAiB,SAAQ,KAAK;IAC1C;QACC,KAAK,CAAC,cAAc,CAAC,CAAC;QACtB,IAAI,CAAC,IAAI,GAAG,kBAAkB,CAAC;IAChC,CAAC;CACD;AAQD,MAAM,UAAU,gBAAgB;IAC/B,MAAM,MAAM,GAAQ,EAAE,CAAC;IACvB,MAAM,OAAO,GAAwE,EAAE,CAAC;IACxF,IAAI,MAAM,GAAG,KAAK,CAAC;IAEnB,OAAO;QACN,IAAI,CAAC,IAAO;YACX,IAAI,MAAM;gBAAE,OAAO;YACnB,MAAM,MAAM,GAAG,OAAO,CAAC,KAAK,EAAE,CAAC;YAC/B,IAAI,MAAM,EAAE,CAAC;gBACZ,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;YACtB,CAAC;iBAAM,CAAC;gBACP,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACnB,CAAC;QACF,CAAC;QAED,IAAI;YACH,IAAI,MAAM;gBAAE,OAAO,OAAO,CAAC,MAAM,CAAC,IAAI,gBAAgB,EAAE,CAAC,CAAC;YAC1D,MAAM,IAAI,GAAG,MAAM,CAAC,KAAK,EAAE,CAAC;YAC5B,IAAI,IAAI,KAAK,SAAS;gBAAE,OAAO,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;YACrD,OAAO,IAAI,OAAO,CAAI,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC;QAC/E,CAAC;QAED,KAAK;YACJ,MAAM,GAAG,IAAI,CAAC;YACd,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;gBAC9B,MAAM,CAAC,MAAM,CAAC,IAAI,gBAAgB,EAAE,CAAC,CAAC;YACvC,CAAC;YACD,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC;YACnB,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC;QACnB,CAAC;KACD,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Self-echo filter — prevents the bot from replying to its own messages.
|
|
3
|
+
*
|
|
4
|
+
* In a self-chat, the Messages app echoes outgoing messages back as incoming.
|
|
5
|
+
* Register each outgoing reply with remember() before sending. When the echo
|
|
6
|
+
* arrives, isEcho() detects the match and the caller drops the message.
|
|
7
|
+
* Each entry is consumed on first match so identical human follow-ups pass through.
|
|
8
|
+
*/
|
|
9
|
+
export interface SelfEchoFilter {
|
|
10
|
+
/** Register a message we just sent so its echo can be suppressed. */
|
|
11
|
+
remember(senderId: string, text: string): void;
|
|
12
|
+
/**
|
|
13
|
+
* Returns true if this message looks like an echo of something we sent.
|
|
14
|
+
* Consumes the entry on match so an identical human follow-up is not dropped.
|
|
15
|
+
*/
|
|
16
|
+
isEcho(senderId: string, text: string): boolean;
|
|
17
|
+
}
|
|
18
|
+
export declare function createSelfEchoFilter(ttlMs?: number): SelfEchoFilter;
|
|
19
|
+
//# sourceMappingURL=self-echo.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"self-echo.d.ts","sourceRoot":"","sources":["../src/self-echo.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AA2CH,MAAM,WAAW,cAAc;IAC9B,qEAAqE;IACrE,QAAQ,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/C;;;OAGG;IACH,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;CAChD;AAED,wBAAgB,oBAAoB,CAAC,KAAK,SAAS,GAAG,cAAc,CAyBnE"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Self-echo filter — prevents the bot from replying to its own messages.
|
|
3
|
+
*
|
|
4
|
+
* In a self-chat, the Messages app echoes outgoing messages back as incoming.
|
|
5
|
+
* Register each outgoing reply with remember() before sending. When the echo
|
|
6
|
+
* arrives, isEcho() detects the match and the caller drops the message.
|
|
7
|
+
* Each entry is consumed on first match so identical human follow-ups pass through.
|
|
8
|
+
*/
|
|
9
|
+
function createExpiringSet(ttlMs) {
|
|
10
|
+
const entries = [];
|
|
11
|
+
function prune() {
|
|
12
|
+
const now = Date.now();
|
|
13
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
14
|
+
if (entries[i].expiresAt <= now)
|
|
15
|
+
entries.splice(i, 1);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function add(value) {
|
|
19
|
+
prune();
|
|
20
|
+
entries.push({ value, expiresAt: Date.now() + ttlMs });
|
|
21
|
+
}
|
|
22
|
+
/** Returns true and consumes the entry if found, false otherwise. */
|
|
23
|
+
function consume(value) {
|
|
24
|
+
prune();
|
|
25
|
+
const idx = entries.findIndex((e) => e.value === value);
|
|
26
|
+
if (idx === -1)
|
|
27
|
+
return false;
|
|
28
|
+
entries.splice(idx, 1);
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
return { add, consume };
|
|
32
|
+
}
|
|
33
|
+
export function createSelfEchoFilter(ttlMs = 60_000) {
|
|
34
|
+
const buckets = new Map();
|
|
35
|
+
function normalise(text) {
|
|
36
|
+
return text.trim().toLowerCase();
|
|
37
|
+
}
|
|
38
|
+
function getBucket(senderId) {
|
|
39
|
+
let bucket = buckets.get(senderId);
|
|
40
|
+
if (!bucket) {
|
|
41
|
+
bucket = createExpiringSet(ttlMs);
|
|
42
|
+
buckets.set(senderId, bucket);
|
|
43
|
+
}
|
|
44
|
+
return bucket;
|
|
45
|
+
}
|
|
46
|
+
function remember(senderId, text) {
|
|
47
|
+
getBucket(senderId).add(normalise(text));
|
|
48
|
+
}
|
|
49
|
+
function isEcho(senderId, text) {
|
|
50
|
+
return getBucket(senderId).consume(normalise(text));
|
|
51
|
+
}
|
|
52
|
+
return { remember, isEcho };
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=self-echo.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"self-echo.js","sourceRoot":"","sources":["../src/self-echo.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAcH,SAAS,iBAAiB,CAAC,KAAa;IACvC,MAAM,OAAO,GAAoB,EAAE,CAAC;IAEpC,SAAS,KAAK;QACb,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACvB,KAAK,IAAI,CAAC,GAAG,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;YAC9C,IAAI,OAAO,CAAC,CAAC,CAAC,CAAC,SAAS,IAAI,GAAG;gBAAE,OAAO,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;QACvD,CAAC;IACF,CAAC;IAED,SAAS,GAAG,CAAC,KAAa;QACzB,KAAK,EAAE,CAAC;QACR,OAAO,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK,EAAE,CAAC,CAAC;IACxD,CAAC;IAED,qEAAqE;IACrE,SAAS,OAAO,CAAC,KAAa;QAC7B,KAAK,EAAE,CAAC;QACR,MAAM,GAAG,GAAG,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,KAAK,KAAK,CAAC,CAAC;QACxD,IAAI,GAAG,KAAK,CAAC,CAAC;YAAE,OAAO,KAAK,CAAC;QAC7B,OAAO,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;QACvB,OAAO,IAAI,CAAC;IACb,CAAC;IAED,OAAO,EAAE,GAAG,EAAE,OAAO,EAAE,CAAC;AACzB,CAAC;AAcD,MAAM,UAAU,oBAAoB,CAAC,KAAK,GAAG,MAAM;IAClD,MAAM,OAAO,GAAG,IAAI,GAAG,EAAgD,CAAC;IAExE,SAAS,SAAS,CAAC,IAAY;QAC9B,OAAO,IAAI,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IAClC,CAAC;IAED,SAAS,SAAS,CAAC,QAAgB;QAClC,IAAI,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QACnC,IAAI,CAAC,MAAM,EAAE,CAAC;YACb,MAAM,GAAG,iBAAiB,CAAC,KAAK,CAAC,CAAC;YAClC,OAAO,CAAC,GAAG,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;QAC/B,CAAC;QACD,OAAO,MAAM,CAAC;IACf,CAAC;IAED,SAAS,QAAQ,CAAC,QAAgB,EAAE,IAAY;QAC/C,SAAS,CAAC,QAAQ,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC;IAC1C,CAAC;IAED,SAAS,MAAM,CAAC,QAAgB,EAAE,IAAY;QAC7C,OAAO,SAAS,CAAC,QAAQ,CAAC,CAAC,OAAO,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC;IACrD,CAAC;IAED,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC;AAC7B,CAAC"}
|
package/dist/send.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Send iMessages via macOS Messages.app AppleScript.
|
|
3
|
+
*
|
|
4
|
+
* ┌──────────┐ AppleScript ┌──────────────┐
|
|
5
|
+
* │ send.ts │ ───────────────> │ Messages.app │
|
|
6
|
+
* └──────────┘ └──────────────┘
|
|
7
|
+
*
|
|
8
|
+
* Supports DM (buddy), group chat (chat id), and SMS targets.
|
|
9
|
+
* Check Messages.app environment before first send with checkEnvironment().
|
|
10
|
+
*/
|
|
11
|
+
/** Check Messages.app is running and iMessage is signed in. */
|
|
12
|
+
export declare function checkEnvironment(): Promise<void>;
|
|
13
|
+
export interface MessageSender {
|
|
14
|
+
sendMessage(chatGuid: string, text: string): Promise<void>;
|
|
15
|
+
}
|
|
16
|
+
export declare function createMessageSender(): MessageSender;
|
|
17
|
+
//# sourceMappingURL=send.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"send.d.ts","sourceRoot":"","sources":["../src/send.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAsDH,+DAA+D;AAC/D,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC,CA0CtD;AAID,MAAM,WAAW,aAAa;IAC7B,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC3D;AAED,wBAAgB,mBAAmB,IAAI,aAAa,CAQnD"}
|
package/dist/send.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Send iMessages via macOS Messages.app AppleScript.
|
|
3
|
+
*
|
|
4
|
+
* ┌──────────┐ AppleScript ┌──────────────┐
|
|
5
|
+
* │ send.ts │ ───────────────> │ Messages.app │
|
|
6
|
+
* └──────────┘ └──────────────┘
|
|
7
|
+
*
|
|
8
|
+
* Supports DM (buddy), group chat (chat id), and SMS targets.
|
|
9
|
+
* Check Messages.app environment before first send with checkEnvironment().
|
|
10
|
+
*/
|
|
11
|
+
import { execFile } from "node:child_process";
|
|
12
|
+
import { constants, accessSync } from "node:fs";
|
|
13
|
+
import { homedir } from "node:os";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
import { promisify } from "node:util";
|
|
16
|
+
const execFileAsync = promisify(execFile);
|
|
17
|
+
const SCRIPT_TIMEOUT_MS = 30_000;
|
|
18
|
+
// ── AppleScript helpers ───────────────────────────────────────────────────────
|
|
19
|
+
/** Escape special characters for AppleScript string literals. */
|
|
20
|
+
function escapeAppleScript(str) {
|
|
21
|
+
return str
|
|
22
|
+
.replace(/\\/g, "\\\\")
|
|
23
|
+
.replace(/"/g, '\\"')
|
|
24
|
+
.replace(/\n/g, "\\n")
|
|
25
|
+
.replace(/\r/g, "\\r")
|
|
26
|
+
.replace(/\t/g, "\\t");
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Build the AppleScript for sending a text message.
|
|
30
|
+
*
|
|
31
|
+
* chatGuid format (from iMessage database):
|
|
32
|
+
* "iMessage;-;+1234567890" → DM → send via buddy
|
|
33
|
+
* "iMessage;+;chat123..." → group → send via chat id
|
|
34
|
+
* "SMS;-;+1234567890" → SMS → send via buddy
|
|
35
|
+
*/
|
|
36
|
+
function buildSendScript(chatGuid, text) {
|
|
37
|
+
const escapedText = escapeAppleScript(text);
|
|
38
|
+
const parts = chatGuid.split(";");
|
|
39
|
+
const isGroup = parts[1] === "+";
|
|
40
|
+
if (isGroup) {
|
|
41
|
+
const escapedChatId = escapeAppleScript(chatGuid);
|
|
42
|
+
return `tell application "Messages"
|
|
43
|
+
set targetChat to chat id "${escapedChatId}"
|
|
44
|
+
send "${escapedText}" to targetChat
|
|
45
|
+
end tell`;
|
|
46
|
+
}
|
|
47
|
+
const recipient = escapeAppleScript(parts[2] ?? "");
|
|
48
|
+
return `tell application "Messages"
|
|
49
|
+
set targetService to 1st service whose service type = iMessage
|
|
50
|
+
set targetBuddy to buddy "${recipient}" of targetService
|
|
51
|
+
send "${escapedText}" to targetBuddy
|
|
52
|
+
end tell`;
|
|
53
|
+
}
|
|
54
|
+
// ── Environment check ─────────────────────────────────────────────────────────
|
|
55
|
+
/** Check Messages.app is running and iMessage is signed in. */
|
|
56
|
+
export async function checkEnvironment() {
|
|
57
|
+
// 1. Check Messages.app process
|
|
58
|
+
try {
|
|
59
|
+
await execFileAsync("pgrep", ["-x", "Messages"], { timeout: 5_000 });
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
throw new Error("Messages.app is not running");
|
|
63
|
+
}
|
|
64
|
+
// 2. Check Full Disk Access (required to read chat.db)
|
|
65
|
+
const chatDbPath = join(homedir(), "Library", "Messages", "chat.db");
|
|
66
|
+
try {
|
|
67
|
+
accessSync(chatDbPath, constants.R_OK);
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
throw new Error(`Cannot read ${chatDbPath} — grant Full Disk Access to your terminal in System Settings > Privacy & Security > Full Disk Access`);
|
|
71
|
+
}
|
|
72
|
+
// 3. Check iMessage has active accounts
|
|
73
|
+
const script = `tell application "Messages"
|
|
74
|
+
try
|
|
75
|
+
set accountList to every account
|
|
76
|
+
if (count of accountList) is 0 then return "no_accounts"
|
|
77
|
+
repeat with acct in accountList
|
|
78
|
+
if enabled of acct is true then return "active"
|
|
79
|
+
end repeat
|
|
80
|
+
return "inactive"
|
|
81
|
+
on error
|
|
82
|
+
return "error"
|
|
83
|
+
end try
|
|
84
|
+
end tell`;
|
|
85
|
+
try {
|
|
86
|
+
const { stdout } = await execFileAsync("osascript", ["-e", script], { timeout: 5_000 });
|
|
87
|
+
const status = stdout.trim();
|
|
88
|
+
if (status !== "active") {
|
|
89
|
+
throw new Error(`iMessage account status: ${status}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
if (error instanceof Error && error.message.startsWith("iMessage account"))
|
|
94
|
+
throw error;
|
|
95
|
+
throw new Error(`Failed to check iMessage status: ${error instanceof Error ? error.message : String(error)}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
export function createMessageSender() {
|
|
99
|
+
return {
|
|
100
|
+
async sendMessage(chatGuid, text) {
|
|
101
|
+
const script = buildSendScript(chatGuid, text);
|
|
102
|
+
await execFileAsync("osascript", ["-e", script], { timeout: SCRIPT_TIMEOUT_MS });
|
|
103
|
+
console.log(`[send] sent to ${chatGuid}: "${text.substring(0, 60)}"`);
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
//# sourceMappingURL=send.js.map
|
package/dist/send.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"send.js","sourceRoot":"","sources":["../src/send.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Settings — workspace-level configuration for the iMessage bot.
|
|
3
|
+
*
|
|
4
|
+
* Loaded from ~/.pi/imessage/settings.json (or WORKING_DIR/settings.json).
|
|
5
|
+
*
|
|
6
|
+
* ### Chat allowlist
|
|
7
|
+
*
|
|
8
|
+
* Controls whether the bot should reply to a given chat.
|
|
9
|
+
* Messages are always logged regardless of this setting.
|
|
10
|
+
*
|
|
11
|
+
* Resolution priority (highest to lowest):
|
|
12
|
+
*
|
|
13
|
+
* blacklist["chatGuid"] > whitelist["chatGuid"] > blacklist["*"] > whitelist["*"]
|
|
14
|
+
*
|
|
15
|
+
* Examples:
|
|
16
|
+
* whitelist: ["*"], blacklist: [] → reply to everyone
|
|
17
|
+
* whitelist: ["1"], blacklist: [] → reply only to "1"
|
|
18
|
+
* whitelist: ["*"], blacklist: ["2"] → reply to everyone except "2"
|
|
19
|
+
* blacklist: ["*"], whitelist: [] → reply to nobody (log-only)
|
|
20
|
+
* whitelist: ["1"], blacklist: ["*"] → reply only to "1"
|
|
21
|
+
* whitelist: ["1"], blacklist: ["1"] → no reply (blacklist wins)
|
|
22
|
+
*
|
|
23
|
+
*/
|
|
24
|
+
export interface ChatAllowlist {
|
|
25
|
+
whitelist: string[];
|
|
26
|
+
blacklist: string[];
|
|
27
|
+
}
|
|
28
|
+
export interface Settings {
|
|
29
|
+
chatAllowlist: ChatAllowlist;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Determine whether the bot should reply to a given chatGuid.
|
|
33
|
+
*
|
|
34
|
+
* Priority: blacklist[chatGuid] > whitelist[chatGuid] > blacklist["*"] > whitelist["*"]
|
|
35
|
+
*/
|
|
36
|
+
export declare function isReplyEnabled(settings: Settings, chatGuid: string): boolean;
|
|
37
|
+
export declare function readSettings(workingDir: string): Settings;
|
|
38
|
+
export declare function writeSettings(workingDir: string, settings: Settings): void;
|
|
39
|
+
//# sourceMappingURL=settings.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"settings.d.ts","sourceRoot":"","sources":["../src/settings.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAKH,MAAM,WAAW,aAAa;IAC7B,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,SAAS,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,MAAM,WAAW,QAAQ;IACxB,aAAa,EAAE,aAAa,CAAC;CAC7B;AAKD;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAO5E;AAQD,wBAAgB,YAAY,CAAC,UAAU,EAAE,MAAM,GAAG,QAAQ,CAoBzD;AAED,wBAAgB,aAAa,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,GAAG,IAAI,CAE1E"}
|
package/dist/settings.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Settings — workspace-level configuration for the iMessage bot.
|
|
3
|
+
*
|
|
4
|
+
* Loaded from ~/.pi/imessage/settings.json (or WORKING_DIR/settings.json).
|
|
5
|
+
*
|
|
6
|
+
* ### Chat allowlist
|
|
7
|
+
*
|
|
8
|
+
* Controls whether the bot should reply to a given chat.
|
|
9
|
+
* Messages are always logged regardless of this setting.
|
|
10
|
+
*
|
|
11
|
+
* Resolution priority (highest to lowest):
|
|
12
|
+
*
|
|
13
|
+
* blacklist["chatGuid"] > whitelist["chatGuid"] > blacklist["*"] > whitelist["*"]
|
|
14
|
+
*
|
|
15
|
+
* Examples:
|
|
16
|
+
* whitelist: ["*"], blacklist: [] → reply to everyone
|
|
17
|
+
* whitelist: ["1"], blacklist: [] → reply only to "1"
|
|
18
|
+
* whitelist: ["*"], blacklist: ["2"] → reply to everyone except "2"
|
|
19
|
+
* blacklist: ["*"], whitelist: [] → reply to nobody (log-only)
|
|
20
|
+
* whitelist: ["1"], blacklist: ["*"] → reply only to "1"
|
|
21
|
+
* whitelist: ["1"], blacklist: ["1"] → no reply (blacklist wins)
|
|
22
|
+
*
|
|
23
|
+
*/
|
|
24
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
25
|
+
import { join } from "node:path";
|
|
26
|
+
const DEFAULT_CHAT_ALLOWLIST = { whitelist: ["*"], blacklist: [] };
|
|
27
|
+
const DEFAULT_SETTINGS = { chatAllowlist: DEFAULT_CHAT_ALLOWLIST };
|
|
28
|
+
/**
|
|
29
|
+
* Determine whether the bot should reply to a given chatGuid.
|
|
30
|
+
*
|
|
31
|
+
* Priority: blacklist[chatGuid] > whitelist[chatGuid] > blacklist["*"] > whitelist["*"]
|
|
32
|
+
*/
|
|
33
|
+
export function isReplyEnabled(settings, chatGuid) {
|
|
34
|
+
const { whitelist, blacklist } = settings.chatAllowlist;
|
|
35
|
+
if (blacklist.includes(chatGuid))
|
|
36
|
+
return false;
|
|
37
|
+
if (whitelist.includes(chatGuid))
|
|
38
|
+
return true;
|
|
39
|
+
if (blacklist.includes("*"))
|
|
40
|
+
return false;
|
|
41
|
+
if (whitelist.includes("*"))
|
|
42
|
+
return true;
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
// ── Persistence ───────────────────────────────────────────────────────────────
|
|
46
|
+
function settingsPath(workingDir) {
|
|
47
|
+
return join(workingDir, "settings.json");
|
|
48
|
+
}
|
|
49
|
+
export function readSettings(workingDir) {
|
|
50
|
+
const filePath = settingsPath(workingDir);
|
|
51
|
+
if (!existsSync(filePath))
|
|
52
|
+
return { ...DEFAULT_SETTINGS };
|
|
53
|
+
try {
|
|
54
|
+
const raw = JSON.parse(readFileSync(filePath, "utf-8"));
|
|
55
|
+
const chatAllowlistRaw = (raw.chatAllowlist ?? {});
|
|
56
|
+
return {
|
|
57
|
+
chatAllowlist: {
|
|
58
|
+
whitelist: Array.isArray(chatAllowlistRaw.whitelist)
|
|
59
|
+
? chatAllowlistRaw.whitelist
|
|
60
|
+
: DEFAULT_CHAT_ALLOWLIST.whitelist,
|
|
61
|
+
blacklist: Array.isArray(chatAllowlistRaw.blacklist)
|
|
62
|
+
? chatAllowlistRaw.blacklist
|
|
63
|
+
: DEFAULT_CHAT_ALLOWLIST.blacklist,
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return { ...DEFAULT_SETTINGS };
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
export function writeSettings(workingDir, settings) {
|
|
72
|
+
writeFileSync(settingsPath(workingDir), `${JSON.stringify(settings, null, 2)}\n`, "utf-8");
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=settings.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"settings.js","sourceRoot":"","sources":["../src/settings.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAClE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAWjC,MAAM,sBAAsB,GAAkB,EAAE,SAAS,EAAE,CAAC,GAAG,CAAC,EAAE,SAAS,EAAE,EAAE,EAAE,CAAC;AAClF,MAAM,gBAAgB,GAAa,EAAE,aAAa,EAAE,sBAAsB,EAAE,CAAC;AAE7E;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAAC,QAAkB,EAAE,QAAgB;IAClE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,GAAG,QAAQ,CAAC,aAAa,CAAC;IACxD,IAAI,SAAS,CAAC,QAAQ,CAAC,QAAQ,CAAC;QAAE,OAAO,KAAK,CAAC;IAC/C,IAAI,SAAS,CAAC,QAAQ,CAAC,QAAQ,CAAC;QAAE,OAAO,IAAI,CAAC;IAC9C,IAAI,SAAS,CAAC,QAAQ,CAAC,GAAG,CAAC;QAAE,OAAO,KAAK,CAAC;IAC1C,IAAI,SAAS,CAAC,QAAQ,CAAC,GAAG,CAAC;QAAE,OAAO,IAAI,CAAC;IACzC,OAAO,KAAK,CAAC;AACd,CAAC;AAED,iFAAiF;AAEjF,SAAS,YAAY,CAAC,UAAkB;IACvC,OAAO,IAAI,CAAC,UAAU,EAAE,eAAe,CAAC,CAAC;AAC1C,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,UAAkB;IAC9C,MAAM,QAAQ,GAAG,YAAY,CAAC,UAAU,CAAC,CAAC;IAC1C,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC;QAAE,OAAO,EAAE,GAAG,gBAAgB,EAAE,CAAC;IAC1D,IAAI,CAAC;QACJ,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,QAAQ,EAAE,OAAO,CAAC,CAA4B,CAAC;QACnF,MAAM,gBAAgB,GAAG,CAAC,GAAG,CAAC,aAAa,IAAI,EAAE,CAA2B,CAAC;QAE7E,OAAO;YACN,aAAa,EAAE;gBACd,SAAS,EAAE,KAAK,CAAC,OAAO,CAAC,gBAAgB,CAAC,SAAS,CAAC;oBACnD,CAAC,CAAC,gBAAgB,CAAC,SAAS;oBAC5B,CAAC,CAAC,sBAAsB,CAAC,SAAS;gBACnC,SAAS,EAAE,KAAK,CAAC,OAAO,CAAC,gBAAgB,CAAC,SAAS,CAAC;oBACnD,CAAC,CAAC,gBAAgB,CAAC,SAAS;oBAC5B,CAAC,CAAC,sBAAsB,CAAC,SAAS;aACnC;SACD,CAAC;IACH,CAAC;IAAC,MAAM,CAAC;QACR,OAAO,EAAE,GAAG,gBAAgB,EAAE,CAAC;IAChC,CAAC;AACF,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,UAAkB,EAAE,QAAkB;IACnE,aAAa,CAAC,YAAY,CAAC,UAAU,CAAC,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;AAC5F,CAAC"}
|
package/dist/store.d.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ChatStore — persistent message log for each iMessage chat.
|
|
3
|
+
*
|
|
4
|
+
* Workspace layout:
|
|
5
|
+
*
|
|
6
|
+
* workingDir/
|
|
7
|
+
* <chatGuid>/ e.g. "iMessage;-;+16501234567"
|
|
8
|
+
* log.jsonl one JSON line per message, append-only
|
|
9
|
+
*
|
|
10
|
+
* Each line is a LoggedMessage serialised as JSON.
|
|
11
|
+
* The file is never rewritten — only appended to.
|
|
12
|
+
*/
|
|
13
|
+
import type { IncomingMessage, MessageType } from "./types.js";
|
|
14
|
+
export interface LoggedMessage {
|
|
15
|
+
/** ISO 8601 timestamp. */
|
|
16
|
+
date: string;
|
|
17
|
+
/** Sender handle (phone / email) or "bot". */
|
|
18
|
+
sender: string;
|
|
19
|
+
text: string | null;
|
|
20
|
+
/** Local paths of attachments, relative to workingDir. */
|
|
21
|
+
attachments: string[];
|
|
22
|
+
isBot: boolean;
|
|
23
|
+
messageType: MessageType;
|
|
24
|
+
/** Group name, only present when messageType is "group". */
|
|
25
|
+
groupName?: string;
|
|
26
|
+
/** Set when the bot encountered an error and did not send a reply. */
|
|
27
|
+
isError?: boolean;
|
|
28
|
+
}
|
|
29
|
+
/** Sender label for display: "bot" for bot messages, otherwise the raw sender handle. */
|
|
30
|
+
export declare function senderLabel(message: LoggedMessage): string;
|
|
31
|
+
/** Collapse multiline text to its first line with "..." appended. */
|
|
32
|
+
export declare function firstLinePreview(text: string | null): string;
|
|
33
|
+
export interface ChatStoreConfig {
|
|
34
|
+
workingDir: string;
|
|
35
|
+
}
|
|
36
|
+
export interface ChatStore {
|
|
37
|
+
logIncoming(message: IncomingMessage): Promise<void>;
|
|
38
|
+
logOutgoing(chatGuid: string, text: string, messageType: MessageType, groupName?: string, isError?: boolean): Promise<void>;
|
|
39
|
+
}
|
|
40
|
+
export declare function createChatStore(config: ChatStoreConfig): ChatStore;
|
|
41
|
+
//# sourceMappingURL=store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAKH,OAAO,KAAK,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAI/D,MAAM,WAAW,aAAa;IAC7B,0BAA0B;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,8CAA8C;IAC9C,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,0DAA0D;IAC1D,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,KAAK,EAAE,OAAO,CAAC;IACf,WAAW,EAAE,WAAW,CAAC;IACzB,4DAA4D;IAC5D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,sEAAsE;IACtE,OAAO,CAAC,EAAE,OAAO,CAAC;CAClB;AAID,yFAAyF;AACzF,wBAAgB,WAAW,CAAC,OAAO,EAAE,aAAa,GAAG,MAAM,CAE1D;AAED,qEAAqE;AACrE,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,CAI5D;AAID,MAAM,WAAW,eAAe;IAC/B,UAAU,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,SAAS;IACzB,WAAW,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACrD,WAAW,CACV,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,EACZ,WAAW,EAAE,WAAW,EACxB,SAAS,CAAC,EAAE,MAAM,EAClB,OAAO,CAAC,EAAE,OAAO,GACf,OAAO,CAAC,IAAI,CAAC,CAAC;CACjB;AAED,wBAAgB,eAAe,CAAC,MAAM,EAAE,eAAe,GAAG,SAAS,CAwDlE"}
|
package/dist/store.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ChatStore — persistent message log for each iMessage chat.
|
|
3
|
+
*
|
|
4
|
+
* Workspace layout:
|
|
5
|
+
*
|
|
6
|
+
* workingDir/
|
|
7
|
+
* <chatGuid>/ e.g. "iMessage;-;+16501234567"
|
|
8
|
+
* log.jsonl one JSON line per message, append-only
|
|
9
|
+
*
|
|
10
|
+
* Each line is a LoggedMessage serialised as JSON.
|
|
11
|
+
* The file is never rewritten — only appended to.
|
|
12
|
+
*/
|
|
13
|
+
import { existsSync, mkdirSync } from "node:fs";
|
|
14
|
+
import { appendFile } from "node:fs/promises";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
// ── Display helpers ───────────────────────────────────────────────────────────
|
|
17
|
+
/** Sender label for display: "bot" for bot messages, otherwise the raw sender handle. */
|
|
18
|
+
export function senderLabel(message) {
|
|
19
|
+
return message.isBot ? "bot" : message.sender;
|
|
20
|
+
}
|
|
21
|
+
/** Collapse multiline text to its first line with "..." appended. */
|
|
22
|
+
export function firstLinePreview(text) {
|
|
23
|
+
const full = text && text.trim() !== "" ? text : "[image]";
|
|
24
|
+
const firstLine = full.split("\n")[0] ?? "";
|
|
25
|
+
return full.includes("\n") ? `${firstLine}...` : full;
|
|
26
|
+
}
|
|
27
|
+
export function createChatStore(config) {
|
|
28
|
+
const { workingDir } = config;
|
|
29
|
+
if (!existsSync(workingDir)) {
|
|
30
|
+
mkdirSync(workingDir, { recursive: true });
|
|
31
|
+
}
|
|
32
|
+
function chatDir(chatGuid) {
|
|
33
|
+
const dir = join(workingDir, chatGuid);
|
|
34
|
+
if (!existsSync(dir)) {
|
|
35
|
+
mkdirSync(dir, { recursive: true });
|
|
36
|
+
}
|
|
37
|
+
return dir;
|
|
38
|
+
}
|
|
39
|
+
function logPath(chatGuid) {
|
|
40
|
+
return join(chatDir(chatGuid), "log.jsonl");
|
|
41
|
+
}
|
|
42
|
+
async function append(chatGuid, entry) {
|
|
43
|
+
await appendFile(logPath(chatGuid), `${JSON.stringify(entry)}\n`, "utf-8");
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
async logIncoming(message) {
|
|
47
|
+
const attachmentPaths = message.attachments.map((a) => a.path);
|
|
48
|
+
await append(message.chatGuid, {
|
|
49
|
+
date: new Date().toISOString(),
|
|
50
|
+
sender: message.sender,
|
|
51
|
+
text: message.text,
|
|
52
|
+
attachments: attachmentPaths,
|
|
53
|
+
isBot: false,
|
|
54
|
+
messageType: message.messageType,
|
|
55
|
+
...(message.messageType === "group" && { groupName: message.groupName }),
|
|
56
|
+
});
|
|
57
|
+
},
|
|
58
|
+
async logOutgoing(chatGuid, text, messageType, groupName, isError) {
|
|
59
|
+
await append(chatGuid, {
|
|
60
|
+
date: new Date().toISOString(),
|
|
61
|
+
sender: "bot",
|
|
62
|
+
text,
|
|
63
|
+
attachments: [],
|
|
64
|
+
isBot: true,
|
|
65
|
+
isError: isError === true ? true : undefined,
|
|
66
|
+
messageType,
|
|
67
|
+
...(messageType === "group" && { groupName }),
|
|
68
|
+
});
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.js","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"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"}
|
package/dist/tasks.d.ts
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
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 type { AgentManager } from "./agent.js";
|
|
23
|
+
import type { DigestLogger } from "./logger.js";
|
|
24
|
+
import type { BeforeTask, EndTask, StartTask } from "./pipeline.js";
|
|
25
|
+
import type { SelfEchoFilter } from "./self-echo.js";
|
|
26
|
+
import type { MessageSender } from "./send.js";
|
|
27
|
+
import type { Settings } from "./settings.js";
|
|
28
|
+
import type { ChatStore } from "./store.js";
|
|
29
|
+
/**
|
|
30
|
+
* Log the incoming message to console. Always passes through.
|
|
31
|
+
*
|
|
32
|
+
* Examples:
|
|
33
|
+
* [sid] <- [DM] +16501234567: hey what's up
|
|
34
|
+
* [sid] <- [SMS] +16501234567: can you call me
|
|
35
|
+
* [sid] <- [GROUP] Family|+16501234567: dinner at 6? [2 attachment(s)]
|
|
36
|
+
*/
|
|
37
|
+
export declare function createLogIncomingTask(digestLogger: DigestLogger): BeforeTask;
|
|
38
|
+
/** Persist the incoming message to log.jsonl. Always passes through. */
|
|
39
|
+
export declare function createStoreIncomingTask(store: ChatStore): BeforeTask;
|
|
40
|
+
/** Drop messages that are echoes of the bot's own replies. */
|
|
41
|
+
export declare function createDropSelfEchoTask(echoFilter: SelfEchoFilter): BeforeTask;
|
|
42
|
+
/**
|
|
43
|
+
* Drop messages when reply is disabled for this chat by settings.
|
|
44
|
+
*
|
|
45
|
+
* Resolution priority (highest to lowest):
|
|
46
|
+
* blacklist["chatGuid"] > whitelist["chatGuid"] > blacklist["*"] > whitelist["*"]
|
|
47
|
+
*
|
|
48
|
+
* Examples:
|
|
49
|
+
* whitelist: ["*"] → reply to everyone
|
|
50
|
+
* whitelist: ["1"] → reply only to "1"
|
|
51
|
+
* whitelist: ["*"], bl: ["2"] → reply to everyone except "2"
|
|
52
|
+
* blacklist: ["*"] → log-only for all
|
|
53
|
+
* whitelist: ["1"], bl: ["*"] → reply only to "1"
|
|
54
|
+
* whitelist: ["1"], bl: ["1"] → no reply (blacklist wins)
|
|
55
|
+
*/
|
|
56
|
+
export declare function createCheckReplyEnabledTask(getSettings: () => Settings): BeforeTask;
|
|
57
|
+
/**
|
|
58
|
+
* Intercept slash commands (e.g. "/new", "/status") before they reach the agent.
|
|
59
|
+
* Sets shouldContinue=false on the outgoing message to skip subsequent start tasks.
|
|
60
|
+
*
|
|
61
|
+
* Supported commands:
|
|
62
|
+
* /new — reset the agent session for this chat (equivalent to /new in pi coding agent).
|
|
63
|
+
* /status — show session stats: tokens, cost, context usage, model, thinking level.
|
|
64
|
+
*/
|
|
65
|
+
export declare function createCommandHandlerTask(agent: AgentManager): StartTask;
|
|
66
|
+
/**
|
|
67
|
+
* Read image attachments from local disk and populate incoming.images in-place.
|
|
68
|
+
* Non-image attachments are skipped; failed reads are logged and silently skipped.
|
|
69
|
+
*/
|
|
70
|
+
export declare function createDownloadImagesTask(): BeforeTask;
|
|
71
|
+
export declare function createResizeImagesTask(): BeforeTask;
|
|
72
|
+
/** Send the message to the agent and dispatch a reply for each agent turn. */
|
|
73
|
+
export declare function createCallAgentTask(agent: AgentManager): StartTask;
|
|
74
|
+
/** Remember echo and send reply via Messages.app AppleScript. */
|
|
75
|
+
export declare function createSendReplyTask(echoFilter: SelfEchoFilter, sender: MessageSender): EndTask;
|
|
76
|
+
/**
|
|
77
|
+
* Log the outgoing reply to console.
|
|
78
|
+
*
|
|
79
|
+
* Examples:
|
|
80
|
+
* [sid] -> [DM] +16501234567: sure, I'll check
|
|
81
|
+
* [sid] -> [SMS] +16501234567: got it
|
|
82
|
+
* [sid] -> [GROUP] Family: sounds good!
|
|
83
|
+
* [sid] -> [DM] +16501234567: (reaction: love)
|
|
84
|
+
*/
|
|
85
|
+
export declare function createLogOutgoingTask(digestLogger: DigestLogger): EndTask;
|
|
86
|
+
/** Persist the outgoing reply to log.jsonl. */
|
|
87
|
+
export declare function createStoreOutgoingTask(store: ChatStore): EndTask;
|
|
88
|
+
//# sourceMappingURL=tasks.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tasks.d.ts","sourceRoot":"","sources":["../src/tasks.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAKH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC/C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,KAAK,EAAE,UAAU,EAAc,OAAO,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAChF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AACrD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAC/C,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAE9C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAiB5C;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CAAC,YAAY,EAAE,YAAY,GAAG,UAAU,CAU5E;AAED,wEAAwE;AACxE,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,SAAS,GAAG,UAAU,CAOpE;AAED,8DAA8D;AAC9D,wBAAgB,sBAAsB,CAAC,UAAU,EAAE,cAAc,GAAG,UAAU,CAQ7E;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,2BAA2B,CAAC,WAAW,EAAE,MAAM,QAAQ,GAAG,UAAU,CAQnF;AAID;;;;;;;GAOG;AACH,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,YAAY,GAAG,SAAS,CA0BvE;AAID;;;GAGG;AACH,wBAAgB,wBAAwB,IAAI,UAAU,CAmBrD;AAcD,wBAAgB,sBAAsB,IAAI,UAAU,CAcnD;AA+BD,8EAA8E;AAC9E,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,YAAY,GAAG,SAAS,CAOlE;AAID,iEAAiE;AACjE,wBAAgB,mBAAmB,CAAC,UAAU,EAAE,cAAc,EAAE,MAAM,EAAE,aAAa,GAAG,OAAO,CAU9F;AAED;;;;;;;;GAQG;AACH,wBAAgB,qBAAqB,CAAC,YAAY,EAAE,YAAY,GAAG,OAAO,CAUzE;AAED,+CAA+C;AAC/C,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,SAAS,GAAG,OAAO,CAYjE"}
|