@kingcrab/pi-imessage 0.0.42 → 1.0.0
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 +101 -108
- package/dist/agent/behavior.js +79 -0
- package/dist/agent/chats.js +67 -0
- package/dist/agent/commands.js +163 -0
- package/dist/agent/compaction.js +51 -0
- package/dist/agent/deliver.js +139 -0
- package/dist/agent/direct-send.js +68 -0
- package/dist/agent/harness.js +79 -0
- package/dist/agent/health.js +32 -0
- package/dist/agent/models.js +69 -0
- package/dist/agent/progress-format.js +87 -0
- package/dist/agent/progress.js +135 -0
- package/dist/agent/prompt.js +79 -0
- package/dist/agent/replies.js +9 -0
- package/dist/agent/run.js +159 -0
- package/dist/agent/scheduling.js +290 -0
- package/dist/agent/time.js +18 -0
- package/dist/cli.js +88 -191
- package/dist/config/settings.js +60 -0
- package/dist/extensions/browser.js +326 -0
- package/dist/extensions/final-text.js +23 -0
- package/dist/extensions/memory.js +109 -0
- package/dist/extensions/message.js +33 -0
- package/dist/extensions/read-image.js +72 -0
- package/dist/extensions/schedules.js +41 -0
- package/dist/extensions/subagent.js +54 -0
- package/dist/extensions/web.js +99 -0
- package/dist/extensions/workspace.js +98 -0
- package/dist/main.js +571 -74
- package/dist/transport/attachments.js +37 -0
- package/dist/transport/http.js +110 -0
- package/dist/{rich-text.js → transport/rich-text.js} +25 -13
- package/dist/{send.js → transport/send.js} +13 -13
- package/dist/transport/watch.js +124 -0
- package/dist/web/app.js +748 -0
- package/dist/web/history.js +87 -0
- package/dist/web/memory.js +81 -0
- package/dist/web/page.js +28 -0
- package/dist/web/schedules.js +74 -0
- package/dist/web/server.js +348 -0
- package/dist/web/style.css +543 -0
- package/dist/web/tasks.js +63 -0
- package/docs/adr/0001-durable-owns-state.md +11 -0
- package/docs/adr/0002-module-layering.md +11 -0
- package/docs/adr/0003-replies-at-most-once.md +11 -0
- package/docs/adr/0004-native-extensions.md +11 -0
- package/docs/adr/0005-history-migration.md +11 -0
- package/docs/adr/0006-jsonl-storage.md +11 -0
- package/docs/adr/0007-images-by-path.md +11 -0
- package/docs/adr/0008-compaction-and-run.md +11 -0
- package/docs/adr/0009-one-shot-cutover.md +11 -0
- package/docs/adr/0010-test-budget.md +11 -0
- package/docs/adr/0011-reconcile-unsent-answers.md +24 -0
- package/docs/adr/0012-run-in-owned-conversation.md +22 -0
- package/docs/adr/0013-thin-adapters-around-durable.md +18 -0
- package/docs/adr/0014-ephemeral-private-storage.md +13 -0
- package/docs/adr/0015-ordinary-stop-keeps-native-inbox.md +21 -0
- package/docs/adr/0016-process-http-transport.md +21 -0
- package/docs/adr/0017-browser-cli-owned-scope.md +22 -0
- package/docs/adr/0018-tasks-without-prompt-api.md +19 -0
- package/docs/adr/0019-remove-scheduled-services.md +41 -0
- package/docs/adr/0020-chat-task-progress.md +41 -0
- package/docs/adr/0021-readable-progress-without-link-previews.md +33 -0
- package/docs/adr/0022-progress-admission-and-settlement.md +35 -0
- package/docs/adr/0023-retain-progress-type-prefixes.md +18 -0
- package/docs/adr/0024-status-marker-before-type-prefix.md +13 -0
- package/docs/adr/0025-reusable-progress-and-command-preview.md +49 -0
- package/docs/adr/0026-inline-command-only-requests.md +16 -0
- package/docs/adr/0027-command-preview-width.md +10 -0
- package/docs/adr/0028-command-preview-120.md +8 -0
- package/docs/adr/0030-shared-web-read-cache.md +33 -0
- package/docs/adr/0031-quiet-compaction-progress.md +32 -0
- package/docs/adr/0032-native-background-schedules.md +58 -0
- package/docs/adr/0033-operator-scheduled-run.md +23 -0
- package/docs/adr/0034-machine-local-schedule-time.md +36 -0
- package/docs/adr/0035-external-business-task-extensions.md +56 -0
- package/docs/adr/0036-workspace-policy-and-neutral-service-id.md +37 -0
- package/docs/adr/0037-restart-during-in-flight-work.md +30 -0
- package/docs/adr/0038-restart-any-time.md +25 -0
- package/docs/adr/0039-rerun-interrupted-tools.md +24 -0
- package/docs/adr/0040-chat-progress-notices.md +44 -0
- package/docs/adr/0041-restart-and-recovery.md +28 -0
- package/docs/adr/0042-background-schedules-and-workspace-extensions.md +44 -0
- package/docs/adr/0043-remove-legacy-migration.md +23 -0
- package/docs/adr/0044-message-channels.md +32 -0
- package/docs/adr/0045-chat-scheduling-tools.md +51 -0
- package/docs/adr/0046-selective-command-redaction.md +31 -0
- package/docs/adr/0047-one-scheduling-path.md +158 -0
- package/docs/adr/README.md +50 -0
- package/docs/api.md +92 -0
- package/docs/architecture.md +164 -0
- package/docs/browser.md +93 -0
- package/docs/memory.md +27 -0
- package/docs/screenshot.png +0 -0
- package/docs/workspace-extensions.md +133 -0
- package/examples/workspace-extensions/system-context/index.ts +69 -0
- package/examples/workspace-extensions/system-context/package.json +3 -0
- package/ops/README.md +267 -0
- package/ops/preflight.mjs +28 -0
- package/package.json +33 -45
- package/dist/agent.d.ts +0 -60
- package/dist/agent.d.ts.map +0 -1
- package/dist/agent.js +0 -715
- package/dist/agent.js.map +0 -1
- package/dist/cli.d.ts +0 -15
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/imessage.d.ts +0 -33
- package/dist/imessage.d.ts.map +0 -1
- package/dist/imessage.js +0 -76
- package/dist/imessage.js.map +0 -1
- package/dist/logger.d.ts +0 -33
- package/dist/logger.d.ts.map +0 -1
- package/dist/logger.js +0 -104
- package/dist/logger.js.map +0 -1
- package/dist/main.d.ts +0 -5
- package/dist/main.d.ts.map +0 -1
- package/dist/main.js.map +0 -1
- package/dist/memory.d.ts +0 -53
- package/dist/memory.d.ts.map +0 -1
- package/dist/memory.js +0 -196
- package/dist/memory.js.map +0 -1
- package/dist/model-health.d.ts +0 -11
- package/dist/model-health.d.ts.map +0 -1
- package/dist/model-health.js +0 -67
- package/dist/model-health.js.map +0 -1
- package/dist/pipeline.d.ts +0 -27
- package/dist/pipeline.d.ts.map +0 -1
- package/dist/pipeline.js +0 -59
- package/dist/pipeline.js.map +0 -1
- package/dist/queue.d.ts +0 -23
- package/dist/queue.d.ts.map +0 -1
- package/dist/queue.js +0 -89
- package/dist/queue.js.map +0 -1
- package/dist/rich-text.d.ts +0 -26
- package/dist/rich-text.d.ts.map +0 -1
- package/dist/rich-text.js.map +0 -1
- package/dist/self-echo.d.ts +0 -19
- package/dist/self-echo.d.ts.map +0 -1
- package/dist/self-echo.js +0 -54
- package/dist/self-echo.js.map +0 -1
- package/dist/send.d.ts +0 -24
- package/dist/send.d.ts.map +0 -1
- package/dist/send.js.map +0 -1
- package/dist/settings.d.ts +0 -44
- package/dist/settings.d.ts.map +0 -1
- package/dist/settings.js +0 -80
- package/dist/settings.js.map +0 -1
- package/dist/store.d.ts +0 -57
- package/dist/store.d.ts.map +0 -1
- package/dist/store.js +0 -124
- package/dist/store.js.map +0 -1
- package/dist/tasks.d.ts +0 -95
- package/dist/tasks.d.ts.map +0 -1
- package/dist/tasks.js +0 -384
- package/dist/tasks.js.map +0 -1
- package/dist/types.d.ts +0 -93
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -38
- package/dist/types.js.map +0 -1
- package/dist/watch.d.ts +0 -21
- package/dist/watch.d.ts.map +0 -1
- package/dist/watch.js +0 -219
- package/dist/watch.js.map +0 -1
- package/dist/web/data.d.ts +0 -11
- package/dist/web/data.d.ts.map +0 -1
- package/dist/web/data.js +0 -56
- package/dist/web/data.js.map +0 -1
- package/dist/web/html.d.ts +0 -5
- package/dist/web/html.d.ts.map +0 -1
- package/dist/web/html.js +0 -16
- package/dist/web/html.js.map +0 -1
- package/dist/web/index.d.ts +0 -23
- package/dist/web/index.d.ts.map +0 -1
- package/dist/web/index.js +0 -279
- package/dist/web/index.js.map +0 -1
- package/dist/web/render.d.ts +0 -11
- package/dist/web/render.d.ts.map +0 -1
- package/dist/web/render.js +0 -56
- package/dist/web/render.js.map +0 -1
- package/dist/web/templates/logs.eta +0 -75
- package/dist/web/templates/memory.eta +0 -91
- package/dist/web/templates/page.eta +0 -98
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
3
|
+
import { access, copyFile, mkdir, readFile, rename, rm } from "node:fs/promises";
|
|
4
|
+
import { extname, join } from "node:path";
|
|
5
|
+
import { promisify } from "node:util";
|
|
6
|
+
const execute = promisify(execFile);
|
|
7
|
+
export async function archiveAttachments(workingDir, chatGuid, paths) {
|
|
8
|
+
const directory = join(workingDir, "attachments", encodeURIComponent(chatGuid));
|
|
9
|
+
await mkdir(directory, { recursive: true });
|
|
10
|
+
const result = [];
|
|
11
|
+
for (const source of paths) {
|
|
12
|
+
const hash = createHash("sha256")
|
|
13
|
+
.update(await readFile(source))
|
|
14
|
+
.digest("hex");
|
|
15
|
+
const extension = extname(source).toLowerCase();
|
|
16
|
+
const heic = [".heic", ".heif"].includes(extension);
|
|
17
|
+
const destination = join(directory, hash + (heic ? ".jpg" : extension));
|
|
18
|
+
try {
|
|
19
|
+
await access(destination);
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
const temporary = `${destination}.${randomUUID()}`;
|
|
23
|
+
try {
|
|
24
|
+
if (heic)
|
|
25
|
+
await execute("/usr/bin/sips", ["-s", "format", "jpeg", source, "--out", temporary]);
|
|
26
|
+
else
|
|
27
|
+
await copyFile(source, temporary);
|
|
28
|
+
await rename(temporary, destination);
|
|
29
|
+
}
|
|
30
|
+
finally {
|
|
31
|
+
await rm(temporary, { force: true });
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
result.push(destination);
|
|
35
|
+
}
|
|
36
|
+
return result;
|
|
37
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { EventEmitter } from "node:events";
|
|
2
|
+
import { Client, EnvHttpProxyAgent, getGlobalDispatcher, install, Pool, setGlobalDispatcher, } from "undici";
|
|
3
|
+
const initialFetch = globalThis.fetch;
|
|
4
|
+
const fetchGlobals = [
|
|
5
|
+
"fetch",
|
|
6
|
+
"Headers",
|
|
7
|
+
"Response",
|
|
8
|
+
"Request",
|
|
9
|
+
"FormData",
|
|
10
|
+
"WebSocket",
|
|
11
|
+
"CloseEvent",
|
|
12
|
+
"ErrorEvent",
|
|
13
|
+
"MessageEvent",
|
|
14
|
+
"EventSource",
|
|
15
|
+
];
|
|
16
|
+
let processOwned = false;
|
|
17
|
+
// A terminated body still rejects its reader. Avoid an unhandled Client error crashing the process.
|
|
18
|
+
function listenForClientErrors(dispatcher) {
|
|
19
|
+
EventEmitter.prototype.on.call(dispatcher, "error", () => { });
|
|
20
|
+
return dispatcher;
|
|
21
|
+
}
|
|
22
|
+
function createClient(origin, options) {
|
|
23
|
+
return listenForClientErrors(new Client(origin, options));
|
|
24
|
+
}
|
|
25
|
+
/** One process owner; settings are defaults beneath explicit environment proxy values. */
|
|
26
|
+
export function openHttpTransport(settings) {
|
|
27
|
+
if (processOwned)
|
|
28
|
+
throw new Error("HTTP transport already has a process owner");
|
|
29
|
+
const previousDispatcher = getGlobalDispatcher();
|
|
30
|
+
const globals = [];
|
|
31
|
+
const retiring = new Set();
|
|
32
|
+
let retirementError;
|
|
33
|
+
let current;
|
|
34
|
+
let closing;
|
|
35
|
+
function retire(dispatcher) {
|
|
36
|
+
const pending = dispatcher
|
|
37
|
+
.close()
|
|
38
|
+
.catch((error) => {
|
|
39
|
+
retirementError ??= error;
|
|
40
|
+
})
|
|
41
|
+
.finally(() => retiring.delete(pending));
|
|
42
|
+
retiring.add(pending);
|
|
43
|
+
}
|
|
44
|
+
function reload(next) {
|
|
45
|
+
if (closing)
|
|
46
|
+
throw new Error("HTTP transport is closed");
|
|
47
|
+
if (current && getGlobalDispatcher() !== current)
|
|
48
|
+
throw new Error("HTTP dispatcher ownership changed; refusing to replace another caller's transport");
|
|
49
|
+
if (!Number.isFinite(next.idleTimeoutMs) || next.idleTimeoutMs < 0)
|
|
50
|
+
throw new Error("Invalid HTTP idle timeout");
|
|
51
|
+
const proxy = next.proxy?.trim() || undefined;
|
|
52
|
+
const dispatcher = listenForClientErrors(new EnvHttpProxyAgent({
|
|
53
|
+
httpProxy: process.env.http_proxy ?? process.env.HTTP_PROXY ?? proxy ?? "",
|
|
54
|
+
httpsProxy: process.env.https_proxy ?? process.env.HTTPS_PROXY ?? proxy ?? "",
|
|
55
|
+
noProxy: process.env.no_proxy ?? process.env.NO_PROXY ?? "",
|
|
56
|
+
allowH2: false,
|
|
57
|
+
proxyTunnel: true,
|
|
58
|
+
headersTimeout: Math.floor(next.idleTimeoutMs),
|
|
59
|
+
bodyTimeout: Math.floor(next.idleTimeoutMs),
|
|
60
|
+
connect: { autoSelectFamilyAttemptTimeout: 2000 },
|
|
61
|
+
clientFactory: createClient,
|
|
62
|
+
factory: (origin, options) => options.connections === 1
|
|
63
|
+
? createClient(origin, options)
|
|
64
|
+
: listenForClientErrors(new Pool(origin, { ...options, factory: createClient })),
|
|
65
|
+
}));
|
|
66
|
+
setGlobalDispatcher(dispatcher);
|
|
67
|
+
const previous = current;
|
|
68
|
+
current = dispatcher;
|
|
69
|
+
if (previous)
|
|
70
|
+
retire(previous); // close drains active requests; it does not abort them.
|
|
71
|
+
}
|
|
72
|
+
reload(settings);
|
|
73
|
+
processOwned = true;
|
|
74
|
+
// Match fetch and its constructors to the dispatcher, but preserve a deliberate fetch override.
|
|
75
|
+
if (globalThis.fetch === initialFetch) {
|
|
76
|
+
for (const name of fetchGlobals)
|
|
77
|
+
globals.push({ name, previous: Object.getOwnPropertyDescriptor(globalThis, name) });
|
|
78
|
+
install();
|
|
79
|
+
for (const entry of globals)
|
|
80
|
+
entry.installed = Object.getOwnPropertyDescriptor(globalThis, entry.name);
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
reload,
|
|
84
|
+
close() {
|
|
85
|
+
closing ??= (async () => {
|
|
86
|
+
if (getGlobalDispatcher() === current)
|
|
87
|
+
setGlobalDispatcher(previousDispatcher);
|
|
88
|
+
for (const entry of globals) {
|
|
89
|
+
if (Object.getOwnPropertyDescriptor(globalThis, entry.name)?.value !== entry.installed?.value)
|
|
90
|
+
continue;
|
|
91
|
+
if (entry.previous)
|
|
92
|
+
Object.defineProperty(globalThis, entry.name, entry.previous);
|
|
93
|
+
else
|
|
94
|
+
Reflect.deleteProperty(globalThis, entry.name);
|
|
95
|
+
}
|
|
96
|
+
if (current)
|
|
97
|
+
retire(current);
|
|
98
|
+
try {
|
|
99
|
+
await Promise.all(retiring);
|
|
100
|
+
if (retirementError)
|
|
101
|
+
throw new Error("HTTP transport cleanup failed", { cause: retirementError });
|
|
102
|
+
}
|
|
103
|
+
finally {
|
|
104
|
+
processOwned = false;
|
|
105
|
+
}
|
|
106
|
+
})();
|
|
107
|
+
return closing;
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
}
|
|
@@ -104,7 +104,12 @@ function parseMarkdownToSpans(markdown) {
|
|
|
104
104
|
for (const match of markdown.matchAll(pattern)) {
|
|
105
105
|
const index = match.index ?? 0;
|
|
106
106
|
if (index > lastIndex) {
|
|
107
|
-
spans.push({
|
|
107
|
+
spans.push({
|
|
108
|
+
text: markdown.slice(lastIndex, index),
|
|
109
|
+
bold: false,
|
|
110
|
+
italic: false,
|
|
111
|
+
strikethrough: false,
|
|
112
|
+
});
|
|
108
113
|
}
|
|
109
114
|
const token = match[0];
|
|
110
115
|
if (token.startsWith("**") && token.endsWith("**")) {
|
|
@@ -163,16 +168,23 @@ async function copyRichTextToClipboard(text, options) {
|
|
|
163
168
|
await rm(dir, { recursive: true, force: true });
|
|
164
169
|
}
|
|
165
170
|
}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
171
|
+
// The pasteboard and frontmost conversation are process-wide, not per chat.
|
|
172
|
+
let pendingSend = Promise.resolve();
|
|
173
|
+
export function sendRichTextMessage(chatGuid, text, options) {
|
|
174
|
+
const resolvedOptions = { ...DEFAULT_RICH_TEXT_OPTIONS, ...options };
|
|
175
|
+
const operation = pendingSend.then(async () => {
|
|
176
|
+
const parts = chatGuid.split(";");
|
|
177
|
+
const isGroup = parts[1] === "+";
|
|
178
|
+
if (isGroup) {
|
|
179
|
+
throw new Error("Rich-text sending is currently only supported for direct-message chats");
|
|
180
|
+
}
|
|
181
|
+
const recipient = recipientFromChatGuid(chatGuid);
|
|
182
|
+
await copyRichTextToClipboard(text, resolvedOptions);
|
|
183
|
+
await openConversation(recipient);
|
|
184
|
+
await pasteAndSend();
|
|
185
|
+
console.log(`[send] sent rich text to ${chatGuid}: "${text.substring(0, 60)}"`);
|
|
186
|
+
});
|
|
187
|
+
// Release the transport turn on failure, without retrying a possibly completed send.
|
|
188
|
+
pendingSend = operation.catch(() => { });
|
|
189
|
+
return operation;
|
|
177
190
|
}
|
|
178
|
-
//# sourceMappingURL=rich-text.js.map
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import { execFile } from "node:child_process";
|
|
17
17
|
import { randomUUID } from "node:crypto";
|
|
18
|
-
import {
|
|
18
|
+
import { accessSync, constants } from "node:fs";
|
|
19
19
|
import { copyFile, mkdir, unlink } from "node:fs/promises";
|
|
20
20
|
import { homedir } from "node:os";
|
|
21
21
|
import { basename, extname, join } from "node:path";
|
|
@@ -26,8 +26,6 @@ const SCRIPT_TIMEOUT_MS = 30_000;
|
|
|
26
26
|
const ATTACHMENT_VERIFY_TIMEOUT_MS = 30_000;
|
|
27
27
|
const ATTACHMENT_VERIFY_POLL_MS = 500;
|
|
28
28
|
const MESSAGES_APP_PATH = "/System/Applications/Messages.app";
|
|
29
|
-
const ATTACHMENTS_ROOT = join(homedir(), "Library", "Messages", "Attachments");
|
|
30
|
-
const STAGING_DIR = join(ATTACHMENTS_ROOT, "pi-imessage");
|
|
31
29
|
const CHAT_DB_PATH = join(homedir(), "Library", "Messages", "chat.db");
|
|
32
30
|
// ── AppleScript helpers ───────────────────────────────────────────────────────
|
|
33
31
|
/** Escape special characters for AppleScript string literals. */
|
|
@@ -94,17 +92,18 @@ end tell`;
|
|
|
94
92
|
*
|
|
95
93
|
* Returns the staged path; caller is responsible for cleanup after send.
|
|
96
94
|
*/
|
|
97
|
-
async function stageAttachment(sourcePath) {
|
|
95
|
+
async function stageAttachment(sourcePath, attachmentsRoot) {
|
|
98
96
|
// If the source is already inside the Attachments root, imagent can read it.
|
|
99
|
-
if (sourcePath.startsWith(`${
|
|
97
|
+
if (sourcePath.startsWith(`${attachmentsRoot}/`)) {
|
|
100
98
|
return { stagedPath: sourcePath, cleanup: async () => { } };
|
|
101
99
|
}
|
|
102
|
-
|
|
100
|
+
const stagingDirectory = join(attachmentsRoot, "pi-imessage");
|
|
101
|
+
await mkdir(stagingDirectory, { recursive: true, mode: 0o755 });
|
|
103
102
|
const ext = extname(sourcePath);
|
|
104
103
|
const base = basename(sourcePath, ext)
|
|
105
104
|
.replace(/[^A-Za-z0-9._-]/g, "_")
|
|
106
105
|
.slice(0, 40);
|
|
107
|
-
const stagedPath = join(
|
|
106
|
+
const stagedPath = join(stagingDirectory, `${Date.now()}-${randomUUID().slice(0, 8)}-${base}${ext}`);
|
|
108
107
|
await copyFile(sourcePath, stagedPath);
|
|
109
108
|
return {
|
|
110
109
|
stagedPath,
|
|
@@ -119,7 +118,7 @@ async function stageAttachment(sourcePath) {
|
|
|
119
118
|
};
|
|
120
119
|
}
|
|
121
120
|
/** Poll chat.db until the staged attachment finishes transferring (or times out). */
|
|
122
|
-
async function verifyAttachmentSent(stagedPath, timeoutMs) {
|
|
121
|
+
async function verifyAttachmentSent(stagedPath, timeoutMs, dbPath) {
|
|
123
122
|
const stagedBase = basename(stagedPath);
|
|
124
123
|
// filename in chat.db can be stored as absolute path or "~/Library/Messages/Attachments/...",
|
|
125
124
|
// so match on the unique basename we generated (Date.now() + UUID prefix).
|
|
@@ -134,7 +133,7 @@ async function verifyAttachmentSent(stagedPath, timeoutMs) {
|
|
|
134
133
|
let last = { attachmentId: null, transferState: null, isSent: null, error: null };
|
|
135
134
|
while (Date.now() < deadline) {
|
|
136
135
|
try {
|
|
137
|
-
const { stdout } = await execFileAsync("sqlite3", ["-json",
|
|
136
|
+
const { stdout } = await execFileAsync("sqlite3", ["-json", dbPath, sql], { timeout: 5_000 });
|
|
138
137
|
const rows = stdout.trim() ? JSON.parse(stdout) : [];
|
|
139
138
|
const row = rows[0];
|
|
140
139
|
if (row) {
|
|
@@ -221,7 +220,9 @@ end tell`;
|
|
|
221
220
|
console.warn(`[send] failed to check iMessage status; continuing so the watchdog can diagnose send failures: ${error instanceof Error ? error.message : String(error)}`);
|
|
222
221
|
}
|
|
223
222
|
}
|
|
224
|
-
export function createMessageSender() {
|
|
223
|
+
export function createMessageSender(options = {}) {
|
|
224
|
+
const attachmentsRoot = options.attachmentsRoot ?? join(homedir(), "Library", "Messages", "Attachments");
|
|
225
|
+
const dbPath = options.dbPath ?? CHAT_DB_PATH;
|
|
225
226
|
return {
|
|
226
227
|
async sendMessage(chatGuid, text, richText) {
|
|
227
228
|
const startTime = Date.now();
|
|
@@ -241,7 +242,7 @@ export function createMessageSender() {
|
|
|
241
242
|
accessSync(filePath, constants.R_OK);
|
|
242
243
|
const origName = basename(filePath);
|
|
243
244
|
console.log(`[send] start: ${chatGuid} mode=attachment file=${origName}`);
|
|
244
|
-
const { stagedPath, cleanup } = await stageAttachment(filePath);
|
|
245
|
+
const { stagedPath, cleanup } = await stageAttachment(filePath, attachmentsRoot);
|
|
245
246
|
const staged = stagedPath !== filePath;
|
|
246
247
|
if (staged) {
|
|
247
248
|
console.log(`[send] staged attachment for imagent: ${basename(stagedPath)}`);
|
|
@@ -249,7 +250,7 @@ export function createMessageSender() {
|
|
|
249
250
|
try {
|
|
250
251
|
const script = buildSendAttachmentScript(chatGuid, stagedPath);
|
|
251
252
|
await execFileAsync("osascript", ["-e", script], { timeout: SCRIPT_TIMEOUT_MS });
|
|
252
|
-
const status = await verifyAttachmentSent(stagedPath, ATTACHMENT_VERIFY_TIMEOUT_MS);
|
|
253
|
+
const status = await verifyAttachmentSent(stagedPath, ATTACHMENT_VERIFY_TIMEOUT_MS, dbPath);
|
|
253
254
|
if (!status.ok) {
|
|
254
255
|
throw new Error(`attachment send failed for ${origName}: ${status.detail}`);
|
|
255
256
|
}
|
|
@@ -266,4 +267,3 @@ export function createMessageSender() {
|
|
|
266
267
|
},
|
|
267
268
|
};
|
|
268
269
|
}
|
|
269
|
-
//# sourceMappingURL=send.js.map
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import Database from "better-sqlite3";
|
|
4
|
+
export const defaultDatabasePath = join(homedir(), "Library", "Messages", "chat.db");
|
|
5
|
+
export function resolveMessageText(row) {
|
|
6
|
+
if (row.text?.trim())
|
|
7
|
+
return row.text.trim();
|
|
8
|
+
const blob = row.attributedBody;
|
|
9
|
+
if (!blob)
|
|
10
|
+
return "";
|
|
11
|
+
const marker = blob.indexOf("NSString");
|
|
12
|
+
if (marker < 0)
|
|
13
|
+
return "";
|
|
14
|
+
let position = blob.indexOf(0x2b, marker + 8) + 1;
|
|
15
|
+
if (position <= 0 || position >= blob.length)
|
|
16
|
+
return "";
|
|
17
|
+
let length = blob[position++];
|
|
18
|
+
if (length === 0x81) {
|
|
19
|
+
if (position + 2 > blob.length)
|
|
20
|
+
return "";
|
|
21
|
+
length = blob.readUInt16LE(position);
|
|
22
|
+
position += 2;
|
|
23
|
+
}
|
|
24
|
+
return position + length <= blob.length
|
|
25
|
+
? blob
|
|
26
|
+
.subarray(position, position + length)
|
|
27
|
+
.toString("utf8")
|
|
28
|
+
.trim()
|
|
29
|
+
: "";
|
|
30
|
+
}
|
|
31
|
+
export function createWatcher(options) {
|
|
32
|
+
const db = new Database(options.dbPath, { readonly: true, fileMustExist: true });
|
|
33
|
+
let cursor;
|
|
34
|
+
try {
|
|
35
|
+
const maximum = db.prepare("SELECT COALESCE(MAX(ROWID),0) AS value FROM message").get().value;
|
|
36
|
+
cursor = options.cursor ?? maximum;
|
|
37
|
+
if (!Number.isSafeInteger(cursor) || cursor < 0 || cursor > maximum)
|
|
38
|
+
throw new Error("Messages cursor is invalid or ahead of the database; reconcile it before starting");
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
db.close();
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
44
|
+
let stopped = false;
|
|
45
|
+
let started = false;
|
|
46
|
+
let closing;
|
|
47
|
+
let timer;
|
|
48
|
+
let pending;
|
|
49
|
+
async function readPage() {
|
|
50
|
+
const rows = db
|
|
51
|
+
.prepare(`SELECT m.ROWID AS rowid,m.guid,m.text,m.attributedBody,
|
|
52
|
+
m.is_from_me AS isFromMe,m.associated_message_type AS reaction,m.service,
|
|
53
|
+
m.thread_originator_guid AS replyGuid,h.id AS sender,c.guid AS chatGuid,c.display_name AS groupName
|
|
54
|
+
FROM message m LEFT JOIN handle h ON h.ROWID=m.handle_id
|
|
55
|
+
LEFT JOIN chat_message_join j ON j.message_id=m.ROWID LEFT JOIN chat c ON c.ROWID=j.chat_id
|
|
56
|
+
WHERE m.ROWID>? ORDER BY m.ROWID LIMIT 200`)
|
|
57
|
+
.all(cursor);
|
|
58
|
+
for (const row of rows) {
|
|
59
|
+
if (stopped)
|
|
60
|
+
break;
|
|
61
|
+
if (!row.isFromMe && !row.reaction && row.chatGuid) {
|
|
62
|
+
const attachments = db
|
|
63
|
+
.prepare(`SELECT a.filename FROM attachment a
|
|
64
|
+
JOIN message_attachment_join j ON j.attachment_id=a.ROWID WHERE j.message_id=?`)
|
|
65
|
+
.all(row.rowid).flatMap(({ filename }) => (filename ? [filename.replace(/^~(?=\/)/, homedir())] : []));
|
|
66
|
+
const reply = row.replyGuid
|
|
67
|
+
? db.prepare("SELECT text,attributedBody FROM message WHERE guid=?").get(row.replyGuid)
|
|
68
|
+
: undefined;
|
|
69
|
+
const text = resolveMessageText(row);
|
|
70
|
+
if (text || attachments.length)
|
|
71
|
+
await options.accept({
|
|
72
|
+
rowid: row.rowid,
|
|
73
|
+
guid: row.guid,
|
|
74
|
+
chatGuid: row.chatGuid,
|
|
75
|
+
text,
|
|
76
|
+
sender: row.sender ?? "unknown",
|
|
77
|
+
service: row.service ?? "iMessage",
|
|
78
|
+
groupName: row.groupName ?? "",
|
|
79
|
+
replyTo: reply ? resolveMessageText(reply) : null,
|
|
80
|
+
attachments,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
await options.saveCursor(row.rowid);
|
|
84
|
+
cursor = row.rowid;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
function poll() {
|
|
88
|
+
if (stopped)
|
|
89
|
+
return Promise.resolve();
|
|
90
|
+
pending ??= readPage().finally(() => {
|
|
91
|
+
pending = undefined;
|
|
92
|
+
});
|
|
93
|
+
return pending;
|
|
94
|
+
}
|
|
95
|
+
function tick() {
|
|
96
|
+
void poll()
|
|
97
|
+
.catch(options.onError)
|
|
98
|
+
.finally(() => {
|
|
99
|
+
if (!stopped)
|
|
100
|
+
timer = setTimeout(tick, options.intervalMs ?? 2000);
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
get cursor() {
|
|
105
|
+
return cursor;
|
|
106
|
+
},
|
|
107
|
+
poll,
|
|
108
|
+
start() {
|
|
109
|
+
if (stopped || started)
|
|
110
|
+
return;
|
|
111
|
+
started = true;
|
|
112
|
+
tick();
|
|
113
|
+
},
|
|
114
|
+
stop() {
|
|
115
|
+
stopped = true;
|
|
116
|
+
clearTimeout(timer);
|
|
117
|
+
closing ??= (async () => {
|
|
118
|
+
await pending?.catch(() => { });
|
|
119
|
+
db.close();
|
|
120
|
+
})();
|
|
121
|
+
return closing;
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
}
|