imsg-mcp 1.6.2 → 1.7.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/dist/index.js CHANGED
@@ -1,21 +1,21 @@
1
1
  #!/usr/bin/env node
2
2
  import { execFileSync, execFile } from "node:child_process";
3
3
  import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync, renameSync, chmodSync, statSync, mkdtempSync, rmSync } from "node:fs";
4
- import { join, dirname, isAbsolute } from "node:path";
4
+ import { join, dirname, basename, isAbsolute } from "node:path";
5
5
  import { promisify } from "node:util";
6
6
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
7
7
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
8
8
  import { ListToolsRequestSchema, CallToolRequestSchema, ListResourcesRequestSchema, ListResourceTemplatesRequestSchema, ReadResourceRequestSchema } from "@modelcontextprotocol/sdk/types.js";
9
- import { g as getHumansDirPath, s as sanitizeSlugPart, c as checkLocalAccess, f as formatAccessReport, a as shutdown, b as getImsgDbPath, d as getContactsDbPaths, e as getSlugsDbPath, h as appendLog, i as getFileLogLines, j as getLogDirectory, p as perf, k as checkMessagesAvailable, l as sendToChat, m as sendToChatId, n as sendMessageReliable, o as sendMessageAlt, q as sendAttachment, r as checkImessageAvailability, t as computeRelationshipLeaderboard, u as dispatchAnalytic, v as installShutdownHandlers, w as registerCleanup, x as enableStdinEofDetection, y as enableOrphanWatchdog, z as logStartup, A as info, B as startHeapMonitor, C as getLogs, D as setLastSendError, E as logShutdown, F as stopHeapMonitor, G as getLogFilePath, H as getLastSendError } from "./shutdown-Vh0gjb6m.js";
9
+ import { g as getHumansDirPath, s as sanitizeSlugPart, a as getTranscribeCloudConfig, c as checkLocalAccess, f as formatAccessReport, b as shutdown, d as getImsgDbPath, e as getContactsDbPaths, h as getSlugsDbPath, i as appendLog, j as getFileLogLines, k as getLogDirectory, p as perf, l as checkMessagesAvailable, m as sendToChat, n as sendToChatId, o as sendMessageReliable, q as sendMessageAlt, r as sendAttachment, t as checkImessageAvailability, u as computeRelationshipLeaderboard, v as dispatchAnalytic, w as installShutdownHandlers, x as registerCleanup, y as enableStdinEofDetection, z as enableOrphanWatchdog, A as logStartup, B as info, C as startHeapMonitor, D as getLogs, E as setLastSendError, F as logShutdown, G as stopHeapMonitor, H as getLogFilePath, I as getLastSendError } from "./shutdown-CQ9wzrxA.js";
10
10
  import { homedir, tmpdir } from "node:os";
11
11
  import Database from "better-sqlite3";
12
- import { A as APP_VERSION, a as APP_NAME, r as renderAnalyticText } from "./meta-GGXPVODc.js";
13
- import { n as normalizedPhoneVariants, I as IMessageDB, r as rankFuzzy, h as hasNativeModule } from "./imessage-db-Dpxn-q2z.js";
12
+ import { A as APP_VERSION, a as APP_NAME, r as renderAnalyticText } from "./meta-Lpz_PHJ5.js";
13
+ import { n as normalizedPhoneVariants, I as IMessageDB, r as rankFuzzy, h as hasNativeModule } from "./imessage-db-Cid38QGT.js";
14
14
  import { parseUserDate } from "./date-parse-DJXMfq3a.js";
15
15
  import { streamExport } from "./exportStream-DgPVRZTZ.js";
16
16
  import { z } from "zod";
17
17
  import { randomUUID } from "node:crypto";
18
- import { m as minMessageId, r as resolveRecipient, d as defaultCountryFromEnv, i as installWatchdog, n as noteActivity, a as readWatchdogState } from "./watchdog-DZ92dXs_.js";
18
+ import { m as minMessageId, r as resolveRecipient, d as defaultCountryFromEnv, i as installWatchdog, n as noteActivity, a as readWatchdogState } from "./watchdog-Cl5PoHhW.js";
19
19
  const DEFAULT_TTL_MS = 24 * 60 * 60 * 1e3;
20
20
  let dbPath = join(homedir(), ".imsg-mcp", "analytics-cache.db");
21
21
  let db = null;
@@ -1948,7 +1948,10 @@ const GetAttachmentOutputSchema = z.object({
1948
1948
  base64: z.string().optional(),
1949
1949
  converted: z.string().optional().describe("Set when source was HEIC and we converted to PNG."),
1950
1950
  mediaInfo: z.string().optional().describe("Compact duration/dimensions/codec summary for audio/video (via mdls)."),
1951
- transcript: z.string().optional().describe("Audio transcript when an optional transcriber (hear/yap/whisper-cli) is installed."),
1951
+ transcript: z.string().optional().describe(
1952
+ "Audio transcript. Local on-device transcriber (hear/yap/whisper-cli) when installed; otherwise the opt-in cloud fallback (IMSG_TRANSCRIBE_PROVIDER + IMSG_TRANSCRIBE_API_KEY) if configured."
1953
+ ),
1954
+ transcriptSource: z.enum(["local", "cloud"]).optional().describe("Where the transcript came from: on-device (local) or the opt-in cloud provider."),
1952
1955
  imageBlockIncluded: z.boolean().optional().describe("True when the tool result carries a real MCP image content block.")
1953
1956
  });
1954
1957
  const noArgsSchema = { type: "object", properties: {} };
@@ -2336,7 +2339,7 @@ const TOOLS = [
2336
2339
  },
2337
2340
  {
2338
2341
  name: "get_attachment",
2339
- description: "Fetch an attachment by ROWID (from search_attachments or a message's attachments[].rowId). IMAGES: returned as a real MCP image content block (downscaled ≤1536px, HEIC auto-converted) so the model can SEE it — plus full-size base64 in structuredContent when ≤ inlineMaxBytes (default 5MB). VIDEO: QuickLook poster frame as an image block + duration/resolution metadata + file path. AUDIO (voice memos): metadata + file path, and a transcript when a transcriber (hear, yap, or whisper-cli) is installed on PATH.",
2342
+ description: "Fetch an attachment by ROWID (from search_attachments or a message's attachments[].rowId). IMAGES: returned as a real MCP image content block (downscaled ≤1536px, HEIC auto-converted) so the model can SEE it — plus full-size base64 in structuredContent when ≤ inlineMaxBytes (default 5MB). VIDEO: QuickLook poster frame as an image block + duration/resolution metadata + file path. AUDIO (voice memos): metadata + file path, and a transcript when a local transcriber (hear, yap, or whisper-cli) is installed on PATH, or via an opt-in OpenAI-compatible cloud endpoint (set IMSG_TRANSCRIBE_PROVIDER + IMSG_TRANSCRIBE_API_KEY — audio leaves the device; local always preferred).",
2340
2343
  annotations: annotations.read,
2341
2344
  inputSchema: {
2342
2345
  type: "object",
@@ -2568,6 +2571,42 @@ function transcribeAudio(path, timeoutMs = 6e4) {
2568
2571
  return null;
2569
2572
  }
2570
2573
  }
2574
+ async function transcribeAudioCloud(path, config, opts = {}) {
2575
+ if (!existsSync(path)) return null;
2576
+ const doFetch = opts.fetchImpl ?? globalThis.fetch;
2577
+ const controller = new AbortController();
2578
+ const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? 6e4);
2579
+ try {
2580
+ const form = new FormData();
2581
+ form.append("file", new Blob([readFileSync(path)]), basename(path));
2582
+ form.append("model", config.model);
2583
+ form.append("response_format", "text");
2584
+ const res = await doFetch(`${config.baseUrl}/audio/transcriptions`, {
2585
+ method: "POST",
2586
+ headers: { Authorization: `Bearer ${config.apiKey}` },
2587
+ body: form,
2588
+ signal: controller.signal
2589
+ });
2590
+ if (!res.ok) return null;
2591
+ const text = (await res.text()).trim();
2592
+ return text.length > 0 ? text : null;
2593
+ } catch {
2594
+ return null;
2595
+ } finally {
2596
+ clearTimeout(timer);
2597
+ }
2598
+ }
2599
+ async function transcribeAudioBest(path, opts = {}) {
2600
+ const timeoutMs = opts.timeoutMs ?? 6e4;
2601
+ const local = transcribeAudio(path, timeoutMs);
2602
+ if (local) return { transcript: local, source: "local" };
2603
+ const cloud = opts.cloudConfig !== void 0 ? opts.cloudConfig : getTranscribeCloudConfig();
2604
+ if (cloud) {
2605
+ const t = await transcribeAudioCloud(path, cloud, { timeoutMs, fetchImpl: opts.fetchImpl });
2606
+ if (t) return { transcript: t, source: "cloud" };
2607
+ }
2608
+ return null;
2609
+ }
2571
2610
  const INSTRUCTIONS_UUID = randomUUID();
2572
2611
  function wrapUntrusted(text) {
2573
2612
  if (text == null || text === "") return "";
@@ -3919,6 +3958,7 @@ ${formatted}`, {
3919
3958
  let finalPath = resolvedPath;
3920
3959
  let mediaInfo;
3921
3960
  let transcript;
3961
+ let transcriptSource;
3922
3962
  let imageBlock = null;
3923
3963
  if (isImage) {
3924
3964
  imageBlock = imageBlockFromFile(resolvedPath);
@@ -3950,7 +3990,11 @@ ${formatted}`, {
3950
3990
  } else if (isAudio) {
3951
3991
  mediaInfo = mediaMetadata(resolvedPath) ?? void 0;
3952
3992
  if (sizeBytes <= TRANSCRIBE_MAX_BYTES) {
3953
- transcript = transcribeAudio(resolvedPath) ?? void 0;
3993
+ const t = await transcribeAudioBest(resolvedPath);
3994
+ if (t) {
3995
+ transcript = t.transcript;
3996
+ transcriptSource = t.source;
3997
+ }
3954
3998
  }
3955
3999
  }
3956
4000
  const lines = [];
@@ -3963,10 +4007,18 @@ ${formatted}`, {
3963
4007
  } else if (isAudio) {
3964
4008
  lines.push(`Audio attachment ${rowId} (${sizeBytes}B${mediaInfo ? `, ${mediaInfo}` : ""}).`);
3965
4009
  if (transcript) {
3966
- lines.push("", "Transcript:", wrapUntrusted(sanitizeUserText(transcript) ?? ""));
4010
+ lines.push(
4011
+ "",
4012
+ transcriptSource === "cloud" ? "Transcript (via cloud provider):" : "Transcript:",
4013
+ wrapUntrusted(sanitizeUserText(transcript) ?? "")
4014
+ );
4015
+ } else if (detectTranscriber()) {
4016
+ lines.push("Transcription produced no text.");
4017
+ } else if (getTranscribeCloudConfig()) {
4018
+ lines.push("Cloud transcription produced no text.");
3967
4019
  } else {
3968
4020
  lines.push(
3969
- detectTranscriber() ? "Transcription produced no text." : "No transcriber installed — `brew install yap` (macOS 26+) or `brew install sveinbjornt/hear/hear` enables on-device transcription."
4021
+ "No transcriber installed — `brew install yap` (macOS 26+) or `brew install sveinbjornt/hear/hear` enables on-device transcription; or set IMSG_TRANSCRIBE_PROVIDER + IMSG_TRANSCRIBE_API_KEY for an OpenAI-compatible cloud fallback (audio leaves this device)."
3970
4022
  );
3971
4023
  }
3972
4024
  lines.push(`Path: ${resolvedPath}`);
@@ -3998,6 +4050,7 @@ ${formatted}`, {
3998
4050
  converted: convertedNote,
3999
4051
  mediaInfo,
4000
4052
  transcript,
4053
+ transcriptSource,
4001
4054
  imageBlockIncluded: imageBlock != null
4002
4055
  }
4003
4056
  };