imsg-mcp 1.4.0 → 1.6.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 CHANGED
@@ -3,6 +3,39 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and follows [Semantic Versioning](https://semver.org/).
5
5
 
6
+ # [1.6.0](https://github.com/george43g/imsg-mcp/compare/v1.5.0...v1.6.0) (2026-07-19)
7
+
8
+
9
+ ### Bug Fixes
10
+
11
+ * **cli:** friendlier windowDays + humans usage; add `resolve` console verb ([4a5f389](https://github.com/george43g/imsg-mcp/commit/4a5f3892d6ca9d0683c49c44b9052190552bc7f9))
12
+ * **db:** detect unsent messages via content-absence, not the lying date columns ([8a2c5e2](https://github.com/george43g/imsg-mcp/commit/8a2c5e2544f5393f7380a1fb4126e782e81e111c))
13
+ * **tui:** filter-commit (Enter) loads the matched thread, not just the cursor ([3666d4a](https://github.com/george43g/imsg-mcp/commit/3666d4a1a6febbdae935fb1406fcf4a39cd9f1ab))
14
+
15
+
16
+ ### Features
17
+
18
+ * **mcp:** resolve_conversation — free-form name → ranked threads in one call ([68485ab](https://github.com/george43g/imsg-mcp/commit/68485ab470b7b7a555719769169107d17c94a1cd))
19
+
20
+ # [1.5.0](https://github.com/george43g/imsg-mcp/compare/v1.4.0...v1.5.0) (2026-07-19)
21
+
22
+
23
+ ### Bug Fixes
24
+
25
+ * **analytics:** bound getMessagesInWindow so opening a pane can't OOM-kill ([708ba7a](https://github.com/george43g/imsg-mcp/commit/708ba7ab223d8c8a6f2d37b51979490630b5f24a))
26
+ * **analytics:** make the relationship leaderboard actually rank ([60159ee](https://github.com/george43g/imsg-mcp/commit/60159ee52776077b8ee5ef6019ec464b3e92c87d))
27
+ * **humans:** getChatStats double-counted messages shared across legs ([e1a17cb](https://github.com/george43g/imsg-mcp/commit/e1a17cbe3a2570249122a4c7a86c69dbabf82ec8))
28
+ * **media:** correct yap/hear transcriber invocations ([f272e15](https://github.com/george43g/imsg-mcp/commit/f272e15aa34d2fc9db5ecbf344025d6aeb3a57a1))
29
+ * **snippets:** unsent last message no longer leaks "DWm" into the preview ([185d073](https://github.com/george43g/imsg-mcp/commit/185d0738d43a5386682e5dc0fbc25e706c1edcea))
30
+ * **test:** make CLI e2e cold-safe (no dependency on a warm slugs.db) ([a00b1a8](https://github.com/george43g/imsg-mcp/commit/a00b1a86f854e7ba8e96ddc9230460f8bab929d7))
31
+ * **tui:** compose-new modal no longer quits the TUI on a "q" recipient ([78589b1](https://github.com/george43g/imsg-mcp/commit/78589b10184ccedc31796a8e840b91245f1a8149))
32
+ * **tui:** stop the near-top loader re-firing when it can't page back further ([1639698](https://github.com/george43g/imsg-mcp/commit/1639698b88e19aa090a64f396810fc400e21542d))
33
+
34
+
35
+ ### Features
36
+
37
+ * **analytics:** expose all 7 analytics via CLI + console with json/yaml ([ac619e9](https://github.com/george43g/imsg-mcp/commit/ac619e904ef7b1aaea6becaa00b896f932d737fc))
38
+
6
39
  # [1.4.0](https://github.com/george43g/imsg-mcp/compare/v1.3.2...v1.4.0) (2026-07-13)
7
40
 
8
41
 
package/README.md CHANGED
@@ -58,6 +58,14 @@ imsg export weekend-crew~imsg~d4e5 \
58
58
  --output ~/weekend-crew.md
59
59
  ```
60
60
 
61
+ Relationship analytics from the CLI (all 7 types; add `--json` / `--yaml` to script them):
62
+
63
+ ```bash
64
+ imsg analytics relationship_leaderboard # top relationships by volume × reciprocity × recency
65
+ imsg analytics daily_heatmap # ASCII 7×24 activity grid
66
+ imsg analytics messaging_streaks --json # machine-readable for pipelines
67
+ ```
68
+
61
69
  Full walkthrough with screenshots: [**docs/WORKFLOWS.md**](docs/WORKFLOWS.md).
62
70
 
63
71
  ---
@@ -65,7 +73,7 @@ Full walkthrough with screenshots: [**docs/WORKFLOWS.md**](docs/WORKFLOWS.md).
65
73
  ## MCP tools
66
74
 
67
75
  <details>
68
- <summary><b>16 tools shipped (click to expand)</b></summary>
76
+ <summary><b>17 tools shipped (click to expand)</b></summary>
69
77
 
70
78
  | Tool | Purpose |
71
79
  |------|---------|
@@ -82,12 +90,13 @@ Full walkthrough with screenshots: [**docs/WORKFLOWS.md**](docs/WORKFLOWS.md).
82
90
  | `chat_analytics` | Pre-aggregated stats (heatmaps, leaderboards, etc). |
83
91
  | `list_contacts` / `search_contacts` | Browse/search contacts (name, phone, email). |
84
92
  | `get_contact` | One contact with all handles **+ the thread slug per handle**. |
93
+ | `resolve_conversation` | Free-form name/phrase → ranked threads (contacts + thread names + message content) in one call. |
85
94
  | `resolve_handle` | Phone/email → contact name. |
86
95
  | `init_human` | Scaffold a [humans/v1 relationship file](skills/humans/SKILL.md) for a contact or your top N relationships. |
87
96
 
88
97
  </details>
89
98
 
90
- Contacts are also on the CLI: `imsg contacts search <query>`, `imsg contacts show <handle-or-id>` (handles → thread slugs), `imsg contacts resolve <handle>`, `imsg contacts list`.
99
+ Contacts are also on the CLI: `imsg contacts search <query>`, `imsg contacts show <handle-or-id>` (handles → thread slugs), `imsg contacts resolve <handle>`, `imsg contacts list`. Turn a name into a thread in one shot with `imsg resolve "selena"` (add `--json`/`--yaml` to script it).
91
100
 
92
101
  Full reference (CLI subcommands + MCP tools + every flag): [**docs/TOOLS.md**](docs/TOOLS.md).
93
102
 
package/dist/cli.js CHANGED
@@ -3,10 +3,10 @@ import { existsSync, realpathSync } from "node:fs";
3
3
  import { createInterface } from "node:readline";
4
4
  import { fileURLToPath, pathToFileURL } from "node:url";
5
5
  import { Command } from "commander";
6
- import { c as checkLocalAccess, f as formatAccessReport, t as installShutdownHandlers, u as registerCleanup, G as looksLikeThreadSlug } from "./shutdown-CqGQKFmr.js";
6
+ import { c as checkLocalAccess, f as formatAccessReport, I as IMPLEMENTED_TYPES, J as ANALYTIC_INFO, v as installShutdownHandlers, w as registerCleanup, K as looksLikeThreadSlug } from "./shutdown-Vh0gjb6m.js";
7
+ import { A as APP_VERSION, t as toYaml } from "./meta-CkA7wU7_.js";
7
8
  import { spawn } from "node:child_process";
8
9
  import { join, dirname } from "node:path";
9
- import { A as APP_VERSION } from "./meta-CGz9n7A2.js";
10
10
  function distRoot() {
11
11
  return dirname(fileURLToPath(import.meta.url));
12
12
  }
@@ -149,12 +149,30 @@ async function withClient(run) {
149
149
  client.close();
150
150
  }
151
151
  }
152
- async function printToolResult(client, name, args, timeoutMs) {
152
+ async function printToolResult(client, name, args, timeoutMs, format) {
153
153
  const result = await client.callTool(name, args, timeoutMs);
154
154
  const text = result.content?.[0]?.text ?? JSON.stringify(result, null, 2);
155
155
  if (result.isError) throw new Error(text);
156
+ if (format === "json" || format === "yaml") {
157
+ const payload = result.structuredContent ?? result;
158
+ console.log(format === "json" ? JSON.stringify(payload, null, 2) : toYaml(payload));
159
+ return;
160
+ }
156
161
  console.log(text);
157
162
  }
163
+ function pickFormat(opts) {
164
+ if (opts.json) return "json";
165
+ if (opts.yaml) return "yaml";
166
+ return "pretty";
167
+ }
168
+ function parseWindowDays(value, fallback) {
169
+ if (value === void 0 || value === "") return fallback;
170
+ const n = Number(value);
171
+ if (!Number.isFinite(n) || n <= 0) {
172
+ throw new Error(`windowDays must be a positive number of days, got "${value}".`);
173
+ }
174
+ return n;
175
+ }
158
176
  function parseConsoleInput(line) {
159
177
  const parts = [];
160
178
  let current = "";
@@ -212,6 +230,13 @@ async function runConsoleCommand(cmd, args, client) {
212
230
  limit: Number(args[1] ?? 20)
213
231
  });
214
232
  return;
233
+ case "resolve":
234
+ if (!args[0]) throw new Error("Usage: resolve <name-or-phrase> [limit]");
235
+ await printToolResult(client, "resolve_conversation", {
236
+ query: args[0],
237
+ limit: Number(args[1] ?? 10)
238
+ });
239
+ return;
215
240
  case "wait":
216
241
  if (!args[0]) throw new Error("Usage: wait <chat> [timeoutSeconds]");
217
242
  await printToolResult(
@@ -271,13 +296,32 @@ async function runConsoleCommand(cmd, args, client) {
271
296
  } else if (verb === "top") {
272
297
  await printToolResult(client, "chat_analytics", {
273
298
  type: "relationship_leaderboard",
274
- windowDays: Number(args[1] ?? 365)
299
+ windowDays: parseWindowDays(args[1], 1825)
275
300
  });
276
301
  } else {
277
- throw new Error(`Unknown humans verb: ${verb}. Use init|top.`);
302
+ const prefix = verb && verb !== "help" ? `Unknown humans verb: ${verb}.
303
+ ` : "";
304
+ throw new Error(
305
+ `${prefix}Usage:
306
+ humans init <contact|slug>
307
+ humans init top [n]
308
+ humans top [days]`
309
+ );
278
310
  }
279
311
  return;
280
312
  }
313
+ case "analytics":
314
+ case "stats": {
315
+ const type = args[0];
316
+ if (!type || !IMPLEMENTED_TYPES.includes(type)) {
317
+ throw new Error(`Usage: analytics <${IMPLEMENTED_TYPES.join("|")}> [windowDays]`);
318
+ }
319
+ const fmtArg = args[args.length - 1];
320
+ const format = fmtArg === "json" ? "json" : fmtArg === "yaml" ? "yaml" : "pretty";
321
+ const windowDays = args[1] && /^\d+$/.test(args[1]) ? Number(args[1]) : ANALYTIC_INFO[type].defaultWindowDays;
322
+ await printToolResult(client, "chat_analytics", { type, windowDays }, void 0, format);
323
+ return;
324
+ }
281
325
  case "logs":
282
326
  await printToolResult(client, "get_logs", args[0] ? { tail: Number(args[0]) } : {});
283
327
  return;
@@ -322,10 +366,13 @@ Available commands:
322
366
  messages [chat] [n] Show recent messages
323
367
  unread [n] Show unread messages
324
368
  search <query> [n] Search messages
369
+ resolve <name> [n] Resolve a name/phrase to ranked conversations
325
370
  wait <chat> [secs] Wait for a reply (default 60s)
326
371
  send <target> <msg> Send a message
327
372
  contacts [verb] Contacts: list [n] [offset] | search <q> [n] | resolve <handle> | show <handle-or-id>
328
373
  humans <verb> Relationship files: init <contact|slug> | init top [n] | top [days]
374
+ analytics <type> [days] [json|yaml]
375
+ Analytics: ${IMPLEMENTED_TYPES.join(" | ")}
329
376
  logs [tail] Show server debug logs
330
377
  last-error Show last send failure
331
378
  tools List available MCP tools
@@ -342,22 +389,42 @@ async function runInteractiveConsole() {
342
389
  console.log(CONSOLE_HELP);
343
390
  console.log("");
344
391
  const rl = createInterface({ input: process.stdin, output: process.stdout });
345
- const prompt = () => rl.question(color.cyan("imsg> "), async (line) => {
346
- const trimmed = line.trim();
347
- if (!trimmed) {
348
- prompt();
349
- return;
350
- }
351
- const { cmd, args } = parseConsoleInput(trimmed);
352
- try {
353
- await runConsoleCommand(cmd, args, client);
354
- } catch (error) {
355
- log(error instanceof Error ? error.message : String(error), "err");
392
+ const queue = [];
393
+ let processing = false;
394
+ let closed = false;
395
+ const finish = () => {
396
+ client.close();
397
+ process.exit(0);
398
+ };
399
+ const drain = async () => {
400
+ if (processing) return;
401
+ processing = true;
402
+ while (queue.length > 0) {
403
+ const line = queue.shift();
404
+ const trimmed = line.trim();
405
+ if (!trimmed) continue;
406
+ const { cmd, args } = parseConsoleInput(trimmed);
407
+ try {
408
+ await runConsoleCommand(cmd, args, client);
409
+ } catch (error) {
410
+ log(error instanceof Error ? error.message : String(error), "err");
411
+ }
412
+ console.log("");
356
413
  }
357
- console.log("");
358
- prompt();
414
+ processing = false;
415
+ if (closed) finish();
416
+ else rl.prompt();
417
+ };
418
+ rl.setPrompt(color.cyan("imsg> "));
419
+ rl.prompt();
420
+ rl.on("line", (line) => {
421
+ queue.push(line);
422
+ void drain();
423
+ });
424
+ rl.on("close", () => {
425
+ closed = true;
426
+ if (!processing && queue.length === 0) finish();
359
427
  });
360
- prompt();
361
428
  installShutdownHandlers();
362
429
  registerCleanup(() => client.close());
363
430
  }
@@ -379,8 +446,8 @@ async function runExportCommand(target, opts) {
379
446
  const { existsSync: existsSync2, mkdirSync, copyFileSync, statSync } = await import("node:fs");
380
447
  const { homedir } = await import("node:os");
381
448
  const { dirname: dirname2, join: join2, isAbsolute, resolve } = await import("node:path");
382
- const { getContactsDbPaths, getImsgDbPath, getSlugsDbPath } = await import("./shutdown-CqGQKFmr.js").then((n) => n.X);
383
- const { IMessageDB } = await import("./imessage-db-C8WKHFQA.js").then((n) => n.i);
449
+ const { getContactsDbPaths, getImsgDbPath, getSlugsDbPath } = await import("./shutdown-Vh0gjb6m.js").then((n) => n.$);
450
+ const { IMessageDB } = await import("./imessage-db-Dg_EPYPC.js").then((n) => n.i);
384
451
  const { streamExport } = await import("./exportStream-BCoZYUUZ.js");
385
452
  const { parseUserDate } = await import("./dateParse-DJXMfq3a.js");
386
453
  const format = normalizeFormat(opts.format ?? "md");
@@ -514,6 +581,17 @@ program.command("unread").description("Show unread messages across all conversat
514
581
  program.command("search").description("Search messages by text content").argument("<query>", "Search query").argument("[limit]", "Number of results", "20").action(async (query, limit) => {
515
582
  await withClient((c) => printToolResult(c, "search_messages", { query, limit: Number(limit) }));
516
583
  });
584
+ program.command("resolve").description("Resolve a free-form name/phrase to ranked conversations (contacts + threads)").argument("<query>", 'Name or phrase, e.g. "Selena"').argument("[limit]", "Max ranked matches", "10").option("--json", "Output structured JSON").option("--yaml", "Output structured YAML").action(async (query, limit, opts) => {
585
+ await withClient(
586
+ (c) => printToolResult(
587
+ c,
588
+ "resolve_conversation",
589
+ { query, limit: Number(limit) },
590
+ void 0,
591
+ pickFormat(opts)
592
+ )
593
+ );
594
+ });
517
595
  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) => {
518
596
  await withClient(
519
597
  (c) => printToolResult(
@@ -567,14 +645,32 @@ humansCommand.command("init").description(
567
645
  const args = opts.top ? { top: Number(opts.top) } : contact?.includes("~") ? { threadSlug: contact } : { contact };
568
646
  await withClient((c) => printToolResult(c, "init_human", args));
569
647
  });
570
- humansCommand.command("top").description("Show the relationship leaderboard (volume × reciprocity × recency, last year)").argument("[windowDays]", "Days of history to rank", "365").action(async (windowDays) => {
648
+ humansCommand.command("top").description("Show the relationship leaderboard (volume × reciprocity × recency, last 5 years)").argument("[windowDays]", "Days of history to rank", "1825").option("--json", "Output structured JSON").option("--yaml", "Output structured YAML").action(async (windowDays, opts) => {
571
649
  await withClient(
572
- (c) => printToolResult(c, "chat_analytics", {
573
- type: "relationship_leaderboard",
574
- windowDays: Number(windowDays)
575
- })
650
+ (c) => printToolResult(
651
+ c,
652
+ "chat_analytics",
653
+ { type: "relationship_leaderboard", windowDays: parseWindowDays(windowDays, 1825) },
654
+ void 0,
655
+ pickFormat(opts)
656
+ )
576
657
  );
577
658
  });
659
+ const analyticsCommand = program.command("analytics").alias("stats").description("Run chat analytics (streaks, response times, heatmap, tapbacks, wrapped, …)");
660
+ for (const type of IMPLEMENTED_TYPES) {
661
+ const info = ANALYTIC_INFO[type];
662
+ analyticsCommand.command(type).description(info.description).argument("[windowDays]", "Days of history to analyze", String(info.defaultWindowDays)).option("--json", "Output structured JSON").option("--yaml", "Output structured YAML").action(async (windowDays, opts) => {
663
+ await withClient(
664
+ (c) => printToolResult(
665
+ c,
666
+ "chat_analytics",
667
+ { type, windowDays: parseWindowDays(windowDays, info.defaultWindowDays) },
668
+ void 0,
669
+ pickFormat(opts)
670
+ )
671
+ );
672
+ });
673
+ }
578
674
  program.command("logs").description("Show server debug logs").argument("[tail]", "Show only last N lines").action(async (tail) => {
579
675
  await withClient((c) => printToolResult(c, "get_logs", tail ? { tail: Number(tail) } : {}));
580
676
  });
@@ -599,7 +695,7 @@ program.command("tui").description("Launch the read-only terminal UI").option("-
599
695
  });
600
696
  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);
601
697
  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) => {
602
- const { probeMachine, buildMcpSnippet, writeHostConfig } = await import("./setup-BDnz3OuH.js");
698
+ const { probeMachine, buildMcpSnippet, writeHostConfig } = await import("./setup-CysR5gId.js");
603
699
  const report = probeMachine();
604
700
  if (!report.imsgDb.readable) {
605
701
  log(`✗ Messages DB is not readable: ${report.imsgDb.path}`, "err");
@@ -690,6 +786,9 @@ if (invokedAsScript) {
690
786
  });
691
787
  }
692
788
  export {
789
+ parseConsoleInput,
790
+ parseWindowDays,
791
+ runConsoleCommand,
693
792
  runExportCommand
694
793
  };
695
794
  //# sourceMappingURL=cli.js.map