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.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shutdown-B9ClCyco.js","sources":["../src/config.ts","../src/logger.ts","../src/db-schema.ts","../src/mock-send-db.ts","../src/applescript.ts","../src/access-check.ts","../src/shutdown.ts"],"sourcesContent":["import { existsSync, readdirSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { dirname, join, resolve, sep } from \"node:path\";\n\nfunction resolveEnvPath(value: string | undefined, fallback: string): string {\n if (!value) return fallback;\n if (value.startsWith(\"~/\") || value === \"~\") {\n return join(homedir(), value.slice(2));\n }\n return resolve(process.cwd(), value);\n}\n\nexport function getEnv(): string {\n return process.env.VITE_ENV ?? \"development\";\n}\n\n/** Bundled DB / mock-sending mode (cloud, CI tests via `.env.test`). */\nexport function isAiEnv(): boolean {\n return getEnv() === \"ai\";\n}\n\n/** Any non-`ai` mode (e.g. `development` on your Mac). */\nexport function isLocalEnv(): boolean {\n return !isAiEnv();\n}\n\nexport function getImsgDbPath(): string {\n return resolveEnvPath(\n process.env.VITE_IMSG_DB_PATH,\n join(homedir(), \"Library\", \"Messages\", \"chat.db\"),\n );\n}\n\nexport function getContactsDbPaths(): string[] | undefined {\n const main =\n process.env.VITE_CONTACTS_DB_PATH ??\n join(homedir(), \"Library\", \"Application Support\", \"AddressBook\", \"AddressBook-v22.abcddb\");\n\n const mainResolved = resolveEnvPath(main, \"\");\n const paths = new Set<string>([mainResolved]);\n\n const sourcesSegment = `${sep}Sources${sep}`;\n const sourcesIndex = mainResolved.indexOf(sourcesSegment);\n const addressBookDir =\n sourcesIndex >= 0 ? mainResolved.slice(0, sourcesIndex) : dirname(mainResolved);\n\n const addSourceDb = (path: string) => {\n if (existsSync(path)) {\n paths.add(path);\n }\n };\n\n const uuid = process.env.VITE_ADDRESS_BOOK_UUID;\n if (uuid) {\n addSourceDb(join(addressBookDir, \"Sources\", uuid, \"AddressBook-v22.abcddb\"));\n }\n\n const sourcesDir = join(addressBookDir, \"Sources\");\n if (existsSync(sourcesDir)) {\n try {\n const entries = readdirSync(sourcesDir, { withFileTypes: true });\n for (const entry of entries) {\n if (!entry.isDirectory()) continue;\n addSourceDb(join(sourcesDir, entry.name, \"AddressBook-v22.abcddb\"));\n }\n } catch {\n // Ignore unreadable Address Book source directories.\n }\n }\n\n return [...paths];\n}\n\nexport function getSlugsDbPath(): string {\n return resolveEnvPath(process.env.VITE_SLUGS_DB_PATH, join(homedir(), \".imsg-mcp\", \"slugs.db\"));\n}\n\nexport function getVcfPath(): string {\n return resolveEnvPath(process.env.VITE_VCF_PATH, join(process.cwd(), \"fixtures\", \"contacts.vcf\"));\n}\n","/**\n * Structured logger with performance monitoring.\n *\n * - In-memory buffer for MCP `get_logs` tool (backwards compatible)\n * - NDJSON file output to $TMPDIR/imsg-mcp/ for post-mortem analysis\n * - Performance spans: `const span = perf(\"op\"); ... span.end({ rows: 100 })`\n * - Heap memory tracking on every log entry\n *\n * Files in $TMPDIR are cleaned up by the OS automatically.\n */\n\nimport { appendFileSync, existsSync, mkdirSync } from \"node:fs\";\nimport { freemem, tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\n\n// ── Types ──────────────────────────────────────────────────────────────\n\nexport type LogLevel = \"info\" | \"warn\" | \"error\" | \"perf\";\n\nexport interface LogEntry {\n ts: string;\n level: LogLevel;\n msg: string;\n dur_ms?: number;\n mem_mb: number;\n mem_delta_mb?: number;\n data?: Record<string, unknown>;\n}\n\nexport interface PerfSpan {\n /** End the span. Logs duration, memory delta, and optional metadata. */\n end(data?: Record<string, unknown>): number;\n}\n\nexport interface LastSendErrorDetails {\n message: string;\n stderr?: string;\n stdout?: string;\n code?: string | number;\n timestamp: string;\n}\n\n// ── State ──────────────────────────────────────────────────────────────\n\nconst MAX_LOG_LINES = 500;\nconst MAX_FILE_BYTES = 10 * 1024 * 1024; // 10 MB rotation\n\n/**\n * File logging is opt-in via IMSG_DEV=1. End users running the published\n * `imsg mcp` bin don't get NDJSON files in $TMPDIR. The in-memory ring buffer\n * still works (it's bounded to 500 lines) so a dev who opts in can still hit\n * the `get_logs` MCP tool — but that tool itself is also gated on IMSG_DEV.\n *\n * Checked at call time, not module-load time, so tests can flip the flag\n * without needing to re-import the module.\n */\nfunction isFileLoggingEnabled(): boolean {\n return process.env.IMSG_DEV === \"1\";\n}\n\nfunction isVerboseLogging(): boolean {\n return process.env.IMSG_LOG_VERBOSE === \"1\";\n}\n\nconst memoryLines: string[] = [];\nlet logFilePath: string | null = null;\nlet logFileBytes = 0;\nlet lastSendError: LastSendErrorDetails | null = null;\n\n// ── File output ────────────────────────────────────────────────────────\n\nfunction getLogDir(): string {\n return join(tmpdir(), \"imsg-mcp\");\n}\n\nfunction ensureLogFile(): string | null {\n if (logFilePath && logFileBytes < MAX_FILE_BYTES) return logFilePath;\n\n try {\n const dir = getLogDir();\n if (!existsSync(dir)) mkdirSync(dir, { recursive: true });\n\n const date = new Date().toISOString().replace(/[:.]/g, \"-\").slice(0, 19);\n logFilePath = join(dir, `imsg-mcp-${process.pid}-${date}.ndjson`);\n logFileBytes = 0;\n return logFilePath;\n } catch {\n return null;\n }\n}\n\nfunction writeToFile(json: string): void {\n if (!isFileLoggingEnabled()) return;\n const path = ensureLogFile();\n if (!path) return;\n try {\n const line = `${json}\\n`;\n appendFileSync(path, line);\n logFileBytes += line.length;\n } catch {\n // Don't let file I/O failures break the app.\n }\n}\n\n// ── Helpers ────────────────────────────────────────────────────────────\n\nfunction heapMB(): number {\n return Math.round((process.memoryUsage().heapUsed / 1024 / 1024) * 10) / 10;\n}\n\nfunction formatMemoryLine(entry: LogEntry): string {\n let line = `${entry.ts} [${entry.level}] ${entry.msg}`;\n if (entry.dur_ms != null) line += ` (${entry.dur_ms.toFixed(1)}ms)`;\n if (entry.data != null) line += ` ${JSON.stringify(entry.data)}`;\n return line;\n}\n\nfunction emit(entry: LogEntry): void {\n // In-memory buffer for MCP get_logs tool\n const line = formatMemoryLine(entry);\n memoryLines.push(line);\n if (memoryLines.length > MAX_LOG_LINES) {\n memoryLines.splice(0, memoryLines.length - MAX_LOG_LINES);\n }\n\n // NDJSON file output\n writeToFile(JSON.stringify(entry));\n}\n\n// ── Public API ─────────────────────────────────────────────────────────\n\nexport function info(msg: string, data?: Record<string, unknown>): void {\n emit({ ts: new Date().toISOString(), level: \"info\", msg, mem_mb: heapMB(), data });\n}\n\nexport function warn(msg: string, data?: Record<string, unknown>): void {\n emit({ ts: new Date().toISOString(), level: \"warn\", msg, mem_mb: heapMB(), data });\n}\n\nexport function error(msg: string, data?: Record<string, unknown>): void {\n emit({ ts: new Date().toISOString(), level: \"error\", msg, mem_mb: heapMB(), data });\n}\n\n/**\n * Start a performance span. Call `.end()` on the returned object to log\n * the duration, heap delta, and optional metadata.\n *\n * ```ts\n * const span = perf(\"listConversations\");\n * // ... work ...\n * span.end({ chats: 200, deduped: 180 });\n * ```\n */\nexport function perf(msg: string): PerfSpan {\n const startTime = performance.now();\n const startHeap = heapMB();\n\n return {\n end(data?: Record<string, unknown>): number {\n const dur_ms = performance.now() - startTime;\n const endHeap = heapMB();\n emit({\n ts: new Date().toISOString(),\n level: \"perf\",\n msg,\n dur_ms,\n mem_mb: endHeap,\n mem_delta_mb: Math.round((endHeap - startHeap) * 10) / 10,\n data,\n });\n return dur_ms;\n },\n };\n}\n\n// ── Backwards-compatible API (used by MCP tools in index.ts) ───────────\n\n/** @deprecated Use info/warn/error instead. Kept for MCP tool compat. */\nexport function appendLog(level: string, message: string, data?: unknown): void {\n const entry: LogEntry = {\n ts: new Date().toISOString(),\n level: level as LogLevel,\n msg: message,\n mem_mb: heapMB(),\n data:\n data != null\n ? typeof data === \"object\"\n ? (data as Record<string, unknown>)\n : { value: data }\n : undefined,\n };\n emit(entry);\n}\n\nexport function getLogs(tail?: number): string[] {\n if (tail != null && tail > 0) return memoryLines.slice(-tail);\n return [...memoryLines];\n}\n\nexport function clearLogs(): void {\n memoryLines.length = 0;\n}\n\nexport function setLastSendError(details: Omit<LastSendErrorDetails, \"timestamp\">): void {\n lastSendError = { ...details, timestamp: new Date().toISOString() };\n error(\"send_message failed\", details as Record<string, unknown>);\n}\n\nexport function getLastSendError(): LastSendErrorDetails | null {\n return lastSendError ? { ...lastSendError } : null;\n}\n\n/** Return the path to the current log file (if any). */\nexport function getLogFilePath(): string | null {\n return logFilePath;\n}\n\n/** Return the log directory path. */\nexport function getLogDirectory(): string {\n return getLogDir();\n}\n\n/** Log a startup marker — call at process start. */\nexport function logStartup(entrypoint: string): void {\n info(\"startup\", { pid: process.pid, ppid: process.ppid, entrypoint, node: process.version });\n}\n\n/** Log a shutdown marker — call before process exits. */\nexport function logShutdown(reason: string): void {\n info(\"shutdown\", { pid: process.pid, reason, uptime_s: Math.round(process.uptime()) });\n}\n\n/**\n * Read the latest NDJSON log file from disk (for external access).\n * Returns the last N lines from the most recent log file.\n */\nexport function getFileLogLines(tail = 50): string[] {\n try {\n const { readdirSync, readFileSync } = require(\"node:fs\") as typeof import(\"node:fs\");\n const dir = getLogDir();\n const files = readdirSync(dir)\n .filter((f: string) => f.endsWith(\".ndjson\"))\n .sort()\n .reverse();\n if (files.length === 0) return [];\n\n const content = readFileSync(join(dir, files[0]), \"utf8\");\n const lines = content.trim().split(\"\\n\").filter(Boolean);\n return tail > 0 ? lines.slice(-tail) : lines;\n } catch {\n return [];\n }\n}\n\n// ── Heap monitor ───────────────────────────────────────────────────────\n\nconst HEAP_WARN_MB = 150;\nlet heapMonitorTimer: ReturnType<typeof setInterval> | null = null;\n\n/**\n * Start periodic heap monitoring. Logs a warning if heap exceeds the\n * threshold. Call once at server startup.\n *\n * No-op when IMSG_DEV is unset — end users running the published bin don't\n * accumulate heartbeats. The watchdog itself (src/watchdog.ts) still runs\n * unconditionally for self-healing; this monitor is purely observability.\n *\n * Heartbeats fire every 60s by default, or every 10s when IMSG_LOG_VERBOSE=1.\n */\nexport function startHeapMonitor(): void {\n if (!isFileLoggingEnabled()) return;\n if (heapMonitorTimer) return;\n const intervalMs = isVerboseLogging() ? 10_000 : 60_000;\n heapMonitorTimer = setInterval(() => {\n const heap = heapMB();\n const { rss } = process.memoryUsage();\n const rssMb = Math.round((rss / 1024 / 1024) * 10) / 10;\n if (heap > HEAP_WARN_MB) {\n warn(\"heap exceeds threshold\", { heap_mb: heap, rss_mb: rssMb, threshold_mb: HEAP_WARN_MB });\n }\n // System-level memory pressure — captures cases where the host or OS may\n // be about to reclaim us. Helps diagnose \"process vanished\" reports where\n // the kill came from outside (SIGKILL, OOM, parent host).\n const freeMb = Math.round(freemem() / 1024 / 1024);\n // Always log a periodic heartbeat at info level for post-mortem analysis\n emit({\n ts: new Date().toISOString(),\n level: \"info\",\n msg: \"heartbeat\",\n mem_mb: heap,\n data: {\n rss_mb: rssMb,\n uptime_s: Math.round(process.uptime()),\n system_free_mb: freeMb || undefined,\n },\n });\n }, intervalMs);\n // Don't prevent process exit\n heapMonitorTimer.unref();\n}\n\nexport function stopHeapMonitor(): void {\n if (heapMonitorTimer) {\n clearInterval(heapMonitorTimer);\n heapMonitorTimer = null;\n }\n}\n","/**\n * iMessage SQLite database schema constants and helpers.\n * Single source of truth for chat.db structure (synced with Messages.app / SMS).\n * See docs/IMESSAGE_DB_SCHEMA.md for full reference.\n */\n\n/** Seconds between Unix epoch (1970-01-01) and Mac epoch (2001-01-01). */\nexport const MAC_EPOCH_OFFSET = 978307200;\n\n/** Timestamps in DB are nanoseconds since 2001-01-01. */\nexport const NANOS_PER_SECOND = 1_000_000_000;\n\nexport const Tables = {\n MESSAGE: \"message\",\n HANDLE: \"handle\",\n CHAT: \"chat\",\n CHAT_MESSAGE_JOIN: \"chat_message_join\",\n CHAT_HANDLE_JOIN: \"chat_handle_join\",\n ATTACHMENT: \"attachment\",\n MESSAGE_ATTACHMENT_JOIN: \"message_attachment_join\",\n RECOVERABLE_MESSAGE_PART: \"recoverable_message_part\",\n} as const;\n\n/** associated_message_type: 0 = normal message; 2,3 = business; 1000 = sticker; 2000–3006 = tapbacks. */\nexport const AssociatedMessageType = {\n NORMAL: 0,\n STICKER: 1000,\n LOVE_ADD: 2000,\n LIKE_ADD: 2001,\n DISLIKE_ADD: 2002,\n LAUGH_ADD: 2003,\n EMPHASIZE_ADD: 2004,\n QUESTION_ADD: 2005,\n EMOJI_ADD: 2006,\n LOVE_REMOVE: 3000,\n LIKE_REMOVE: 3001,\n DISLIKE_REMOVE: 3002,\n LAUGH_REMOVE: 3003,\n EMPHASIZE_REMOVE: 3004,\n QUESTION_REMOVE: 3005,\n EMOJI_REMOVE: 3006,\n} as const;\n\n/** Minimum type value for tapback/reaction messages (add). */\nexport const TAPBACK_ADD_MIN = 2000;\n\n/** Minimum type value for reaction messages we query (adds). */\nexport const TAPBACK_QUERY_MIN = 2000;\n\n/** Format: p:PART_INDEX/MESSAGE_GUID. */\nexport const ASSOCIATED_MESSAGE_GUID_REGEX = /^p:(\\d+)\\/(.+)$/;\n\n/** Object replacement character used as placeholder for inline attachments / rich content. */\nexport const OBJECT_REPLACEMENT_CHAR = \"\\uFFFC\";\n\n/**\n * Convert macOS timestamp (nanoseconds since 2001-01-01) to JS Date.\n */\nexport function macTimestampToDate(timestamp: number | null): Date | null {\n if (timestamp == null || timestamp === 0) return null;\n const unixSeconds = timestamp / NANOS_PER_SECOND + MAC_EPOCH_OFFSET;\n return new Date(unixSeconds * 1000);\n}\n\n/**\n * Parse associated_message_guid to extract target message GUID and part index.\n * Format: \"p:PART_INDEX/MESSAGE_GUID\"\n */\nexport function parseAssociatedMessageGuid(\n guid: string | null,\n): { targetGuid: string; partIndex: number } | null {\n if (!guid) return null;\n const match = guid.match(ASSOCIATED_MESSAGE_GUID_REGEX);\n if (match) {\n return { partIndex: parseInt(match[1], 10), targetGuid: match[2] };\n }\n return null;\n}\n\n/** Whether associated_message_type represents a reaction/tapback (excludes normal and business). */\nexport function isReactionType(associatedMessageType: number): boolean {\n return associatedMessageType >= 1000 && associatedMessageType !== 0;\n}\n","import { randomUUID } from \"node:crypto\";\nimport Database from \"better-sqlite3\";\nimport { MAC_EPOCH_OFFSET, NANOS_PER_SECOND } from \"./db-schema.js\";\n\nfunction dateToMacTimestamp(date: Date): number {\n return Math.floor((date.getTime() / 1000 - MAC_EPOCH_OFFSET) * NANOS_PER_SECOND);\n}\n\nfunction resolveChat(\n db: Database.Database,\n opts: { chatIdentifier?: string; chatGuid?: string },\n): { chatRowId: number; handleId: number } | null {\n let chatRow: any;\n if (opts.chatGuid) {\n chatRow = db\n .prepare(\"SELECT ROWID, chat_identifier FROM chat WHERE guid = ?\")\n .get(opts.chatGuid);\n }\n if (!chatRow && opts.chatIdentifier) {\n chatRow = db\n .prepare(\"SELECT ROWID, chat_identifier FROM chat WHERE chat_identifier = ?\")\n .get(opts.chatIdentifier);\n }\n if (!chatRow) return null;\n\n const handle: any = db\n .prepare(\"SELECT ROWID FROM handle WHERE id = ?\")\n .get(chatRow.chat_identifier);\n\n const handleId = handle?.ROWID ?? 0;\n return { chatRowId: chatRow.ROWID, handleId };\n}\n\nexport function insertSentMessage(\n dbPath: string,\n target: { chatIdentifier?: string; chatGuid?: string },\n text: string,\n): number | null {\n const db = new Database(dbPath);\n db.pragma(\"journal_mode = WAL\");\n\n try {\n const resolved = resolveChat(db, target);\n if (!resolved) {\n console.warn(\"[mock-send] Could not resolve chat for\", target);\n return null;\n }\n\n const now = new Date();\n const macDate = dateToMacTimestamp(now);\n const guid = `mock:${randomUUID()}`;\n\n const insert = db.prepare(`\n INSERT INTO message (guid, text, handle_id, date, is_from_me, associated_message_type, is_read, is_delivered, service)\n VALUES (?, ?, ?, ?, 1, 0, 1, 1, 'iMessage')\n `);\n const result = insert.run(guid, text, resolved.handleId, macDate);\n const messageRowId = Number(result.lastInsertRowid);\n\n db.prepare(\n \"INSERT INTO chat_message_join (chat_id, message_id, message_date) VALUES (?, ?, ?)\",\n ).run(resolved.chatRowId, messageRowId, macDate);\n\n return messageRowId;\n } finally {\n db.close();\n }\n}\n","import { execFile } from \"node:child_process\";\nimport { randomBytes } from \"node:crypto\";\nimport { unlinkSync, writeFileSync } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { promisify } from \"node:util\";\nimport { getImsgDbPath, isAiEnv } from \"./config.js\";\nimport { setLastSendError } from \"./logger.js\";\nimport { insertSentMessage } from \"./mock-send-db.js\";\nimport type { SendMessageResult } from \"./types.js\";\n\nconst execFileAsync = promisify(execFile);\n\n/**\n * Detect whether a handle looks like a phone number suitable for SMS\n * fallback. We don't bother validating the country/area code — Messages.app\n * will reject malformed ones with a clear error.\n */\nfunction isPhoneLike(handle: string): boolean {\n return /^\\+?[\\d\\s\\-()]+$/.test(handle.trim());\n}\n\n/** Result shape for the preflight reachability check. */\nexport interface ImessageAvailability {\n /** Best-guess service that will reach this handle. */\n service: \"iMessage\" | \"SMS\" | \"unknown\";\n /** True iff at least one service appears to be reachable for the handle. */\n reachable: boolean;\n /** Human-readable hint when not reachable, for LLM remediation. */\n hint?: string;\n}\n/** `VITE_ENV=ai`, or any test run (never hit Messages.app / osascript under Vitest). */\nconst MOCK = isAiEnv() || process.env.VITEST === \"true\";\n\n// ---------------------------------------------------------------------------\n// Mock helpers (MOCK: return success + optional insert into chat.db)\n// ---------------------------------------------------------------------------\n\nfunction mockSend(\n text: string,\n target: { chatIdentifier?: string; chatGuid?: string },\n): SendMessageResult {\n // Under Vitest, return success only — no SQLite (avoids LFS/pointer noise; tests cover API shape).\n if (process.env.VITEST !== \"true\") {\n try {\n insertSentMessage(getImsgDbPath(), target, text);\n } catch (err) {\n console.warn(\"[mock-send] DB insert failed (non-fatal):\", err);\n }\n }\n return { success: true, timestamp: new Date() };\n}\n\n// ---------------------------------------------------------------------------\n// Real AppleScript helpers\n// ---------------------------------------------------------------------------\n\nasync function runAppleScript(\n script: string,\n captureErrorForSend: boolean = false,\n): Promise<string> {\n try {\n const { stdout, stderr } = await execFileAsync(\"osascript\", [\"-e\", script], {\n timeout: 30000,\n });\n if (stderr?.trim()) {\n console.error(\"[osascript] stderr:\", stderr);\n }\n return stdout.trim();\n } catch (error: any) {\n if (captureErrorForSend) {\n setLastSendError({\n message: error.message || String(error),\n stderr: error.stderr ?? undefined,\n stdout: error.stdout ?? undefined,\n code: error.code ?? undefined,\n });\n }\n if (error.code === \"ENOENT\") {\n throw new Error(\"osascript not found. This tool requires macOS.\");\n }\n if (error.stderr) {\n throw new Error(`AppleScript error: ${error.stderr}`);\n }\n throw error;\n }\n}\n\nexport function appleScriptEscape(str: string): string {\n return str\n .replace(/\\\\/g, \"\\\\\\\\\")\n .replace(/\"/g, '\\\\\"')\n .replace(/\\n/g, \"\\\\n\")\n .replace(/\\r/g, \"\\\\r\")\n .replace(/\\t/g, \"\\\\t\");\n}\n\n// ---------------------------------------------------------------------------\n// Public API -- each function branches on MOCK\n// ---------------------------------------------------------------------------\n\nexport async function sendMessage(recipient: string, message: string): Promise<SendMessageResult> {\n if (MOCK) return mockSend(message, { chatIdentifier: recipient });\n\n const escapedRecipient = appleScriptEscape(recipient);\n const escapedMessage = appleScriptEscape(message);\n const script = `\n tell application \"Messages\"\n set targetService to 1st account whose service type = iMessage\n set targetBuddy to participant \"${escapedRecipient}\" of targetService\n send \"${escapedMessage}\" to targetBuddy\n end tell\n `;\n try {\n await runAppleScript(script, true);\n return { success: true, timestamp: new Date() };\n } catch (error: any) {\n return { success: false, error: error.message || String(error) };\n }\n}\n\nexport async function sendMessageAlt(\n recipient: string,\n message: string,\n): Promise<SendMessageResult> {\n if (MOCK) return mockSend(message, { chatIdentifier: recipient });\n\n const escapedRecipient = appleScriptEscape(recipient);\n const escapedMessage = appleScriptEscape(message);\n const script = `\n tell application \"Messages\"\n send \"${escapedMessage}\" to buddy \"${escapedRecipient}\" of (service 1 whose service type is iMessage)\n end tell\n `;\n try {\n await runAppleScript(script, true);\n return { success: true, timestamp: new Date() };\n } catch (_error: any) {\n return sendSMS(recipient, message);\n }\n}\n\nexport async function sendSMS(phoneNumber: string, message: string): Promise<SendMessageResult> {\n if (MOCK) return mockSend(message, { chatIdentifier: phoneNumber });\n\n const escapedPhone = appleScriptEscape(phoneNumber);\n const escapedMessage = appleScriptEscape(message);\n const script = `\n tell application \"Messages\"\n send \"${escapedMessage}\" to buddy \"${escapedPhone}\" of (service 1 whose service type is SMS)\n end tell\n `;\n try {\n await runAppleScript(script, true);\n return { success: true, timestamp: new Date() };\n } catch (error: any) {\n return { success: false, error: error.message || String(error) };\n }\n}\n\nexport async function sendToChat(chatName: string, message: string): Promise<SendMessageResult> {\n if (MOCK) return mockSend(message, { chatIdentifier: chatName });\n\n const escapedName = appleScriptEscape(chatName);\n const escapedMessage = appleScriptEscape(message);\n const script = `\n tell application \"Messages\"\n send \"${escapedMessage}\" to chat \"${escapedName}\"\n end tell\n `;\n try {\n await runAppleScript(script, true);\n return { success: true, timestamp: new Date() };\n } catch (error: any) {\n return { success: false, error: error.message || String(error) };\n }\n}\n\nexport async function sendToChatId(chatId: string, message: string): Promise<SendMessageResult> {\n if (MOCK) return mockSend(message, { chatGuid: chatId });\n\n const escapedId = appleScriptEscape(chatId);\n const escapedMessage = appleScriptEscape(message);\n const script = `\n tell application \"Messages\"\n send \"${escapedMessage}\" to text chat id \"${escapedId}\"\n end tell\n `;\n try {\n await runAppleScript(script, true);\n return { success: true, timestamp: new Date() };\n } catch (error: any) {\n return { success: false, error: error.message || String(error) };\n }\n}\n\nexport async function checkMessagesAvailable(): Promise<boolean> {\n if (MOCK) return true;\n\n const script = `\n tell application \"System Events\"\n return exists application process \"Messages\"\n end tell\n `;\n try {\n const result = await runAppleScript(script);\n return result === \"true\";\n } catch {\n return false;\n }\n}\n\nexport async function getAvailableServices(): Promise<string[]> {\n if (MOCK) return [\"iMessage\", \"SMS\"];\n\n const script = `\n tell application \"Messages\"\n set serviceList to {}\n repeat with s in services\n set end of serviceList to (service type of s as string)\n end repeat\n return serviceList\n end tell\n `;\n try {\n const result = await runAppleScript(script);\n return result\n .split(\", \")\n .map((s) => s.trim())\n .filter(Boolean);\n } catch {\n return [];\n }\n}\n\nexport async function activateMessages(): Promise<void> {\n if (MOCK) return;\n await runAppleScript('tell application \"Messages\" to activate');\n}\n\nexport async function buddyExists(address: string): Promise<boolean> {\n if (MOCK) return true;\n\n const escaped = appleScriptEscape(address);\n const script = `\n tell application \"Messages\"\n try\n set targetBuddy to buddy \"${escaped}\" of (service 1 whose service type is iMessage)\n return true\n on error\n return false\n end try\n end tell\n `;\n try {\n const result = await runAppleScript(script);\n return result === \"true\";\n } catch {\n return false;\n }\n}\n\n/**\n * Reliable send: writes the message body to a UTF-8 temp file and uses\n * `read (POSIX file \"...\") as «class utf8»` inside AppleScript. This avoids\n * two recurring failure modes of inline-string sends:\n * 1. AppleScript string-length limits on very long messages.\n * 2. Quote/backslash/escape bugs when the message contains arbitrary user\n * content (emoji ZWJ sequences, smart quotes, backticks, etc).\n *\n * Auto-fallback: if iMessage send fails AND the recipient looks like a phone\n * number, retries via the SMS service in the same AppleScript invocation.\n */\nexport async function sendMessageReliable(\n recipient: string,\n message: string,\n): Promise<SendMessageResult> {\n if (MOCK) return mockSend(message, { chatIdentifier: recipient });\n\n const tmpFile = join(tmpdir(), `imsg-send-${randomBytes(8).toString(\"hex\")}.txt`);\n try {\n writeFileSync(tmpFile, message, { encoding: \"utf8\" });\n } catch (error: any) {\n return { success: false, error: `Failed to stage send payload: ${error.message ?? error}` };\n }\n\n const escapedRecipient = appleScriptEscape(recipient);\n const escapedPath = appleScriptEscape(tmpFile);\n const phoneFallback = isPhoneLike(recipient);\n\n // Outer try sends via iMessage; if it errors AND the recipient is a phone\n // number, the on-error branch falls back to SMS. Both branches read the\n // body from the temp file to avoid AppleScript string handling.\n const script = phoneFallback\n ? `\n tell application \"Messages\"\n set msgBody to read (POSIX file \"${escapedPath}\") as «class utf8»\n try\n set iSvc to 1st account whose service type = iMessage\n send msgBody to participant \"${escapedRecipient}\" of iSvc\n return \"iMessage\"\n on error\n set sSvc to 1st account whose service type = SMS\n send msgBody to participant \"${escapedRecipient}\" of sSvc\n return \"SMS\"\n end try\n end tell\n `\n : `\n tell application \"Messages\"\n set msgBody to read (POSIX file \"${escapedPath}\") as «class utf8»\n set iSvc to 1st account whose service type = iMessage\n send msgBody to participant \"${escapedRecipient}\" of iSvc\n return \"iMessage\"\n end tell\n `;\n\n try {\n const service = await runAppleScript(script, true);\n return {\n success: true,\n timestamp: new Date(),\n service: service === \"SMS\" || service === \"iMessage\" ? service : undefined,\n };\n } catch (error: any) {\n return { success: false, error: error.message || String(error) };\n } finally {\n // Best-effort cleanup — failure here is non-fatal (OS will reap $TMPDIR).\n try {\n unlinkSync(tmpFile);\n } catch {\n // ignore\n }\n }\n}\n\n/**\n * Send a file attachment to a participant. Returns success/error.\n *\n * Uses `send (POSIX file \"...\") to participant ...` — the Messages SDEF\n * accepts a file specifier as the first argument to `send`. The file must\n * exist on disk; Messages.app will rate-limit or reject very large files\n * (typical cap ~100MB).\n *\n * For phone-like recipients, falls back to SMS service on iMessage failure.\n */\nexport async function sendAttachment(\n recipient: string,\n filepath: string,\n): Promise<SendMessageResult> {\n if (MOCK) return mockSend(`[attachment:${filepath}]`, { chatIdentifier: recipient });\n\n const escapedRecipient = appleScriptEscape(recipient);\n const escapedPath = appleScriptEscape(filepath);\n const phoneFallback = isPhoneLike(recipient);\n\n const script = phoneFallback\n ? `\n tell application \"Messages\"\n try\n set iSvc to 1st account whose service type = iMessage\n send (POSIX file \"${escapedPath}\") to participant \"${escapedRecipient}\" of iSvc\n return \"iMessage\"\n on error\n set sSvc to 1st account whose service type = SMS\n send (POSIX file \"${escapedPath}\") to participant \"${escapedRecipient}\" of sSvc\n return \"SMS\"\n end try\n end tell\n `\n : `\n tell application \"Messages\"\n set iSvc to 1st account whose service type = iMessage\n send (POSIX file \"${escapedPath}\") to participant \"${escapedRecipient}\" of iSvc\n return \"iMessage\"\n end tell\n `;\n\n try {\n const service = await runAppleScript(script, true);\n return {\n success: true,\n timestamp: new Date(),\n service: service === \"SMS\" || service === \"iMessage\" ? service : undefined,\n };\n } catch (error: any) {\n return { success: false, error: error.message || String(error) };\n }\n}\n\n/**\n * Preflight reachability check. Cheap call that an agent should make BEFORE\n * `send_message` to avoid wasted attempts when the handle can't be reached.\n *\n * Detection rules:\n * - Try iMessage `buddy` lookup → if success, return `iMessage`.\n * - If handle looks like a phone number, try SMS `buddy` → if success,\n * return `SMS`.\n * - Otherwise return `unknown` with a remediation hint.\n */\nexport async function checkImessageAvailability(handle: string): Promise<ImessageAvailability> {\n if (MOCK) {\n return { service: \"iMessage\", reachable: true };\n }\n\n const escaped = appleScriptEscape(handle);\n const script = `\n tell application \"Messages\"\n try\n set b to buddy \"${escaped}\" of (1st account whose service type is iMessage)\n return \"iMessage\"\n on error\n try\n set b to buddy \"${escaped}\" of (1st account whose service type is SMS)\n return \"SMS\"\n on error\n return \"unknown\"\n end try\n end try\n end tell\n `;\n try {\n const result = await runAppleScript(script);\n if (result === \"iMessage\" || result === \"SMS\") {\n return { service: result, reachable: true };\n }\n return {\n service: \"unknown\",\n reachable: false,\n hint: isPhoneLike(handle)\n ? \"Handle not found in iMessage or SMS buddies. Verify the number format (try '+1...' for US) and that the recipient has at least one of iMessage or SMS reachable.\"\n : \"Handle not found in iMessage buddies. For email addresses, the recipient must have iMessage active on that address.\",\n };\n } catch (error: any) {\n return {\n service: \"unknown\",\n reachable: false,\n hint: `Availability check failed: ${error.message ?? error}. Most common cause: Messages.app Automation permission is not granted to this terminal/IDE.`,\n };\n }\n}\n","import { existsSync } from \"node:fs\";\nimport os from \"node:os\";\nimport Database from \"better-sqlite3\";\nimport { checkMessagesAvailable } from \"./applescript.js\";\nimport { getContactsDbPaths, getImsgDbPath } from \"./config.js\";\n\nexport type AccessStatus = \"ok\" | \"warn\" | \"error\";\n\nexport interface AccessCheckItem {\n key: string;\n label: string;\n status: AccessStatus;\n detail: string;\n}\n\nexport interface AccessReport {\n ok: boolean;\n items: AccessCheckItem[];\n}\n\nfunction inspectSqlite(path: string): string | null {\n const db = new Database(path, { readonly: true, fileMustExist: true });\n try {\n db.pragma(\"schema_version\");\n return null;\n } finally {\n db.close();\n }\n}\n\nfunction classifyDbError(error: unknown): string {\n const message = error instanceof Error ? error.message : String(error);\n if (/operation not permitted/i.test(message) || /authorization denied/i.test(message)) {\n return [\n \"Full Disk Access is missing for the app running this command.\",\n \"Grant Full Disk Access to Terminal, iTerm2, Warp, VS Code, Cursor, or whichever app is launching imsg-mcp, then fully restart that app.\",\n \"Open System Settings -> Privacy & Security -> Full Disk Access.\",\n ].join(\" \");\n }\n if (/unable to open database file/i.test(message) || /no such file/i.test(message)) {\n return \"Database file was not found at the configured path.\";\n }\n return message;\n}\n\nfunction iconFor(status: AccessStatus): string {\n if (status === \"ok\") return \"OK\";\n if (status === \"warn\") return \"WARN\";\n return \"ERR\";\n}\n\nexport async function checkLocalAccess(): Promise<AccessReport> {\n const items: AccessCheckItem[] = [];\n\n items.push({\n key: \"platform\",\n label: \"Platform\",\n status: process.platform === \"darwin\" ? \"ok\" : \"error\",\n detail:\n process.platform === \"darwin\"\n ? `Running on macOS ${os.release()}`\n : `Running on ${process.platform}; live iMessage access only works on macOS.`,\n });\n\n const imsgDbPath = getImsgDbPath();\n if (!existsSync(imsgDbPath)) {\n items.push({\n key: \"messages-db\",\n label: \"Messages DB\",\n status: \"error\",\n detail: `Missing ${imsgDbPath}`,\n });\n } else {\n try {\n const error = inspectSqlite(imsgDbPath);\n items.push({\n key: \"messages-db\",\n label: \"Messages DB\",\n status: error ? \"error\" : \"ok\",\n detail: error ? classifyDbError(error) : `Readable at ${imsgDbPath}`,\n });\n } catch (error) {\n items.push({\n key: \"messages-db\",\n label: \"Messages DB\",\n status: \"error\",\n detail: classifyDbError(error),\n });\n }\n }\n\n const contactPaths = getContactsDbPaths() ?? [];\n const existingContactPaths = contactPaths.filter((path) => existsSync(path));\n if (existingContactPaths.length === 0) {\n items.push({\n key: \"contacts-db\",\n label: \"Contacts DB\",\n status: \"warn\",\n detail:\n \"No readable Address Book database was found. Message reads still work, but names may stay as raw phone numbers or emails.\",\n });\n } else {\n const failures: string[] = [];\n for (const path of existingContactPaths) {\n try {\n inspectSqlite(path);\n } catch (error) {\n failures.push(`${path}: ${classifyDbError(error)}`);\n }\n }\n items.push({\n key: \"contacts-db\",\n label: \"Contacts DB\",\n status: failures.length === 0 ? \"ok\" : \"warn\",\n detail:\n failures.length === 0\n ? `Readable at ${existingContactPaths.join(\", \")}`\n : `Some contact databases are unreadable. ${failures.join(\" \")}`,\n });\n }\n\n const messagesRunning = await checkMessagesAvailable().catch(() => false);\n items.push({\n key: \"messages-app\",\n label: \"Messages.app\",\n status: messagesRunning ? \"ok\" : \"warn\",\n detail: messagesRunning\n ? \"Messages.app is running.\"\n : \"Messages.app is not running. Reading still works, but sending requires Messages.app to be open.\",\n });\n\n return {\n ok: items.every((item) => item.status !== \"error\"),\n items,\n };\n}\n\nexport function formatAccessReport(report: AccessReport): string {\n const lines = [\"imsg-mcp doctor\", \"\"];\n for (const item of report.items) {\n lines.push(`${iconFor(item.status)} ${item.label}`);\n lines.push(` ${item.detail}`);\n }\n lines.push(\"\");\n lines.push(\n report.ok\n ? \"Environment looks ready for local message reads.\"\n : \"Fix the ERR items above, then rerun `imsg doctor`.\",\n );\n return lines.join(\"\\n\");\n}\n","/**\n * Central shutdown / cleanup registry.\n *\n * Any module can register a cleanup function. On process exit (via signal,\n * stdin EOF, or orphan detection), all registered functions run exactly once\n * before the process exits.\n *\n * Prevents orphaned processes by:\n * 1. Trapping SIGINT, SIGTERM, SIGHUP, SIGQUIT\n * 2. Detecting stdin EOF (parent MCP host died)\n * 3. Watching for parent PID change (reparented to launchd = orphaned)\n */\n\nimport { appendLog } from \"./logger.js\";\n\ntype CleanupFn = () => void | Promise<void>;\n\nconst registry = new Set<CleanupFn>();\nlet shuttingDown = false;\nlet watchdogTimer: ReturnType<typeof setInterval> | null = null;\n\n/**\n * Register a cleanup function to run on shutdown.\n * Functions are called in registration order.\n */\nexport function registerCleanup(fn: CleanupFn): void {\n registry.add(fn);\n}\n\n/**\n * Unregister a previously registered cleanup function.\n */\nexport function unregisterCleanup(fn: CleanupFn): void {\n registry.delete(fn);\n}\n\n/**\n * Trigger graceful shutdown. Runs all cleanup functions, then exits.\n * Safe to call multiple times — only runs once.\n */\nexport async function shutdown(exitCode = 0): Promise<never> {\n if (shuttingDown) {\n // Already shutting down — force exit after 3s safety net\n setTimeout(() => process.exit(exitCode), 3000).unref();\n // Block forever (the timeout or a prior shutdown will exit)\n return new Promise<never>(() => {});\n }\n shuttingDown = true;\n\n // Stop watchdog\n if (watchdogTimer) {\n clearInterval(watchdogTimer);\n watchdogTimer = null;\n }\n\n // Run all cleanup functions (best-effort, don't let one failure block others)\n for (const fn of registry) {\n try {\n await fn();\n } catch {\n // Ignore cleanup errors during shutdown\n }\n }\n registry.clear();\n\n process.exit(exitCode);\n}\n\n/** Synchronous cleanup — last resort on process.on('exit') */\nfunction syncCleanup(): void {\n for (const fn of registry) {\n try {\n // Only call sync-safe functions here; async ones will be no-ops\n const result = fn();\n // If it returns a promise, we can't await it here — just ignore\n if (result && typeof (result as Promise<void>).catch === \"function\") {\n (result as Promise<void>).catch(() => {});\n }\n } catch {\n // Ignore\n }\n }\n}\n\n/**\n * Install signal handlers and orphan detection.\n * Call once at process startup.\n */\nexport function installShutdownHandlers(): void {\n // Signal handlers — log the signal name before cleanup so the post-mortem\n // NDJSON tells us *why* the process ended (e.g. host sent SIGTERM at $time).\n const onSignal = (signal: string) => {\n try {\n appendLog(\"info\", \"signal_received\", { signal });\n } catch {\n // logger may already be torn down; never fail shutdown over logging.\n }\n shutdown(signal === \"SIGINT\" ? 130 : 0);\n };\n\n for (const sig of [\"SIGINT\", \"SIGTERM\", \"SIGHUP\", \"SIGQUIT\"] as const) {\n process.on(sig, () => onSignal(sig));\n }\n\n // Synchronous last-resort cleanup\n process.on(\"exit\", syncCleanup);\n}\n\n/**\n * Enable stdin EOF detection — when the parent process dies, stdin closes.\n * Essential for MCP stdio servers to detect host death.\n */\nexport function enableStdinEofDetection(): void {\n process.stdin.on(\"end\", () => {\n if (!shuttingDown) shutdown(0);\n });\n process.stdin.resume();\n}\n\n/**\n * Enable parent PID watchdog — detects orphaned processes.\n * If the parent PID changes (reparented to launchd/init), trigger shutdown.\n * Timer is unref'd so it doesn't prevent natural exit.\n */\nexport function enableOrphanWatchdog(intervalMs = 5000): void {\n if (watchdogTimer) return;\n const parentPid = process.ppid;\n\n watchdogTimer = setInterval(() => {\n // ppid changes to 1 (launchd/init) when parent dies\n if (process.ppid === 1 || process.ppid !== parentPid) {\n shutdown(0);\n }\n }, intervalMs);\n watchdogTimer.unref();\n}\n\n/**\n * Check if shutdown is in progress.\n */\nexport function isShuttingDown(): boolean {\n return shuttingDown;\n}\n"],"names":["readdirSync","error"],"mappings":"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"}
@@ -0,0 +1,122 @@
1
+ import { existsSync, mkdirSync, writeFileSync, readFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { dirname, join } from "node:path";
4
+ import { z } from "zod";
5
+ const HEX6 = /^#[0-9a-fA-F]{6}$/;
6
+ const TuiConfigSchema = z.object({
7
+ /** Glyph preset. "safe" is the universally-renderable default. */
8
+ theme: z.enum(["safe", "powerline"]).default("safe"),
9
+ /** 6-digit hex color used to derive the whole UI palette. */
10
+ accentColor: z.string().regex(HEX6, "must be a 6-digit hex like #RRGGBB").default("#1982FC")
11
+ });
12
+ const DEFAULT_TUI_CONFIG = {
13
+ theme: "safe",
14
+ accentColor: "#1982FC"
15
+ };
16
+ function candidatePaths() {
17
+ const home = homedir();
18
+ const out = [];
19
+ if (process.env.XDG_CONFIG_HOME) {
20
+ out.push(join(process.env.XDG_CONFIG_HOME, "imsg-mcp", "config.json"));
21
+ }
22
+ out.push(join(home, ".config", "imsg-mcp", "config.json"));
23
+ out.push(join(home, ".imsg-mcp", "config.json"));
24
+ return out;
25
+ }
26
+ function findTuiConfigPath() {
27
+ for (const p of candidatePaths()) {
28
+ if (existsSync(p)) return p;
29
+ }
30
+ return null;
31
+ }
32
+ function defaultTuiConfigPath() {
33
+ return candidatePaths()[0] ?? candidatePaths()[1];
34
+ }
35
+ function loadTuiConfig() {
36
+ const warnings = [];
37
+ const path = findTuiConfigPath();
38
+ if (!path) return { config: DEFAULT_TUI_CONFIG, source: null, warnings };
39
+ try {
40
+ const raw = readFileSync(path, "utf8");
41
+ const parsed = JSON.parse(raw);
42
+ const result = TuiConfigSchema.safeParse(parsed);
43
+ if (!result.success) {
44
+ const issues = result.error.issues.map((i) => `${i.path.join(".") || "<root>"}: ${i.message}`).join("; ");
45
+ warnings.push(`tui-config: ${path} has invalid fields (${issues}); using defaults`);
46
+ return { config: DEFAULT_TUI_CONFIG, source: path, warnings };
47
+ }
48
+ return { config: result.data, source: path, warnings };
49
+ } catch (err) {
50
+ warnings.push(
51
+ `tui-config: failed to read ${path} (${err instanceof Error ? err.message : String(err)}); using defaults`
52
+ );
53
+ return { config: DEFAULT_TUI_CONFIG, source: path, warnings };
54
+ }
55
+ }
56
+ function writeTuiConfig(config, path = defaultTuiConfigPath()) {
57
+ TuiConfigSchema.parse(config);
58
+ mkdirSync(dirname(path), { recursive: true });
59
+ writeFileSync(path, `${JSON.stringify(config, null, 2)}
60
+ `);
61
+ return path;
62
+ }
63
+ function resolveTuiConfig(overrides = {}) {
64
+ const loaded = loadTuiConfig();
65
+ const warnings = [...loaded.warnings];
66
+ let theme = loaded.config.theme;
67
+ let themeOrigin = loaded.source ? "config" : "default";
68
+ if (process.env.IMSG_TUI_THEME) {
69
+ const t = process.env.IMSG_TUI_THEME;
70
+ if (t === "safe" || t === "powerline") {
71
+ theme = t;
72
+ themeOrigin = "env";
73
+ } else {
74
+ warnings.push(`IMSG_TUI_THEME="${t}" is not "safe" or "powerline"; ignoring`);
75
+ }
76
+ }
77
+ if (overrides.cliTheme !== void 0) {
78
+ if (overrides.cliTheme === "safe" || overrides.cliTheme === "powerline") {
79
+ theme = overrides.cliTheme;
80
+ themeOrigin = "cli";
81
+ } else {
82
+ warnings.push(`--theme="${overrides.cliTheme}" is not "safe" or "powerline"; ignoring`);
83
+ }
84
+ }
85
+ let accentColor = loaded.config.accentColor;
86
+ let accentOrigin = loaded.source ? "config" : "default";
87
+ if (process.env.IMSG_TUI_ACCENT) {
88
+ if (HEX6.test(process.env.IMSG_TUI_ACCENT)) {
89
+ accentColor = process.env.IMSG_TUI_ACCENT;
90
+ accentOrigin = "env";
91
+ } else {
92
+ warnings.push(
93
+ `IMSG_TUI_ACCENT="${process.env.IMSG_TUI_ACCENT}" is not a 6-digit hex; ignoring`
94
+ );
95
+ }
96
+ }
97
+ if (overrides.cliAccent !== void 0) {
98
+ if (HEX6.test(overrides.cliAccent)) {
99
+ accentColor = overrides.cliAccent;
100
+ accentOrigin = "cli";
101
+ } else {
102
+ warnings.push(`--accent="${overrides.cliAccent}" is not a 6-digit hex; ignoring`);
103
+ }
104
+ }
105
+ return {
106
+ theme,
107
+ accentColor,
108
+ origin: { theme: themeOrigin, accentColor: accentOrigin },
109
+ configPath: loaded.source,
110
+ warnings
111
+ };
112
+ }
113
+ export {
114
+ DEFAULT_TUI_CONFIG,
115
+ TuiConfigSchema,
116
+ defaultTuiConfigPath,
117
+ findTuiConfigPath,
118
+ loadTuiConfig,
119
+ resolveTuiConfig,
120
+ writeTuiConfig
121
+ };
122
+ //# sourceMappingURL=tui-config-Crn6TZPg.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tui-config-Crn6TZPg.js","sources":["../src/tui-config.ts"],"sourcesContent":["/**\n * Persistent TUI settings — loaded from a JSON file on the filesystem.\n *\n * Resolution order (first one found wins):\n * 1. $XDG_CONFIG_HOME/imsg-mcp/config.json\n * 2. $HOME/.config/imsg-mcp/config.json\n * 3. $HOME/.imsg-mcp/config.json (matches the slugs.db location)\n *\n * If no file exists, defaults are returned and the *write* path is\n * `$HOME/.config/imsg-mcp/config.json` (modern XDG-style location).\n *\n * Schema is validated with Zod. Parse errors → defaults + a stderr\n * warning (we never fail the TUI launch over a malformed config).\n */\n\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { dirname, join } from \"node:path\";\nimport { z } from \"zod\";\n\n// ── Schema ────────────────────────────────────────────────────────────────\n\nconst HEX6 = /^#[0-9a-fA-F]{6}$/;\n\nexport const TuiConfigSchema = z.object({\n /** Glyph preset. \"safe\" is the universally-renderable default. */\n theme: z.enum([\"safe\", \"powerline\"]).default(\"safe\"),\n /** 6-digit hex color used to derive the whole UI palette. */\n accentColor: z.string().regex(HEX6, \"must be a 6-digit hex like #RRGGBB\").default(\"#1982FC\"),\n});\n\nexport type TuiConfig = z.infer<typeof TuiConfigSchema>;\n\nexport const DEFAULT_TUI_CONFIG: TuiConfig = {\n theme: \"safe\",\n accentColor: \"#1982FC\",\n};\n\n// ── Path resolution ───────────────────────────────────────────────────────\n\n/** Candidate paths we try to read from, in order. */\nfunction candidatePaths(): string[] {\n const home = homedir();\n const out: string[] = [];\n if (process.env.XDG_CONFIG_HOME) {\n out.push(join(process.env.XDG_CONFIG_HOME, \"imsg-mcp\", \"config.json\"));\n }\n out.push(join(home, \".config\", \"imsg-mcp\", \"config.json\"));\n out.push(join(home, \".imsg-mcp\", \"config.json\"));\n return out;\n}\n\n/** Where the file actually lives (first existing candidate), or null. */\nexport function findTuiConfigPath(): string | null {\n for (const p of candidatePaths()) {\n if (existsSync(p)) return p;\n }\n return null;\n}\n\n/** Where to write a fresh config file. Returns the *first* candidate so\n * XDG-style wins on a clean machine. */\nexport function defaultTuiConfigPath(): string {\n return candidatePaths()[0] ?? candidatePaths()[1];\n}\n\n// ── Loader ────────────────────────────────────────────────────────────────\n\nexport interface LoadedTuiConfig {\n config: TuiConfig;\n /** Path of the file we read from, or `null` if defaults were used. */\n source: string | null;\n /** Human-readable warnings (parse errors, schema errors). Empty when clean. */\n warnings: string[];\n}\n\nexport function loadTuiConfig(): LoadedTuiConfig {\n const warnings: string[] = [];\n const path = findTuiConfigPath();\n if (!path) return { config: DEFAULT_TUI_CONFIG, source: null, warnings };\n\n try {\n const raw = readFileSync(path, \"utf8\");\n const parsed = JSON.parse(raw);\n const result = TuiConfigSchema.safeParse(parsed);\n if (!result.success) {\n const issues = result.error.issues\n .map((i) => `${i.path.join(\".\") || \"<root>\"}: ${i.message}`)\n .join(\"; \");\n warnings.push(`tui-config: ${path} has invalid fields (${issues}); using defaults`);\n return { config: DEFAULT_TUI_CONFIG, source: path, warnings };\n }\n return { config: result.data, source: path, warnings };\n } catch (err) {\n warnings.push(\n `tui-config: failed to read ${path} (${err instanceof Error ? err.message : String(err)}); using defaults`,\n );\n return { config: DEFAULT_TUI_CONFIG, source: path, warnings };\n }\n}\n\n// ── Writer (used by `imsg config edit` to seed a file if missing) ────────\n\nexport function writeTuiConfig(config: TuiConfig, path = defaultTuiConfigPath()): string {\n // Validate before writing so we never persist garbage.\n TuiConfigSchema.parse(config);\n mkdirSync(dirname(path), { recursive: true });\n writeFileSync(path, `${JSON.stringify(config, null, 2)}\\n`);\n return path;\n}\n\n// ── Layered resolution (CLI > env > config > defaults) ───────────────────\n\nexport interface TuiConfigOverrides {\n /** From `imsg --theme=...` */\n cliTheme?: string;\n /** From `imsg --accent=...` */\n cliAccent?: string;\n}\n\nexport interface ResolvedTuiConfig extends TuiConfig {\n /** Where each value came from, useful for `imsg config show`. */\n origin: {\n theme: \"cli\" | \"env\" | \"config\" | \"default\";\n accentColor: \"cli\" | \"env\" | \"config\" | \"default\";\n };\n /** The on-disk file path that contributed (or `null` if no file). */\n configPath: string | null;\n /** Any warnings raised during loading (malformed file, invalid env, etc). */\n warnings: string[];\n}\n\nexport function resolveTuiConfig(overrides: TuiConfigOverrides = {}): ResolvedTuiConfig {\n const loaded = loadTuiConfig();\n const warnings = [...loaded.warnings];\n\n // Theme precedence: CLI > env > config > default\n let theme: TuiConfig[\"theme\"] = loaded.config.theme;\n let themeOrigin: ResolvedTuiConfig[\"origin\"][\"theme\"] = loaded.source ? \"config\" : \"default\";\n\n if (process.env.IMSG_TUI_THEME) {\n const t = process.env.IMSG_TUI_THEME;\n if (t === \"safe\" || t === \"powerline\") {\n theme = t;\n themeOrigin = \"env\";\n } else {\n warnings.push(`IMSG_TUI_THEME=\"${t}\" is not \"safe\" or \"powerline\"; ignoring`);\n }\n }\n if (overrides.cliTheme !== undefined) {\n if (overrides.cliTheme === \"safe\" || overrides.cliTheme === \"powerline\") {\n theme = overrides.cliTheme;\n themeOrigin = \"cli\";\n } else {\n warnings.push(`--theme=\"${overrides.cliTheme}\" is not \"safe\" or \"powerline\"; ignoring`);\n }\n }\n\n // Accent precedence: CLI > env > config > default\n let accentColor = loaded.config.accentColor;\n let accentOrigin: ResolvedTuiConfig[\"origin\"][\"accentColor\"] = loaded.source\n ? \"config\"\n : \"default\";\n\n if (process.env.IMSG_TUI_ACCENT) {\n if (HEX6.test(process.env.IMSG_TUI_ACCENT)) {\n accentColor = process.env.IMSG_TUI_ACCENT;\n accentOrigin = \"env\";\n } else {\n warnings.push(\n `IMSG_TUI_ACCENT=\"${process.env.IMSG_TUI_ACCENT}\" is not a 6-digit hex; ignoring`,\n );\n }\n }\n if (overrides.cliAccent !== undefined) {\n if (HEX6.test(overrides.cliAccent)) {\n accentColor = overrides.cliAccent;\n accentOrigin = \"cli\";\n } else {\n warnings.push(`--accent=\"${overrides.cliAccent}\" is not a 6-digit hex; ignoring`);\n }\n }\n\n return {\n theme,\n accentColor,\n origin: { theme: themeOrigin, accentColor: accentOrigin },\n configPath: loaded.source,\n warnings,\n };\n}\n"],"names":[],"mappings":"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"}