imsg-mcp 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/CHANGELOG.md +6 -0
- package/LICENSE +21 -0
- package/README.md +127 -0
- package/dist/cli.js +611 -0
- package/dist/cli.js.map +1 -0
- package/dist/dateParse-DJXMfq3a.js +74 -0
- package/dist/dateParse-DJXMfq3a.js.map +1 -0
- package/dist/exportFormats-CWWiy5uz.js +108 -0
- package/dist/exportFormats-CWWiy5uz.js.map +1 -0
- package/dist/exportStream-BaheQ6M4.js +130 -0
- package/dist/exportStream-BaheQ6M4.js.map +1 -0
- package/dist/imessage-db-BVDtx0Sn.js +2263 -0
- package/dist/imessage-db-BVDtx0Sn.js.map +1 -0
- package/dist/index.js +3503 -0
- package/dist/index.js.map +1 -0
- package/dist/meta-D3NoTAjA.js +7 -0
- package/dist/meta-D3NoTAjA.js.map +1 -0
- package/dist/setup-DMckHRnI.js +103 -0
- package/dist/setup-DMckHRnI.js.map +1 -0
- package/dist/shutdown-B9ClCyco.js +775 -0
- package/dist/shutdown-B9ClCyco.js.map +1 -0
- package/dist/tui-config-Crn6TZPg.js +122 -0
- package/dist/tui-config-Crn6TZPg.js.map +1 -0
- package/dist/tui.js +2706 -0
- package/dist/tui.js.map +1 -0
- package/dist/watchdog-V3lgEhMp.js +207 -0
- package/dist/watchdog-V3lgEhMp.js.map +1 -0
- package/native/imsg-native.darwin-arm64.node +0 -0
- package/native/index.d.ts +86 -0
- package/native/index.js +582 -0
- package/package.json +135 -0
- package/skills/imsg-mcp/SKILL.md +168 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,611 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createInterface } from "node:readline";
|
|
3
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
import { c as checkLocalAccess, f as formatAccessReport, q as installShutdownHandlers, r as registerCleanup } from "./shutdown-B9ClCyco.js";
|
|
6
|
+
import { spawn } from "node:child_process";
|
|
7
|
+
import { existsSync } from "node:fs";
|
|
8
|
+
import { join, dirname } from "node:path";
|
|
9
|
+
import { A as APP_VERSION } from "./meta-D3NoTAjA.js";
|
|
10
|
+
function distRoot() {
|
|
11
|
+
return dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
}
|
|
13
|
+
class LocalMcpClient {
|
|
14
|
+
constructor(onStderr) {
|
|
15
|
+
this.onStderr = onStderr;
|
|
16
|
+
const entry = join(distRoot(), "cli.js");
|
|
17
|
+
if (!existsSync(entry)) {
|
|
18
|
+
throw new Error("dist/cli.js not found. Run `pnpm build` first.");
|
|
19
|
+
}
|
|
20
|
+
this.proc = spawn(process.execPath, [entry, "mcp"], {
|
|
21
|
+
cwd: process.cwd(),
|
|
22
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
23
|
+
});
|
|
24
|
+
this.proc.stderr.on("data", (chunk) => {
|
|
25
|
+
this.onStderr?.(chunk.toString("utf8"));
|
|
26
|
+
});
|
|
27
|
+
this.proc.stdout.on("data", (chunk) => {
|
|
28
|
+
this.outBuf += chunk.toString("utf8");
|
|
29
|
+
const lines = this.outBuf.split("\n");
|
|
30
|
+
this.outBuf = lines.pop() ?? "";
|
|
31
|
+
for (const line of lines) {
|
|
32
|
+
if (!line.trim()) continue;
|
|
33
|
+
try {
|
|
34
|
+
const message = JSON.parse(line);
|
|
35
|
+
if (message.id != null && this.pending.has(message.id)) {
|
|
36
|
+
const pending = this.pending.get(message.id);
|
|
37
|
+
this.pending.delete(message.id);
|
|
38
|
+
pending?.resolve(message);
|
|
39
|
+
}
|
|
40
|
+
} catch {
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
this.proc.on("exit", (code) => {
|
|
45
|
+
if (this.closed) return;
|
|
46
|
+
for (const [, p] of this.pending) {
|
|
47
|
+
p.reject(new Error(`MCP child exited unexpectedly with code ${code}`));
|
|
48
|
+
}
|
|
49
|
+
this.pending.clear();
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
proc;
|
|
53
|
+
requestId = 0;
|
|
54
|
+
pending = /* @__PURE__ */ new Map();
|
|
55
|
+
outBuf = "";
|
|
56
|
+
closed = false;
|
|
57
|
+
async start() {
|
|
58
|
+
const response = await this.call("initialize", {
|
|
59
|
+
protocolVersion: "2024-11-05",
|
|
60
|
+
capabilities: {},
|
|
61
|
+
clientInfo: { name: "imsg", version: "1.0.0" }
|
|
62
|
+
});
|
|
63
|
+
if (response.result) {
|
|
64
|
+
this.proc.stdin.write(
|
|
65
|
+
`${JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized", params: {} })}
|
|
66
|
+
`
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
async callTool(name, args, timeoutMs = 3e4) {
|
|
71
|
+
const response = await this.call("tools/call", { name, arguments: args }, timeoutMs);
|
|
72
|
+
if (response.error) {
|
|
73
|
+
throw new Error(response.error.message);
|
|
74
|
+
}
|
|
75
|
+
return response.result ?? {};
|
|
76
|
+
}
|
|
77
|
+
async listTools(timeoutMs = 15e3) {
|
|
78
|
+
const response = await this.call("tools/list", {}, timeoutMs);
|
|
79
|
+
if (response.error) {
|
|
80
|
+
throw new Error(response.error.message);
|
|
81
|
+
}
|
|
82
|
+
return response.result?.tools ?? [];
|
|
83
|
+
}
|
|
84
|
+
close() {
|
|
85
|
+
if (this.closed) return;
|
|
86
|
+
this.closed = true;
|
|
87
|
+
this.proc.stdin.end();
|
|
88
|
+
const killTimer = setTimeout(() => {
|
|
89
|
+
if (!this.proc.killed) {
|
|
90
|
+
this.proc.kill("SIGKILL");
|
|
91
|
+
}
|
|
92
|
+
}, 2e3);
|
|
93
|
+
killTimer.unref();
|
|
94
|
+
this.proc.on("exit", () => clearTimeout(killTimer));
|
|
95
|
+
this.proc.kill("SIGTERM");
|
|
96
|
+
for (const [, p] of this.pending) {
|
|
97
|
+
p.reject(new Error("MCP client closed"));
|
|
98
|
+
}
|
|
99
|
+
this.pending.clear();
|
|
100
|
+
}
|
|
101
|
+
call(method, params, timeoutMs = 15e3) {
|
|
102
|
+
return new Promise((resolve, reject) => {
|
|
103
|
+
if (this.closed) {
|
|
104
|
+
reject(new Error("MCP client is closed"));
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const id = ++this.requestId;
|
|
108
|
+
this.pending.set(id, { resolve, reject });
|
|
109
|
+
this.proc.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", id, method, params })}
|
|
110
|
+
`);
|
|
111
|
+
const timer = setTimeout(() => {
|
|
112
|
+
if (!this.pending.has(id)) return;
|
|
113
|
+
this.pending.delete(id);
|
|
114
|
+
reject(new Error(`Request timeout after ${timeoutMs}ms.`));
|
|
115
|
+
}, timeoutMs);
|
|
116
|
+
timer.unref();
|
|
117
|
+
const original = this.pending.get(id);
|
|
118
|
+
if (!original) return;
|
|
119
|
+
this.pending.set(id, {
|
|
120
|
+
resolve: (value) => {
|
|
121
|
+
clearTimeout(timer);
|
|
122
|
+
original.resolve(value);
|
|
123
|
+
},
|
|
124
|
+
reject: (error) => {
|
|
125
|
+
clearTimeout(timer);
|
|
126
|
+
original.reject(error);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
const color = {
|
|
133
|
+
dim: (v) => `\x1B[2m${v}\x1B[0m`,
|
|
134
|
+
cyan: (v) => `\x1B[36m${v}\x1B[0m`,
|
|
135
|
+
green: (v) => `\x1B[32m${v}\x1B[0m`,
|
|
136
|
+
yellow: (v) => `\x1B[33m${v}\x1B[0m`,
|
|
137
|
+
red: (v) => `\x1B[31m${v}\x1B[0m`
|
|
138
|
+
};
|
|
139
|
+
function log(message, style = "dim") {
|
|
140
|
+
const fn = style === "ok" ? color.green : style === "warn" ? color.yellow : style === "err" ? color.red : color.dim;
|
|
141
|
+
console.log(fn(message));
|
|
142
|
+
}
|
|
143
|
+
function looksLikeThreadSlug(value) {
|
|
144
|
+
return Boolean(value?.includes("~"));
|
|
145
|
+
}
|
|
146
|
+
async function withClient(run) {
|
|
147
|
+
const client = new LocalMcpClient((line) => process.stderr.write(color.dim(`[server] ${line}`)));
|
|
148
|
+
try {
|
|
149
|
+
await client.start();
|
|
150
|
+
return await run(client);
|
|
151
|
+
} finally {
|
|
152
|
+
client.close();
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
async function printToolResult(client, name, args, timeoutMs) {
|
|
156
|
+
const result = await client.callTool(name, args, timeoutMs);
|
|
157
|
+
const text = result.content?.[0]?.text ?? JSON.stringify(result, null, 2);
|
|
158
|
+
if (result.isError) throw new Error(text);
|
|
159
|
+
console.log(text);
|
|
160
|
+
}
|
|
161
|
+
function parseConsoleInput(line) {
|
|
162
|
+
const parts = [];
|
|
163
|
+
let current = "";
|
|
164
|
+
let quote = null;
|
|
165
|
+
for (const char of line) {
|
|
166
|
+
if ((char === '"' || char === "'") && quote == null) {
|
|
167
|
+
quote = char;
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
if (char === quote) {
|
|
171
|
+
quote = null;
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
if (char === " " && quote == null) {
|
|
175
|
+
if (current) {
|
|
176
|
+
parts.push(current);
|
|
177
|
+
current = "";
|
|
178
|
+
}
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
current += char;
|
|
182
|
+
}
|
|
183
|
+
if (current) parts.push(current);
|
|
184
|
+
return { cmd: parts[0]?.toLowerCase() ?? "", args: parts.slice(1) };
|
|
185
|
+
}
|
|
186
|
+
async function runConsoleCommand(cmd, args, client) {
|
|
187
|
+
switch (cmd) {
|
|
188
|
+
case "conversations":
|
|
189
|
+
case "list":
|
|
190
|
+
await printToolResult(client, "list_conversations", { limit: Number(args[0] ?? 20) });
|
|
191
|
+
return;
|
|
192
|
+
case "messages":
|
|
193
|
+
case "msg": {
|
|
194
|
+
const first = args[0];
|
|
195
|
+
const firstIsLimit = first != null && /^\d+$/.test(first);
|
|
196
|
+
const chatIdentifier = firstIsLimit ? void 0 : first;
|
|
197
|
+
const limit = Number(firstIsLimit ? first : args[1] ?? 20);
|
|
198
|
+
await printToolResult(client, "get_messages", {
|
|
199
|
+
...chatIdentifier ? looksLikeThreadSlug(chatIdentifier) ? { threadSlug: chatIdentifier } : { chatIdentifier } : {},
|
|
200
|
+
limit
|
|
201
|
+
});
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
case "unread":
|
|
205
|
+
await printToolResult(
|
|
206
|
+
client,
|
|
207
|
+
"get_unread_messages",
|
|
208
|
+
args[0] ? { limit: Number(args[0]) } : {}
|
|
209
|
+
);
|
|
210
|
+
return;
|
|
211
|
+
case "search":
|
|
212
|
+
if (!args[0]) throw new Error("Usage: search <query> [limit]");
|
|
213
|
+
await printToolResult(client, "search_messages", {
|
|
214
|
+
query: args[0],
|
|
215
|
+
limit: Number(args[1] ?? 20)
|
|
216
|
+
});
|
|
217
|
+
return;
|
|
218
|
+
case "wait":
|
|
219
|
+
if (!args[0]) throw new Error("Usage: wait <chat> [timeoutSeconds]");
|
|
220
|
+
await printToolResult(
|
|
221
|
+
client,
|
|
222
|
+
"wait_for_reply",
|
|
223
|
+
{
|
|
224
|
+
...looksLikeThreadSlug(args[0]) ? { threadSlug: args[0] } : { chatIdentifier: args[0] },
|
|
225
|
+
timeoutSeconds: Number(args[1] ?? 60),
|
|
226
|
+
pollIntervalSeconds: 5
|
|
227
|
+
},
|
|
228
|
+
7e4
|
|
229
|
+
);
|
|
230
|
+
return;
|
|
231
|
+
case "send":
|
|
232
|
+
if (args.length < 2) throw new Error("Usage: send <target> <message>");
|
|
233
|
+
await printToolResult(client, "send_message", {
|
|
234
|
+
...looksLikeThreadSlug(args[0]) ? { threadSlug: args[0] } : { recipient: args[0] },
|
|
235
|
+
message: args.slice(1).join(" ")
|
|
236
|
+
});
|
|
237
|
+
return;
|
|
238
|
+
case "logs":
|
|
239
|
+
await printToolResult(client, "get_logs", args[0] ? { tail: Number(args[0]) } : {});
|
|
240
|
+
return;
|
|
241
|
+
case "last-error":
|
|
242
|
+
case "lasterror":
|
|
243
|
+
await printToolResult(client, "get_last_send_error", {});
|
|
244
|
+
return;
|
|
245
|
+
case "tools":
|
|
246
|
+
for (const tool of await client.listTools()) {
|
|
247
|
+
console.log(`${tool.name}${tool.description ? ` - ${tool.description}` : ""}`);
|
|
248
|
+
}
|
|
249
|
+
return;
|
|
250
|
+
case "raw":
|
|
251
|
+
if (!args[0]) throw new Error("Usage: raw '<json>'");
|
|
252
|
+
{
|
|
253
|
+
const parsed = JSON.parse(args.join(" "));
|
|
254
|
+
if (!parsed.name)
|
|
255
|
+
throw new Error('Expected JSON like {"name":"tool_name","arguments":{...}}.');
|
|
256
|
+
await printToolResult(client, parsed.name, parsed.arguments ?? {});
|
|
257
|
+
}
|
|
258
|
+
return;
|
|
259
|
+
case "tui": {
|
|
260
|
+
const { runTui } = await import("./tui.js");
|
|
261
|
+
await runTui();
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
case "help":
|
|
265
|
+
case "?":
|
|
266
|
+
console.log(CONSOLE_HELP);
|
|
267
|
+
return;
|
|
268
|
+
case "quit":
|
|
269
|
+
case "exit":
|
|
270
|
+
process.exit(0);
|
|
271
|
+
return;
|
|
272
|
+
default:
|
|
273
|
+
throw new Error(`Unknown command: ${cmd}. Type "help" for available commands.`);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
const CONSOLE_HELP = `
|
|
277
|
+
Available commands:
|
|
278
|
+
conversations [n] List recent conversations (default 20)
|
|
279
|
+
messages [chat] [n] Show recent messages
|
|
280
|
+
unread [n] Show unread messages
|
|
281
|
+
search <query> [n] Search messages
|
|
282
|
+
wait <chat> [secs] Wait for a reply (default 60s)
|
|
283
|
+
send <target> <msg> Send a message
|
|
284
|
+
logs [tail] Show server debug logs
|
|
285
|
+
last-error Show last send failure
|
|
286
|
+
tools List available MCP tools
|
|
287
|
+
raw <json> Send raw JSON-RPC to tools/call
|
|
288
|
+
tui Launch the read-only TUI
|
|
289
|
+
help Show this help
|
|
290
|
+
quit Exit
|
|
291
|
+
`.trim();
|
|
292
|
+
async function runInteractiveConsole() {
|
|
293
|
+
log("Starting local MCP server...", "dim");
|
|
294
|
+
const client = new LocalMcpClient((line) => process.stderr.write(color.dim(`[server] ${line}`)));
|
|
295
|
+
await client.start();
|
|
296
|
+
log("Console ready.\n", "ok");
|
|
297
|
+
console.log(CONSOLE_HELP);
|
|
298
|
+
console.log("");
|
|
299
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
300
|
+
const prompt = () => rl.question(color.cyan("imsg> "), async (line) => {
|
|
301
|
+
const trimmed = line.trim();
|
|
302
|
+
if (!trimmed) {
|
|
303
|
+
prompt();
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
const { cmd, args } = parseConsoleInput(trimmed);
|
|
307
|
+
try {
|
|
308
|
+
await runConsoleCommand(cmd, args, client);
|
|
309
|
+
} catch (error) {
|
|
310
|
+
log(error instanceof Error ? error.message : String(error), "err");
|
|
311
|
+
}
|
|
312
|
+
console.log("");
|
|
313
|
+
prompt();
|
|
314
|
+
});
|
|
315
|
+
prompt();
|
|
316
|
+
installShutdownHandlers();
|
|
317
|
+
registerCleanup(() => client.close());
|
|
318
|
+
}
|
|
319
|
+
function normalizeFormat(raw) {
|
|
320
|
+
const v = raw.toLowerCase();
|
|
321
|
+
if (v === "md" || v === "markdown") return "markdown";
|
|
322
|
+
if (v === "csv") return "csv";
|
|
323
|
+
if (v === "json") return "json";
|
|
324
|
+
if (v === "ndjson") return "ndjson";
|
|
325
|
+
throw new Error(`Unknown format: ${raw}. Expected md, csv, json, or ndjson.`);
|
|
326
|
+
}
|
|
327
|
+
function extForFormat(fmt) {
|
|
328
|
+
return fmt === "markdown" ? "md" : fmt;
|
|
329
|
+
}
|
|
330
|
+
function sanitizeForFilename(s) {
|
|
331
|
+
return s.replace(/[^A-Za-z0-9._~-]+/g, "_").replace(/^_+|_+$/g, "") || "chat";
|
|
332
|
+
}
|
|
333
|
+
async function runExportCommand(target, opts) {
|
|
334
|
+
const { existsSync: existsSync2, mkdirSync, copyFileSync, statSync } = await import("node:fs");
|
|
335
|
+
const { homedir } = await import("node:os");
|
|
336
|
+
const { dirname: dirname2, join: join2, isAbsolute, resolve } = await import("node:path");
|
|
337
|
+
const { getContactsDbPaths, getImsgDbPath, getSlugsDbPath } = await import("./shutdown-B9ClCyco.js").then((n) => n.K);
|
|
338
|
+
const { IMessageDB } = await import("./imessage-db-BVDtx0Sn.js").then((n) => n.i);
|
|
339
|
+
const { streamExport } = await import("./exportStream-BaheQ6M4.js");
|
|
340
|
+
const { parseUserDate } = await import("./dateParse-DJXMfq3a.js");
|
|
341
|
+
const format = normalizeFormat(opts.format ?? "md");
|
|
342
|
+
const ext = extForFormat(format);
|
|
343
|
+
const pageSize = Number(opts.pageSize ?? "1000");
|
|
344
|
+
if (!Number.isFinite(pageSize) || pageSize < 100 || pageSize > 5e3) {
|
|
345
|
+
throw new Error("--page-size must be between 100 and 5000.");
|
|
346
|
+
}
|
|
347
|
+
const since = opts.since ? parseUserDate(opts.since) : null;
|
|
348
|
+
if (opts.since && !since) throw new Error(`Could not parse --since: ${opts.since}`);
|
|
349
|
+
const until = opts.until ? parseUserDate(opts.until) : null;
|
|
350
|
+
if (opts.until && !until) throw new Error(`Could not parse --until: ${opts.until}`);
|
|
351
|
+
const db = new IMessageDB(getImsgDbPath(), getContactsDbPaths(), getSlugsDbPath());
|
|
352
|
+
try {
|
|
353
|
+
let chatIdentifier = target;
|
|
354
|
+
let displayHandle = target;
|
|
355
|
+
if (looksLikeThreadSlug(target)) {
|
|
356
|
+
const rec = db.getSlugRecord(target);
|
|
357
|
+
if (!rec) throw new Error(`Unknown thread slug: ${target}`);
|
|
358
|
+
chatIdentifier = rec.chatIdentifier;
|
|
359
|
+
displayHandle = rec.slug;
|
|
360
|
+
}
|
|
361
|
+
let outputPath;
|
|
362
|
+
if (opts.output) {
|
|
363
|
+
outputPath = isAbsolute(opts.output) ? opts.output : resolve(opts.output);
|
|
364
|
+
} else {
|
|
365
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
366
|
+
outputPath = join2(
|
|
367
|
+
homedir(),
|
|
368
|
+
`imsg-export-${sanitizeForFilename(displayHandle)}-${today}.${ext}`
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
const parent = dirname2(outputPath);
|
|
372
|
+
if (!existsSync2(parent)) mkdirSync(parent, { recursive: true });
|
|
373
|
+
log(`Exporting ${displayHandle} → ${outputPath} (${format})`, "dim");
|
|
374
|
+
const result = await streamExport({
|
|
375
|
+
db,
|
|
376
|
+
chatIdentifier,
|
|
377
|
+
format,
|
|
378
|
+
outputPath,
|
|
379
|
+
since,
|
|
380
|
+
until,
|
|
381
|
+
pageSize
|
|
382
|
+
});
|
|
383
|
+
let attachmentSummary = "";
|
|
384
|
+
if (opts.includeAttachments) {
|
|
385
|
+
const dir = opts.attachmentsDir ? isAbsolute(opts.attachmentsDir) ? opts.attachmentsDir : resolve(opts.attachmentsDir) : `${outputPath}.attachments`;
|
|
386
|
+
if (!existsSync2(dir)) mkdirSync(dir, { recursive: true });
|
|
387
|
+
const filters = {
|
|
388
|
+
chatIdentifier,
|
|
389
|
+
limit: 0
|
|
390
|
+
};
|
|
391
|
+
if (since) filters.sinceMs = since.getTime();
|
|
392
|
+
if (until) filters.untilMs = until.getTime();
|
|
393
|
+
const attachments = db.searchAttachments(filters);
|
|
394
|
+
let copied = 0;
|
|
395
|
+
let totalBytes = 0;
|
|
396
|
+
const seen = /* @__PURE__ */ new Set();
|
|
397
|
+
for (const a of attachments) {
|
|
398
|
+
if (!a.filename) continue;
|
|
399
|
+
const src = a.filename.replace(/^~/, homedir());
|
|
400
|
+
if (!existsSync2(src)) continue;
|
|
401
|
+
const baseName = a.transferName || src.split("/").pop() || `att-${a.rowId}`;
|
|
402
|
+
let destName = `${a.rowId}-${sanitizeForFilename(baseName)}`;
|
|
403
|
+
if (seen.has(destName)) destName = `${a.rowId}-${Date.now()}-${baseName}`;
|
|
404
|
+
seen.add(destName);
|
|
405
|
+
const dest = join2(dir, destName);
|
|
406
|
+
try {
|
|
407
|
+
copyFileSync(src, dest);
|
|
408
|
+
copied++;
|
|
409
|
+
totalBytes += statSync(dest).size;
|
|
410
|
+
} catch (err) {
|
|
411
|
+
log(
|
|
412
|
+
` warn: copy failed for ${src}: ${err instanceof Error ? err.message : String(err)}`,
|
|
413
|
+
"warn"
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
attachmentSummary = `
|
|
418
|
+
Attachments: ${copied} file(s), ${(totalBytes / 1024).toFixed(1)} KB → ${dir}`;
|
|
419
|
+
}
|
|
420
|
+
log(
|
|
421
|
+
[
|
|
422
|
+
"",
|
|
423
|
+
`✓ Exported ${result.count} message(s) to ${result.savedTo}`,
|
|
424
|
+
` Format: ${format}`,
|
|
425
|
+
` Range: ${result.oldest?.toISOString() ?? "(none)"} → ${result.newest?.toISOString() ?? "(none)"}`,
|
|
426
|
+
` Size: ${(result.sizeBytes / 1024).toFixed(1)} KB${attachmentSummary}`
|
|
427
|
+
].join("\n"),
|
|
428
|
+
"ok"
|
|
429
|
+
);
|
|
430
|
+
} finally {
|
|
431
|
+
await db.close();
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
const program = new Command().name("imsg").version(APP_VERSION, "-v, --version").description("CLI for the imsg-mcp iMessage MCP server").addHelpText(
|
|
435
|
+
"after",
|
|
436
|
+
`
|
|
437
|
+
Notes:
|
|
438
|
+
Use "imsg doctor" on a new machine.
|
|
439
|
+
Run the MCP stdio server with "imsg mcp".
|
|
440
|
+
Launch the read-only TUI with "imsg tui".`
|
|
441
|
+
);
|
|
442
|
+
program.command("mcp").description("Run the MCP stdio server").action(async () => {
|
|
443
|
+
const { runMcpServer } = await import("./index.js");
|
|
444
|
+
await runMcpServer();
|
|
445
|
+
});
|
|
446
|
+
program.command("cli").alias("console").description("Launch interactive console").action(runInteractiveConsole);
|
|
447
|
+
program.command("doctor").description("Check local permissions and database access").action(async () => {
|
|
448
|
+
const report = await checkLocalAccess();
|
|
449
|
+
console.log(formatAccessReport(report));
|
|
450
|
+
process.exitCode = report.ok ? 0 : 1;
|
|
451
|
+
});
|
|
452
|
+
program.command("conversations").alias("list").description("List recent conversations").argument("[limit]", "Number of conversations", "20").action(async (limit) => {
|
|
453
|
+
await withClient((c) => printToolResult(c, "list_conversations", { limit: Number(limit) }));
|
|
454
|
+
});
|
|
455
|
+
program.command("messages").alias("msg").description("Show recent messages from a conversation").argument("[chat]", "Phone number, email, or thread slug").argument("[limit]", "Number of messages", "20").action(async (chat, limit) => {
|
|
456
|
+
const firstIsLimit = chat != null && /^\d+$/.test(chat);
|
|
457
|
+
const chatIdentifier = firstIsLimit ? void 0 : chat;
|
|
458
|
+
const finalLimit = Number(firstIsLimit ? chat : limit);
|
|
459
|
+
await withClient(
|
|
460
|
+
(c) => printToolResult(c, "get_messages", {
|
|
461
|
+
...chatIdentifier ? looksLikeThreadSlug(chatIdentifier) ? { threadSlug: chatIdentifier } : { chatIdentifier } : {},
|
|
462
|
+
limit: finalLimit
|
|
463
|
+
})
|
|
464
|
+
);
|
|
465
|
+
});
|
|
466
|
+
program.command("unread").description("Show unread messages across all conversations").argument("[limit]", "Number of messages", "100").action(async (limit) => {
|
|
467
|
+
await withClient((c) => printToolResult(c, "get_unread_messages", { limit: Number(limit) }));
|
|
468
|
+
});
|
|
469
|
+
program.command("search").description("Search messages by text content").argument("<query>", "Search query").argument("[limit]", "Number of results", "20").action(async (query, limit) => {
|
|
470
|
+
await withClient((c) => printToolResult(c, "search_messages", { query, limit: Number(limit) }));
|
|
471
|
+
});
|
|
472
|
+
program.command("wait").description("Wait for a reply in a conversation").argument("<chat>", "Phone number, email, or thread slug").argument("[timeout]", "Timeout in seconds", "60").action(async (chat, timeout) => {
|
|
473
|
+
await withClient(
|
|
474
|
+
(c) => printToolResult(
|
|
475
|
+
c,
|
|
476
|
+
"wait_for_reply",
|
|
477
|
+
{
|
|
478
|
+
...looksLikeThreadSlug(chat) ? { threadSlug: chat } : { chatIdentifier: chat },
|
|
479
|
+
timeoutSeconds: Number(timeout),
|
|
480
|
+
pollIntervalSeconds: 5
|
|
481
|
+
},
|
|
482
|
+
7e4
|
|
483
|
+
)
|
|
484
|
+
);
|
|
485
|
+
});
|
|
486
|
+
program.command("send").description("Send a message via Messages.app").argument("<target>", "Phone number, email, or thread slug").argument("<message...>", "Message text").action(async (target, messageParts) => {
|
|
487
|
+
await withClient(
|
|
488
|
+
(c) => printToolResult(c, "send_message", {
|
|
489
|
+
...looksLikeThreadSlug(target) ? { threadSlug: target } : { recipient: target },
|
|
490
|
+
message: messageParts.join(" ")
|
|
491
|
+
})
|
|
492
|
+
);
|
|
493
|
+
});
|
|
494
|
+
program.command("logs").description("Show server debug logs").argument("[tail]", "Show only last N lines").action(async (tail) => {
|
|
495
|
+
await withClient((c) => printToolResult(c, "get_logs", tail ? { tail: Number(tail) } : {}));
|
|
496
|
+
});
|
|
497
|
+
program.command("last-error").description("Show last send_message failure details").action(async () => {
|
|
498
|
+
await withClient((c) => printToolResult(c, "get_last_send_error", {}));
|
|
499
|
+
});
|
|
500
|
+
program.command("tools").description("List available MCP tools").action(async () => {
|
|
501
|
+
await withClient(async (c) => {
|
|
502
|
+
for (const tool of await c.listTools()) {
|
|
503
|
+
console.log(`${tool.name}${tool.description ? ` - ${tool.description}` : ""}`);
|
|
504
|
+
}
|
|
505
|
+
});
|
|
506
|
+
});
|
|
507
|
+
program.command("raw").description("Send raw JSON-RPC params to tools/call").argument("<json>", 'JSON like {"name":"tool_name","arguments":{...}}').action(async (json) => {
|
|
508
|
+
const parsed = JSON.parse(json);
|
|
509
|
+
if (!parsed.name) throw new Error('Expected JSON like {"name":"tool_name","arguments":{...}}.');
|
|
510
|
+
await withClient((c) => printToolResult(c, parsed.name, parsed.arguments ?? {}));
|
|
511
|
+
});
|
|
512
|
+
program.command("tui").description("Launch the read-only terminal UI").option("--theme <theme>", 'TUI theme: "safe" or "powerline"').option("--accent <hex>", "TUI accent color as #RRGGBB").action(async () => {
|
|
513
|
+
const { runTui } = await import("./tui.js");
|
|
514
|
+
await runTui();
|
|
515
|
+
});
|
|
516
|
+
program.command("export <target>").description("Export a conversation to a file (md/csv/json/ndjson)").option("-f, --format <fmt>", "Output format: md (default), csv, json, ndjson", "md").option("--since <date>", "Earliest date (ISO or relative, e.g. '3 months ago')").option("--until <date>", "Latest date (ISO or relative)").option("-o, --output <path>", "Output path (default: ~/imsg-export-<target>-<YYYY-MM-DD>.<ext>)").option("--include-attachments", "Copy attachments next to the export").option("--attachments-dir <path>", "Where to copy attachments (default: <output>.attachments/)").option("--page-size <n>", "Messages per DB page (100-5000)", "1000").action(runExportCommand);
|
|
517
|
+
program.command("setup").description("Autodetect DB paths and emit an MCP host config snippet").option("-w, --write <host>", 'Write into a host config: "claude" or "cursor"').option("-r, --runtime <runtime>", 'Runtime command: "npx" (default), "bunx", or "global"').option("--print-only", "Just print the snippet (default behaviour)").action(async (opts) => {
|
|
518
|
+
const { probeMachine, buildMcpSnippet, writeHostConfig } = await import("./setup-DMckHRnI.js");
|
|
519
|
+
const report = probeMachine();
|
|
520
|
+
if (!report.imsgDb.readable) {
|
|
521
|
+
log(`✗ Messages DB is not readable: ${report.imsgDb.path}`, "err");
|
|
522
|
+
log(` ${report.imsgDb.error ?? ""}`, "err");
|
|
523
|
+
log(
|
|
524
|
+
" Grant Full Disk Access to the running app: System Settings → Privacy & Security → Full Disk Access",
|
|
525
|
+
"warn"
|
|
526
|
+
);
|
|
527
|
+
process.exitCode = 1;
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
log(`✓ Messages DB readable: ${report.imsgDb.path}`, "ok");
|
|
531
|
+
log(
|
|
532
|
+
`✓ Address Book: ${report.contactsDbs.length} source(s), ${report.contactsDbs.filter((p) => p.readable).length} readable`,
|
|
533
|
+
"ok"
|
|
534
|
+
);
|
|
535
|
+
log(
|
|
536
|
+
` slugs.db: ${report.slugsDb.path} ${report.slugsDb.exists ? "(exists)" : "(will be created on first run)"}`
|
|
537
|
+
);
|
|
538
|
+
const runtime = opts.runtime === "bunx" || opts.runtime === "global" ? opts.runtime : "npx";
|
|
539
|
+
const snippet = buildMcpSnippet(report, { runtime });
|
|
540
|
+
if (opts.write) {
|
|
541
|
+
if (opts.write !== "claude" && opts.write !== "cursor") {
|
|
542
|
+
log(`✗ unknown host: ${opts.write} (expected "claude" or "cursor")`, "err");
|
|
543
|
+
process.exitCode = 1;
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
const result = writeHostConfig(opts.write, report, { runtime });
|
|
547
|
+
log(
|
|
548
|
+
`✓ wrote ${opts.write} config to ${result.path}${result.replaced ? " (replaced existing imessage entry)" : ""}`,
|
|
549
|
+
"ok"
|
|
550
|
+
);
|
|
551
|
+
log(` backup of any prior file at ${result.path}.bak`);
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
log("--- snippet ---", "dim");
|
|
555
|
+
process.stdout.write(snippet);
|
|
556
|
+
});
|
|
557
|
+
const configCmd = program.command("config").description("Manage TUI settings (theme, accent color)");
|
|
558
|
+
configCmd.command("show").description("Print resolved TUI settings and where each value came from").action(async () => {
|
|
559
|
+
const { resolveTuiConfig, defaultTuiConfigPath } = await import("./tui-config-Crn6TZPg.js");
|
|
560
|
+
const cfg = resolveTuiConfig();
|
|
561
|
+
log(
|
|
562
|
+
`config file : ${cfg.configPath ?? `(none — defaults; would write to ${defaultTuiConfigPath()})`}`
|
|
563
|
+
);
|
|
564
|
+
log(`theme : ${cfg.theme} (from ${cfg.origin.theme})`);
|
|
565
|
+
log(`accentColor : ${cfg.accentColor} (from ${cfg.origin.accentColor})`);
|
|
566
|
+
if (cfg.theme === "powerline") {
|
|
567
|
+
log(" — powerline theme requires a Nerd Font (https://www.nerdfonts.com)", "warn");
|
|
568
|
+
}
|
|
569
|
+
for (const w of cfg.warnings) log(w, "warn");
|
|
570
|
+
});
|
|
571
|
+
configCmd.command("edit").description("Open the TUI config file in $EDITOR (creates it if missing)").action(async () => {
|
|
572
|
+
const { defaultTuiConfigPath, findTuiConfigPath, writeTuiConfig, DEFAULT_TUI_CONFIG } = await import("./tui-config-Crn6TZPg.js");
|
|
573
|
+
const path = findTuiConfigPath() ?? defaultTuiConfigPath();
|
|
574
|
+
const { existsSync: existsSync2 } = await import("node:fs");
|
|
575
|
+
if (!existsSync2(path)) {
|
|
576
|
+
writeTuiConfig(DEFAULT_TUI_CONFIG, path);
|
|
577
|
+
log(`created ${path}`, "ok");
|
|
578
|
+
}
|
|
579
|
+
const editor = process.env.EDITOR ?? "vi";
|
|
580
|
+
const { spawn: spawn2 } = await import("node:child_process");
|
|
581
|
+
const child = spawn2(editor, [path], { stdio: "inherit" });
|
|
582
|
+
await new Promise((resolve, reject) => {
|
|
583
|
+
child.on(
|
|
584
|
+
"exit",
|
|
585
|
+
(code) => code === 0 ? resolve() : reject(new Error(`editor exited ${code}`))
|
|
586
|
+
);
|
|
587
|
+
child.on("error", reject);
|
|
588
|
+
});
|
|
589
|
+
});
|
|
590
|
+
program.action(() => {
|
|
591
|
+
program.outputHelp();
|
|
592
|
+
});
|
|
593
|
+
const invokedAsScript = (() => {
|
|
594
|
+
try {
|
|
595
|
+
const entry = process.argv[1];
|
|
596
|
+
if (!entry) return false;
|
|
597
|
+
return import.meta.url === pathToFileURL(entry).href;
|
|
598
|
+
} catch {
|
|
599
|
+
return false;
|
|
600
|
+
}
|
|
601
|
+
})();
|
|
602
|
+
if (invokedAsScript) {
|
|
603
|
+
program.parseAsync(process.argv).catch((error) => {
|
|
604
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
605
|
+
process.exit(1);
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
export {
|
|
609
|
+
runExportCommand
|
|
610
|
+
};
|
|
611
|
+
//# sourceMappingURL=cli.js.map
|