imsg-mcp 1.19.2 → 1.19.4
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 +14 -0
- package/dist/{app-config-Bbm63BAU.js → app-config-BHA2RWD5.js} +2 -2
- package/dist/{app-config-Bbm63BAU.js.map → app-config-BHA2RWD5.js.map} +1 -1
- package/dist/{attachment-sync-CgRSN-_2.js → attachment-sync-CIoN37ih.js} +2 -2
- package/dist/{attachment-sync-CgRSN-_2.js.map → attachment-sync-CIoN37ih.js.map} +1 -1
- package/dist/cli.js +14 -14
- package/dist/{exportStream-BS14zWPF.js → exportStream-Bn0Ir89w.js} +2 -2
- package/dist/{exportStream-BS14zWPF.js.map → exportStream-Bn0Ir89w.js.map} +1 -1
- package/dist/{imessage-db-vk35lIQ9.js → imessage-db-BTy3p2Qk.js} +5 -5
- package/dist/imessage-db-BTy3p2Qk.js.map +1 -0
- package/dist/index-D8A4ss3o.js.map +1 -1
- package/dist/index.js +7 -7
- package/dist/index.js.map +1 -1
- package/dist/{media-intel-cache-BsLo2vHg.js → media-intel-cache-E3ejWhWo.js} +2 -2
- package/dist/{media-intel-cache-BsLo2vHg.js.map → media-intel-cache-E3ejWhWo.js.map} +1 -1
- package/dist/{media-intel-runtime-CduJ-yfL.js → media-intel-runtime-DwG4nhXt.js} +3 -3
- package/dist/{media-intel-runtime-CduJ-yfL.js.map → media-intel-runtime-DwG4nhXt.js.map} +1 -1
- package/dist/{setup-p_EQU1Xr.js → setup-BRZC_tgU.js} +2 -2
- package/dist/{setup-p_EQU1Xr.js.map → setup-BRZC_tgU.js.map} +1 -1
- package/dist/{setup-wizard-CHkl-ho5.js → setup-wizard-CM_3dRi_.js} +3 -3
- package/dist/{setup-wizard-CHkl-ho5.js.map → setup-wizard-CM_3dRi_.js.map} +1 -1
- package/dist/{shutdown-DvAA20z4.js → shutdown-CWS6IvCE.js} +515 -196
- package/dist/shutdown-CWS6IvCE.js.map +1 -0
- package/dist/{tui-config-By80ingt.js → tui-config-BPDDTKNp.js} +3 -3
- package/dist/tui-config-BPDDTKNp.js.map +1 -0
- package/dist/tui.js +136 -115
- package/dist/tui.js.map +1 -1
- package/dist/watchdog-bq8BYT-s.js +528 -0
- package/dist/watchdog-bq8BYT-s.js.map +1 -0
- package/native/imsg-native.darwin-arm64.node +0 -0
- package/package.json +62 -60
- package/dist/imessage-db-vk35lIQ9.js.map +0 -1
- package/dist/shutdown-DvAA20z4.js.map +0 -1
- package/dist/tui-config-By80ingt.js.map +0 -1
- package/dist/watchdog-DshveVDU.js +0 -368
- package/dist/watchdog-DshveVDU.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,20 @@
|
|
|
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
|
+
## imsg-mcp [1.19.4](https://github.com/george43g/EQStack/compare/imsg-mcp-v1.19.3...imsg-mcp-v1.19.4) (2026-08-09)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Bug Fixes
|
|
10
|
+
|
|
11
|
+
* **imsg-mcp:** redact logs by default via shared logger facade ([bbacf60](https://github.com/george43g/EQStack/commit/bbacf60168fa5ab21d081f30655d90a9e533ba39))
|
|
12
|
+
|
|
13
|
+
## imsg-mcp [1.19.3](https://github.com/george43g/EQStack/compare/imsg-mcp-v1.19.2...imsg-mcp-v1.19.3) (2026-08-09)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
### Bug Fixes
|
|
17
|
+
|
|
18
|
+
* **imsg-mcp:** scrub real-message fragments from native parser comments and fixtures ([40f99b4](https://github.com/george43g/EQStack/commit/40f99b47e8337c552073f77943312ad3e50d8586))
|
|
19
|
+
|
|
6
20
|
## [1.19.2](https://github.com/george43g/imsg-mcp/compare/v1.19.1...v1.19.2) (2026-08-02)
|
|
7
21
|
|
|
8
22
|
|
|
@@ -2,7 +2,7 @@ import { existsSync, mkdirSync, writeFileSync, readFileSync, chmodSync } from "n
|
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
4
|
import { z } from "zod";
|
|
5
|
-
import { c as getTranscribeCloudConfig } from "./shutdown-
|
|
5
|
+
import { c as getTranscribeCloudConfig } from "./shutdown-CWS6IvCE.js";
|
|
6
6
|
const HEX6 = /^#[0-9a-fA-F]{6}$/;
|
|
7
7
|
const PROVIDER_PRESET_NAMES = [
|
|
8
8
|
"openai",
|
|
@@ -248,4 +248,4 @@ export {
|
|
|
248
248
|
writeCredentials,
|
|
249
249
|
writeTuiConfig
|
|
250
250
|
};
|
|
251
|
-
//# sourceMappingURL=app-config-
|
|
251
|
+
//# sourceMappingURL=app-config-BHA2RWD5.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app-config-Bbm63BAU.js","sources":["../src/app-config.ts"],"sourcesContent":["/**\n * Canonical persistent configuration for imsg-mcp — one JSON file that carries\n * both the flat TUI keys (theme / accentColor) AND the `interpret` block that\n * configures media interpretation (Stage 2 core: chains, providers, toggles).\n *\n * Resolution order (first one found wins) — unchanged from the original\n * tui-config module so existing files keep loading:\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 * `src/tui-config.ts` re-exports this module verbatim for back-compat.\n *\n * API keys are NEVER stored in config.json. They live in\n * `~/.imsg-mcp/credentials.json` (chmod 600, `{ \"<providerName>\": \"sk-…\" }`)\n * or the legacy `IMSG_TRANSCRIBE_*` env vars, and are merged into providers only\n * at resolution time (`resolveInterpretConfig`).\n */\n\nimport { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { dirname, join } from \"node:path\";\nimport { z } from \"zod\";\nimport { getTranscribeCloudConfig } from \"./config.js\";\nimport type { AutoMode, InterpretChains, InterpretConfig } from \"./media-intel.js\";\nimport type { ProviderConfig, ProviderPreset } from \"./media-providers.js\";\n\n// ── Schema ────────────────────────────────────────────────────────────────\n\nconst HEX6 = /^#[0-9a-fA-F]{6}$/;\n\n/** Preset names accepted in the `interpret.providers[].preset` field. Kept as a\n * literal tuple so Zod can build an enum without a runtime import of the\n * providers module (and asserted to stay in sync with `ProviderPreset`). */\nconst PROVIDER_PRESET_NAMES = [\n \"openai\",\n \"groq\",\n \"openrouter\",\n \"cloudflare\",\n \"huggingface\",\n \"ollama\",\n] as const satisfies readonly ProviderPreset[];\n\nconst ProviderModelsSchema = z\n .object({ transcribe: z.string().optional(), vision: z.string().optional() })\n .optional();\n\n/** A provider profile as stored in config.json — NO apiKey (that lives in\n * credentials.json / env and is merged in at resolution time). */\nconst ProviderConfigSchema = z\n .object({\n name: z.string().min(1),\n preset: z.enum(PROVIDER_PRESET_NAMES).optional(),\n baseUrl: z.string().url().optional(),\n accountId: z.string().optional(),\n models: ProviderModelsSchema,\n })\n .refine((p) => Boolean(p.preset) || Boolean(p.baseUrl), {\n message: \"provider needs either a preset or a baseUrl\",\n });\n\nconst ChainsSchema = z\n .object({\n audio: z.array(z.string()).default([\"apple\", \"local\"]),\n image: z.array(z.string()).default([]),\n video: z.array(z.string()).default([]),\n })\n .default({});\n\nconst NudgeSchema = z\n .object({\n enabled: z.boolean().default(true),\n tier2SyncNow: z.boolean().default(false),\n timeoutSeconds: z.number().int().positive().default(30),\n })\n .default({});\n\nexport const InterpretConfigSchema = z.object({\n /** Auto-interpretation gate. \"free\" (default) never makes a paid call without\n * an explicit force; \"all\" runs the full chain incl. cloud; \"off\" disables. */\n auto: z.enum([\"all\", \"free\", \"off\"]).default(\"free\"),\n /** Inline cached transcripts/captions in read surfaces (get_messages, TUI). */\n inlineTranscripts: z.boolean().default(true),\n /** Confirm before an export triggers more than this many uncached cloud calls. */\n exportConfirmThreshold: z.number().int().nonnegative().default(25),\n chains: ChainsSchema,\n providers: z.array(ProviderConfigSchema).default([]),\n nudge: NudgeSchema,\n});\n\nexport const AppConfigSchema = 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 /** Media-interpretation config (optional — absent on TUI-only configs). */\n interpret: InterpretConfigSchema.optional(),\n});\n\nexport type AppConfig = z.infer<typeof AppConfigSchema>;\nexport type InterpretConfigInput = z.infer<typeof InterpretConfigSchema>;\n\n/** Back-compat alias — the TUI only ever reads theme/accentColor. */\nexport const TuiConfigSchema = AppConfigSchema;\nexport type TuiConfig = AppConfig;\n\nexport const DEFAULT_TUI_CONFIG: TuiConfig = {\n theme: \"safe\",\n accentColor: \"#1982FC\",\n};\n\n/** Free-first interpret defaults used when the block is absent from config. */\nexport const DEFAULT_INTERPRET_CONFIG: InterpretConfigInput = InterpretConfigSchema.parse({});\n\n// ── Path resolution ───────────────────────────────────────────────────────\n\n/** Candidate paths we try to read from, in order. Per the XDG spec, `~/.config`\n * is the DEFAULT VALUE of `$XDG_CONFIG_HOME` — not an additional search path —\n * so when the env var is set we must not fall through to the real `~/.config`\n * (that fall-through also let a dev machine's real config leak into tests that\n * point XDG at a temp dir). */\nfunction candidatePaths(): string[] {\n const home = homedir();\n const xdgBase = process.env.XDG_CONFIG_HOME || join(home, \".config\");\n return [join(xdgBase, \"imsg-mcp\", \"config.json\"), join(home, \".imsg-mcp\", \"config.json\")];\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: AppConfig;\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 let parsed: unknown;\n try {\n parsed = JSON.parse(readFileSync(path, \"utf8\"));\n } catch (err) {\n warnings.push(\n `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 const result = AppConfigSchema.safeParse(parsed);\n if (result.success) return { config: result.data, source: path, warnings };\n\n // Full parse failed. Degrade gracefully: if the flat TUI keys alone are valid,\n // keep them (a malformed `interpret` block should not blow away the theme).\n const tuiOnly = AppConfigSchema.pick({ theme: true, accentColor: true }).safeParse(parsed);\n const issues = result.error.issues\n .map((i) => `${i.path.join(\".\") || \"<root>\"}: ${i.message}`)\n .join(\"; \");\n if (tuiOnly.success) {\n warnings.push(`config: ${path} has invalid fields (${issues}); ignoring them`);\n return { config: { ...tuiOnly.data }, source: path, warnings };\n }\n warnings.push(`config: ${path} has invalid fields (${issues}); using defaults`);\n return { config: { ...DEFAULT_TUI_CONFIG }, source: path, warnings };\n}\n\n// ── Writer (used by `imsg config edit` + `imsg setup`) ────────────────────\n\nexport function writeTuiConfig(config: AppConfig, path = defaultTuiConfigPath()): string {\n // Validate before writing so we never persist garbage.\n const validated = AppConfigSchema.parse(config);\n mkdirSync(dirname(path), { recursive: true });\n writeFileSync(path, `${JSON.stringify(validated, null, 2)}\\n`);\n return path;\n}\n\n// ── Credentials (chmod 600) ───────────────────────────────────────────────\n\n/** `~/.imsg-mcp/credentials.json` — `{ \"<providerName>\": \"<apiKey>\" }`. */\nexport function credentialsPath(): string {\n return join(homedir(), \".imsg-mcp\", \"credentials.json\");\n}\n\nexport function readCredentials(): Record<string, string> {\n const path = credentialsPath();\n if (!existsSync(path)) return {};\n try {\n const parsed = JSON.parse(readFileSync(path, \"utf8\"));\n if (parsed && typeof parsed === \"object\" && !Array.isArray(parsed)) {\n const out: Record<string, string> = {};\n for (const [k, v] of Object.entries(parsed)) {\n if (typeof v === \"string\") out[k] = v;\n }\n return out;\n }\n } catch {\n // Corrupt credentials file — treat as empty (never throw on read).\n }\n return {};\n}\n\n/** Write the whole credentials map, enforcing 0600 on the file (and 0700 dir). */\nexport function writeCredentials(creds: Record<string, string>): string {\n const path = credentialsPath();\n const dir = dirname(path);\n mkdirSync(dir, { recursive: true });\n try {\n chmodSync(dir, 0o700);\n } catch {\n // Best-effort — a shared dir may already exist with other perms.\n }\n writeFileSync(path, `${JSON.stringify(creds, null, 2)}\\n`, { mode: 0o600 });\n // writeFileSync's `mode` is masked by umask on CREATE and ignored when the\n // file already exists — chmod unconditionally so 0600 is guaranteed.\n chmodSync(path, 0o600);\n return path;\n}\n\n/** Set (or clear, when key is empty) one provider's credential. */\nexport function setCredential(name: string, key: string): void {\n const creds = readCredentials();\n if (key) creds[name] = key;\n else delete creds[name];\n writeCredentials(creds);\n}\n\n// ── Interpret-config resolution (config + credentials + env → service) ─────\n\n/** The env-mapped implicit provider name (legacy IMSG_TRANSCRIBE_* path). */\nexport const ENV_PROVIDER_NAME = \"env\";\n\n/** Interpret config ready for `MediaIntelService`, plus the surfacing toggles\n * that frontends (Stage 4/6/7) read. Providers carry their merged apiKey. */\nexport interface ResolvedInterpretConfig extends InterpretConfig {\n inlineTranscripts: boolean;\n exportConfirmThreshold: number;\n nudge: { enabled: boolean; tier2SyncNow: boolean; timeoutSeconds: number };\n /** Where the config was read from (null = defaults). */\n configPath: string | null;\n warnings: string[];\n}\n\n/**\n * Resolve the full interpret configuration:\n * - start from the config file's `interpret` block (or free-first defaults),\n * - merge API keys from credentials.json into each named provider,\n * - map the legacy `IMSG_TRANSCRIBE_*` env vars to an implicit `env` provider\n * (and append `provider:env` to the audio chain if it has no provider yet),\n * preserving the shipped v1.7.0 cloud-transcription fallback.\n */\nexport function resolveInterpretConfig(): ResolvedInterpretConfig {\n const loaded = loadTuiConfig();\n const warnings = [...loaded.warnings];\n const block = InterpretConfigSchema.parse(loaded.config.interpret ?? {});\n const creds = readCredentials();\n\n const providers: ProviderConfig[] = block.providers.map((p) => ({\n ...p,\n apiKey: creds[p.name],\n }));\n const chains: InterpretChains = {\n audio: [...block.chains.audio],\n image: [...block.chains.image],\n video: [...block.chains.video],\n };\n\n // Legacy env fallback → implicit provider, if not already configured by name.\n const envCloud = getTranscribeCloudConfig();\n if (envCloud && !providers.some((p) => p.name === ENV_PROVIDER_NAME)) {\n providers.push({\n name: ENV_PROVIDER_NAME,\n baseUrl: envCloud.baseUrl,\n apiKey: envCloud.apiKey,\n models: { transcribe: envCloud.model },\n });\n if (!chains.audio.some((l) => l.startsWith(\"provider:\"))) {\n chains.audio.push(`provider:${ENV_PROVIDER_NAME}`);\n }\n }\n\n const auto: AutoMode = block.auto;\n return {\n auto,\n chains,\n providers,\n inlineTranscripts: block.inlineTranscripts,\n exportConfirmThreshold: block.exportConfirmThreshold,\n nudge: block.nudge,\n configPath: loaded.source,\n warnings,\n };\n}\n\n// ── Layered resolution (CLI > env > config > defaults) — TUI theme/accent ──\n\nexport interface TuiConfigOverrides {\n /** From `imsg --theme=...` */\n cliTheme?: string;\n /** From `imsg --accent=...` */\n cliAccent?: string;\n}\n\nexport interface ResolvedTuiConfig {\n theme: TuiConfig[\"theme\"];\n accentColor: string;\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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"}
|
|
1
|
+
{"version":3,"file":"app-config-BHA2RWD5.js","sources":["../src/app-config.ts"],"sourcesContent":["/**\n * Canonical persistent configuration for imsg-mcp — one JSON file that carries\n * both the flat TUI keys (theme / accentColor) AND the `interpret` block that\n * configures media interpretation (Stage 2 core: chains, providers, toggles).\n *\n * Resolution order (first one found wins) — unchanged from the original\n * tui-config module so existing files keep loading:\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 * `src/tui-config.ts` re-exports this module verbatim for back-compat.\n *\n * API keys are NEVER stored in config.json. They live in\n * `~/.imsg-mcp/credentials.json` (chmod 600, `{ \"<providerName>\": \"sk-…\" }`)\n * or the legacy `IMSG_TRANSCRIBE_*` env vars, and are merged into providers only\n * at resolution time (`resolveInterpretConfig`).\n */\n\nimport { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { dirname, join } from \"node:path\";\nimport { z } from \"zod\";\nimport { getTranscribeCloudConfig } from \"./config.js\";\nimport type { AutoMode, InterpretChains, InterpretConfig } from \"./media-intel.js\";\nimport type { ProviderConfig, ProviderPreset } from \"./media-providers.js\";\n\n// ── Schema ────────────────────────────────────────────────────────────────\n\nconst HEX6 = /^#[0-9a-fA-F]{6}$/;\n\n/** Preset names accepted in the `interpret.providers[].preset` field. Kept as a\n * literal tuple so Zod can build an enum without a runtime import of the\n * providers module (and asserted to stay in sync with `ProviderPreset`). */\nconst PROVIDER_PRESET_NAMES = [\n \"openai\",\n \"groq\",\n \"openrouter\",\n \"cloudflare\",\n \"huggingface\",\n \"ollama\",\n] as const satisfies readonly ProviderPreset[];\n\nconst ProviderModelsSchema = z\n .object({ transcribe: z.string().optional(), vision: z.string().optional() })\n .optional();\n\n/** A provider profile as stored in config.json — NO apiKey (that lives in\n * credentials.json / env and is merged in at resolution time). */\nconst ProviderConfigSchema = z\n .object({\n name: z.string().min(1),\n preset: z.enum(PROVIDER_PRESET_NAMES).optional(),\n baseUrl: z.string().url().optional(),\n accountId: z.string().optional(),\n models: ProviderModelsSchema,\n })\n .refine((p) => Boolean(p.preset) || Boolean(p.baseUrl), {\n message: \"provider needs either a preset or a baseUrl\",\n });\n\nconst ChainsSchema = z\n .object({\n audio: z.array(z.string()).default([\"apple\", \"local\"]),\n image: z.array(z.string()).default([]),\n video: z.array(z.string()).default([]),\n })\n .default({});\n\nconst NudgeSchema = z\n .object({\n enabled: z.boolean().default(true),\n tier2SyncNow: z.boolean().default(false),\n timeoutSeconds: z.number().int().positive().default(30),\n })\n .default({});\n\nexport const InterpretConfigSchema = z.object({\n /** Auto-interpretation gate. \"free\" (default) never makes a paid call without\n * an explicit force; \"all\" runs the full chain incl. cloud; \"off\" disables. */\n auto: z.enum([\"all\", \"free\", \"off\"]).default(\"free\"),\n /** Inline cached transcripts/captions in read surfaces (get_messages, TUI). */\n inlineTranscripts: z.boolean().default(true),\n /** Confirm before an export triggers more than this many uncached cloud calls. */\n exportConfirmThreshold: z.number().int().nonnegative().default(25),\n chains: ChainsSchema,\n providers: z.array(ProviderConfigSchema).default([]),\n nudge: NudgeSchema,\n});\n\nexport const AppConfigSchema = 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 /** Media-interpretation config (optional — absent on TUI-only configs). */\n interpret: InterpretConfigSchema.optional(),\n});\n\nexport type AppConfig = z.infer<typeof AppConfigSchema>;\nexport type InterpretConfigInput = z.infer<typeof InterpretConfigSchema>;\n\n/** Back-compat alias — the TUI only ever reads theme/accentColor. */\nexport const TuiConfigSchema = AppConfigSchema;\nexport type TuiConfig = AppConfig;\n\nexport const DEFAULT_TUI_CONFIG: TuiConfig = {\n theme: \"safe\",\n accentColor: \"#1982FC\",\n};\n\n/** Free-first interpret defaults used when the block is absent from config. */\nexport const DEFAULT_INTERPRET_CONFIG: InterpretConfigInput = InterpretConfigSchema.parse({});\n\n// ── Path resolution ───────────────────────────────────────────────────────\n\n/** Candidate paths we try to read from, in order. Per the XDG spec, `~/.config`\n * is the DEFAULT VALUE of `$XDG_CONFIG_HOME` — not an additional search path —\n * so when the env var is set we must not fall through to the real `~/.config`\n * (that fall-through also let a dev machine's real config leak into tests that\n * point XDG at a temp dir). */\nfunction candidatePaths(): string[] {\n const home = homedir();\n const xdgBase = process.env.XDG_CONFIG_HOME || join(home, \".config\");\n return [join(xdgBase, \"imsg-mcp\", \"config.json\"), join(home, \".imsg-mcp\", \"config.json\")];\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: AppConfig;\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 let parsed: unknown;\n try {\n parsed = JSON.parse(readFileSync(path, \"utf8\"));\n } catch (err) {\n warnings.push(\n `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 const result = AppConfigSchema.safeParse(parsed);\n if (result.success) return { config: result.data, source: path, warnings };\n\n // Full parse failed. Degrade gracefully: if the flat TUI keys alone are valid,\n // keep them (a malformed `interpret` block should not blow away the theme).\n const tuiOnly = AppConfigSchema.pick({ theme: true, accentColor: true }).safeParse(parsed);\n const issues = result.error.issues\n .map((i) => `${i.path.join(\".\") || \"<root>\"}: ${i.message}`)\n .join(\"; \");\n if (tuiOnly.success) {\n warnings.push(`config: ${path} has invalid fields (${issues}); ignoring them`);\n return { config: { ...tuiOnly.data }, source: path, warnings };\n }\n warnings.push(`config: ${path} has invalid fields (${issues}); using defaults`);\n return { config: { ...DEFAULT_TUI_CONFIG }, source: path, warnings };\n}\n\n// ── Writer (used by `imsg config edit` + `imsg setup`) ────────────────────\n\nexport function writeTuiConfig(config: AppConfig, path = defaultTuiConfigPath()): string {\n // Validate before writing so we never persist garbage.\n const validated = AppConfigSchema.parse(config);\n mkdirSync(dirname(path), { recursive: true });\n writeFileSync(path, `${JSON.stringify(validated, null, 2)}\\n`);\n return path;\n}\n\n// ── Credentials (chmod 600) ───────────────────────────────────────────────\n\n/** `~/.imsg-mcp/credentials.json` — `{ \"<providerName>\": \"<apiKey>\" }`. */\nexport function credentialsPath(): string {\n return join(homedir(), \".imsg-mcp\", \"credentials.json\");\n}\n\nexport function readCredentials(): Record<string, string> {\n const path = credentialsPath();\n if (!existsSync(path)) return {};\n try {\n const parsed = JSON.parse(readFileSync(path, \"utf8\"));\n if (parsed && typeof parsed === \"object\" && !Array.isArray(parsed)) {\n const out: Record<string, string> = {};\n for (const [k, v] of Object.entries(parsed)) {\n if (typeof v === \"string\") out[k] = v;\n }\n return out;\n }\n } catch {\n // Corrupt credentials file — treat as empty (never throw on read).\n }\n return {};\n}\n\n/** Write the whole credentials map, enforcing 0600 on the file (and 0700 dir). */\nexport function writeCredentials(creds: Record<string, string>): string {\n const path = credentialsPath();\n const dir = dirname(path);\n mkdirSync(dir, { recursive: true });\n try {\n chmodSync(dir, 0o700);\n } catch {\n // Best-effort — a shared dir may already exist with other perms.\n }\n writeFileSync(path, `${JSON.stringify(creds, null, 2)}\\n`, { mode: 0o600 });\n // writeFileSync's `mode` is masked by umask on CREATE and ignored when the\n // file already exists — chmod unconditionally so 0600 is guaranteed.\n chmodSync(path, 0o600);\n return path;\n}\n\n/** Set (or clear, when key is empty) one provider's credential. */\nexport function setCredential(name: string, key: string): void {\n const creds = readCredentials();\n if (key) creds[name] = key;\n else delete creds[name];\n writeCredentials(creds);\n}\n\n// ── Interpret-config resolution (config + credentials + env → service) ─────\n\n/** The env-mapped implicit provider name (legacy IMSG_TRANSCRIBE_* path). */\nexport const ENV_PROVIDER_NAME = \"env\";\n\n/** Interpret config ready for `MediaIntelService`, plus the surfacing toggles\n * that frontends (Stage 4/6/7) read. Providers carry their merged apiKey. */\nexport interface ResolvedInterpretConfig extends InterpretConfig {\n inlineTranscripts: boolean;\n exportConfirmThreshold: number;\n nudge: { enabled: boolean; tier2SyncNow: boolean; timeoutSeconds: number };\n /** Where the config was read from (null = defaults). */\n configPath: string | null;\n warnings: string[];\n}\n\n/**\n * Resolve the full interpret configuration:\n * - start from the config file's `interpret` block (or free-first defaults),\n * - merge API keys from credentials.json into each named provider,\n * - map the legacy `IMSG_TRANSCRIBE_*` env vars to an implicit `env` provider\n * (and append `provider:env` to the audio chain if it has no provider yet),\n * preserving the shipped v1.7.0 cloud-transcription fallback.\n */\nexport function resolveInterpretConfig(): ResolvedInterpretConfig {\n const loaded = loadTuiConfig();\n const warnings = [...loaded.warnings];\n const block = InterpretConfigSchema.parse(loaded.config.interpret ?? {});\n const creds = readCredentials();\n\n const providers: ProviderConfig[] = block.providers.map((p) => ({\n ...p,\n apiKey: creds[p.name],\n }));\n const chains: InterpretChains = {\n audio: [...block.chains.audio],\n image: [...block.chains.image],\n video: [...block.chains.video],\n };\n\n // Legacy env fallback → implicit provider, if not already configured by name.\n const envCloud = getTranscribeCloudConfig();\n if (envCloud && !providers.some((p) => p.name === ENV_PROVIDER_NAME)) {\n providers.push({\n name: ENV_PROVIDER_NAME,\n baseUrl: envCloud.baseUrl,\n apiKey: envCloud.apiKey,\n models: { transcribe: envCloud.model },\n });\n if (!chains.audio.some((l) => l.startsWith(\"provider:\"))) {\n chains.audio.push(`provider:${ENV_PROVIDER_NAME}`);\n }\n }\n\n const auto: AutoMode = block.auto;\n return {\n auto,\n chains,\n providers,\n inlineTranscripts: block.inlineTranscripts,\n exportConfirmThreshold: block.exportConfirmThreshold,\n nudge: block.nudge,\n configPath: loaded.source,\n warnings,\n };\n}\n\n// ── Layered resolution (CLI > env > config > defaults) — TUI theme/accent ──\n\nexport interface TuiConfigOverrides {\n /** From `imsg --theme=...` */\n cliTheme?: string;\n /** From `imsg --accent=...` */\n cliAccent?: string;\n}\n\nexport interface ResolvedTuiConfig {\n theme: TuiConfig[\"theme\"];\n accentColor: string;\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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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
|
-
import { s as syncNowViaSystemEvents, o as openConversationInMessages } from "./shutdown-
|
|
2
|
+
import { s as syncNowViaSystemEvents, o as openConversationInMessages } from "./shutdown-CWS6IvCE.js";
|
|
3
3
|
function defaultAttachmentSyncDeps() {
|
|
4
4
|
return {
|
|
5
5
|
fileExists: (p) => existsSync(p),
|
|
@@ -76,4 +76,4 @@ export {
|
|
|
76
76
|
defaultAttachmentSyncDeps,
|
|
77
77
|
ensureAttachmentDownloaded
|
|
78
78
|
};
|
|
79
|
-
//# sourceMappingURL=attachment-sync-
|
|
79
|
+
//# sourceMappingURL=attachment-sync-CIoN37ih.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"attachment-sync-
|
|
1
|
+
{"version":3,"file":"attachment-sync-CIoN37ih.js","sources":["../src/attachment-sync.ts"],"sourcesContent":["/**\n * Attachment sync nudge (Stage 7) — best-effort, tiered coaxing of Messages.app\n * into downloading an attachment whose bytes aren't on disk yet\n * (`transfer_state === -1`, purged/never-downloaded). iCloud syncs Messages\n * media only while a conversation is OPEN, and there is no public download API\n * (IMTransferAgent is a private SIP'd daemon). So we:\n *\n * Tier 1 (default): open the conversation in Messages via an `imessage:` URL\n * — community-verified to make the app pull that thread's media — then poll\n * `existsSync` until the file appears or a timeout elapses.\n * Tier 2 (opt-in, `nudge.tier2SyncNow`): UI-script Messages ▸ Settings ▸\n * iMessage ▸ \"Sync Now\". Needs Accessibility permission; we detect the\n * missing-permission error and return an actionable hint.\n * Tier 3: per-conversation \"download all\" UI script — documented only\n * (too fragile across macOS versions), no code here.\n *\n * This module is PURE ORCHESTRATION: every side effect (AppleScript, fs, timers)\n * is injected via {@link AttachmentSyncDeps}, so the tier/poll/timeout logic is\n * unit-tested deterministically with no real app launch and no real clock. The\n * default deps wire the real AppleScript primitives (themselves mocked under\n * Vitest) + node fs/timers.\n */\nimport { existsSync } from \"node:fs\";\nimport {\n openConversationInMessages,\n type SyncNowOutcome,\n syncNowViaSystemEvents,\n} from \"./applescript.js\";\n\nexport type { SyncNowOutcome };\n\n/** The subset of the resolved `nudge` config this module needs. */\nexport interface AttachmentSyncConfig {\n /** Master switch — when false, no tier runs and we never touch Messages. */\n enabled: boolean;\n /** Enable Tier 2 (System Events \"Sync Now\"). Default off. */\n tier2SyncNow: boolean;\n /** How long to wait for the file to appear after a nudge, in seconds. */\n timeoutSeconds: number;\n /** Poll cadence while waiting. Default 1000ms; floored at 100ms. */\n pollIntervalMs?: number;\n}\n\n/** Highest tier that actually ran (0 = none). */\nexport type SyncTier = 0 | 1 | 2;\n\nexport interface EnsureDownloadedInput {\n /** Absolute path (already `~`-expanded) we expect the file to appear at. */\n filePath: string;\n /** Chat identifier (handle or GUID) to open in Messages for Tier 1. */\n chatId?: string | null;\n /** `attachment.transfer_state` from chat.db (-1 = undownloaded); advisory. */\n transferState?: number | null;\n}\n\nexport interface EnsureDownloadedResult {\n /** True iff the file is on disk when we return. */\n downloaded: boolean;\n /** True iff we actually ran at least one tier (vs short-circuited). */\n attempted: boolean;\n /** Highest tier attempted. */\n tier: SyncTier;\n /** Machine-readable reason when not downloaded / skipped. */\n reason?: string;\n /** Human-actionable remediation (e.g. grant Accessibility). */\n hint?: string;\n}\n\n/** All side effects the orchestrator needs, injectable for tests. */\nexport interface AttachmentSyncDeps {\n fileExists(path: string): boolean;\n openConversation(chatId: string): Promise<void>;\n syncNow(): Promise<SyncNowOutcome>;\n sleep(ms: number): Promise<void>;\n now(): number;\n}\n\n/** Production deps: real fs + timers + AppleScript (AppleScript self-mocks under Vitest). */\nexport function defaultAttachmentSyncDeps(): AttachmentSyncDeps {\n return {\n fileExists: (p) => existsSync(p),\n openConversation: (chatId) => openConversationInMessages(chatId),\n syncNow: () => syncNowViaSystemEvents(),\n sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),\n now: () => Date.now(),\n };\n}\n\nfunction errMsg(e: unknown): string {\n return e instanceof Error ? e.message : String(e);\n}\n\n/**\n * Poll `fileExists` until it's true or `timeoutMs` elapses (measured against\n * `deps.now()`). Checks immediately first — opening the chat may have already\n * pulled the file — then sleeps `pollIntervalMs` between checks.\n */\nasync function pollForFile(\n deps: AttachmentSyncDeps,\n path: string,\n timeoutMs: number,\n pollIntervalMs: number,\n): Promise<boolean> {\n const start = deps.now();\n if (deps.fileExists(path)) return true;\n while (deps.now() - start < timeoutMs) {\n await deps.sleep(pollIntervalMs);\n if (deps.fileExists(path)) return true;\n }\n return false;\n}\n\n/**\n * Ensure `input.filePath` is on disk, nudging Messages to download it if not.\n * Best-effort: returns a structured result rather than throwing. Idempotent and\n * cheap when the file already exists (no side effects) or when disabled.\n */\nexport async function ensureAttachmentDownloaded(\n input: EnsureDownloadedInput,\n config: AttachmentSyncConfig,\n deps: AttachmentSyncDeps = defaultAttachmentSyncDeps(),\n): Promise<EnsureDownloadedResult> {\n const { filePath } = input;\n\n // Already on disk → nothing to do (no app launch).\n if (deps.fileExists(filePath)) {\n return { downloaded: true, attempted: false, tier: 0 };\n }\n if (!config.enabled) {\n return { downloaded: false, attempted: false, tier: 0, reason: \"sync nudge disabled\" };\n }\n\n const pollIntervalMs = Math.max(100, config.pollIntervalMs ?? 1000);\n const timeoutMs = Math.max(1000, config.timeoutSeconds * 1000);\n let tier: SyncTier = 0;\n let hint: string | undefined;\n\n // ── Tier 1: open the conversation so Messages pulls its media ──\n if (input.chatId) {\n tier = 1;\n try {\n await deps.openConversation(input.chatId);\n } catch (e) {\n hint = `Could not open the conversation in Messages: ${errMsg(e)}`;\n }\n if (await pollForFile(deps, filePath, timeoutMs, pollIntervalMs)) {\n return { downloaded: true, attempted: true, tier };\n }\n } else {\n hint = \"No chat identifier available to open the conversation in Messages.\";\n }\n\n // ── Tier 2 (opt-in): System Events \"Sync Now\" ──\n if (config.tier2SyncNow) {\n tier = 2;\n const outcome = await deps.syncNow();\n if (!outcome.ok) {\n if (outcome.needsAccessibility) {\n return {\n downloaded: deps.fileExists(filePath),\n attempted: true,\n tier,\n reason: \"accessibility-permission-required\",\n hint: \"Sync Now needs Accessibility permission. Grant it in System Settings ▸ Privacy & Security ▸ Accessibility for your terminal/IDE, then retry.\",\n };\n }\n hint = outcome.error ?? hint ?? \"Sync Now failed.\";\n } else if (await pollForFile(deps, filePath, timeoutMs, pollIntervalMs)) {\n return { downloaded: true, attempted: true, tier };\n }\n }\n\n return {\n downloaded: deps.fileExists(filePath),\n attempted: tier > 0,\n tier,\n reason: tier === 0 ? \"no sync tier could run\" : \"attachment did not download in time\",\n hint,\n };\n}\n"],"names":[],"mappings":";;AA8EO,SAAS,4BAAgD;AAC9D,SAAO;AAAA,IACL,YAAY,CAAC,MAAM,WAAW,CAAC;AAAA,IAC/B,kBAAkB,CAAC,WAAW,2BAA2B,MAAM;AAAA,IAC/D,SAAS,MAAM,uBAAA;AAAA,IACf,OAAO,CAAC,OAAO,IAAI,QAAQ,CAAC,YAAY,WAAW,SAAS,EAAE,CAAC;AAAA,IAC/D,KAAK,MAAM,KAAK,IAAA;AAAA,EAAI;AAExB;AAEA,SAAS,OAAO,GAAoB;AAClC,SAAO,aAAa,QAAQ,EAAE,UAAU,OAAO,CAAC;AAClD;AAOA,eAAe,YACb,MACA,MACA,WACA,gBACkB;AAClB,QAAM,QAAQ,KAAK,IAAA;AACnB,MAAI,KAAK,WAAW,IAAI,EAAG,QAAO;AAClC,SAAO,KAAK,QAAQ,QAAQ,WAAW;AACrC,UAAM,KAAK,MAAM,cAAc;AAC/B,QAAI,KAAK,WAAW,IAAI,EAAG,QAAO;AAAA,EACpC;AACA,SAAO;AACT;AAOA,eAAsB,2BACpB,OACA,QACA,OAA2B,6BACM;AACjC,QAAM,EAAE,aAAa;AAGrB,MAAI,KAAK,WAAW,QAAQ,GAAG;AAC7B,WAAO,EAAE,YAAY,MAAM,WAAW,OAAO,MAAM,EAAA;AAAA,EACrD;AACA,MAAI,CAAC,OAAO,SAAS;AACnB,WAAO,EAAE,YAAY,OAAO,WAAW,OAAO,MAAM,GAAG,QAAQ,sBAAA;AAAA,EACjE;AAEA,QAAM,iBAAiB,KAAK,IAAI,KAAK,OAAO,kBAAkB,GAAI;AAClE,QAAM,YAAY,KAAK,IAAI,KAAM,OAAO,iBAAiB,GAAI;AAC7D,MAAI,OAAiB;AACrB,MAAI;AAGJ,MAAI,MAAM,QAAQ;AAChB,WAAO;AACP,QAAI;AACF,YAAM,KAAK,iBAAiB,MAAM,MAAM;AAAA,IAC1C,SAAS,GAAG;AACV,aAAO,gDAAgD,OAAO,CAAC,CAAC;AAAA,IAClE;AACA,QAAI,MAAM,YAAY,MAAM,UAAU,WAAW,cAAc,GAAG;AAChE,aAAO,EAAE,YAAY,MAAM,WAAW,MAAM,KAAA;AAAA,IAC9C;AAAA,EACF,OAAO;AACL,WAAO;AAAA,EACT;AAGA,MAAI,OAAO,cAAc;AACvB,WAAO;AACP,UAAM,UAAU,MAAM,KAAK,QAAA;AAC3B,QAAI,CAAC,QAAQ,IAAI;AACf,UAAI,QAAQ,oBAAoB;AAC9B,eAAO;AAAA,UACL,YAAY,KAAK,WAAW,QAAQ;AAAA,UACpC,WAAW;AAAA,UACX;AAAA,UACA,QAAQ;AAAA,UACR,MAAM;AAAA,QAAA;AAAA,MAEV;AACA,aAAO,QAAQ,SAAS,QAAQ;AAAA,IAClC,WAAW,MAAM,YAAY,MAAM,UAAU,WAAW,cAAc,GAAG;AACvE,aAAO,EAAE,YAAY,MAAM,WAAW,MAAM,KAAA;AAAA,IAC9C;AAAA,EACF;AAEA,SAAO;AAAA,IACL,YAAY,KAAK,WAAW,QAAQ;AAAA,IACpC,WAAW,OAAO;AAAA,IAClB;AAAA,IACA,QAAQ,SAAS,IAAI,2BAA2B;AAAA,IAChD;AAAA,EAAA;AAEJ;"}
|
package/dist/cli.js
CHANGED
|
@@ -3,7 +3,7 @@ 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 { A as APP_VERSION, d as checkLocalAccess, f as formatAccessReport, I as IMPLEMENTED_TYPES, e as ANALYTIC_INFO, i as installShutdownHandlers, r as registerCleanup, l as looksLikeThreadSlug } from "./shutdown-
|
|
6
|
+
import { A as APP_VERSION, d as checkLocalAccess, f as formatAccessReport, I as IMPLEMENTED_TYPES, e as ANALYTIC_INFO, i as installShutdownHandlers, r as registerCleanup, l as looksLikeThreadSlug } from "./shutdown-CWS6IvCE.js";
|
|
7
7
|
import { t as toYaml } from "./analytics-render-DC26KLXv.js";
|
|
8
8
|
import { spawn } from "node:child_process";
|
|
9
9
|
import { join, dirname } from "node:path";
|
|
@@ -446,9 +446,9 @@ async function runExportCommand(target, opts) {
|
|
|
446
446
|
const { existsSync: existsSync2, mkdirSync, copyFileSync, statSync } = await import("node:fs");
|
|
447
447
|
const { homedir } = await import("node:os");
|
|
448
448
|
const { dirname: dirname2, join: join2, isAbsolute, resolve } = await import("node:path");
|
|
449
|
-
const { getContactsDbPaths, getImsgDbPath, getSlugsDbPath } = await import("./shutdown-
|
|
450
|
-
const { IMessageDB } = await import("./imessage-db-
|
|
451
|
-
const { streamExport } = await import("./exportStream-
|
|
449
|
+
const { getContactsDbPaths, getImsgDbPath, getSlugsDbPath } = await import("./shutdown-CWS6IvCE.js").then((n) => n.ae);
|
|
450
|
+
const { IMessageDB } = await import("./imessage-db-BTy3p2Qk.js").then((n) => n.i);
|
|
451
|
+
const { streamExport } = await import("./exportStream-Bn0Ir89w.js");
|
|
452
452
|
const { parseUserDate } = await import("./date-parse-DJXMfq3a.js");
|
|
453
453
|
const format = normalizeFormat(opts.format ?? "md");
|
|
454
454
|
const ext = extForFormat(format);
|
|
@@ -503,13 +503,13 @@ async function runExportCommand(target, opts) {
|
|
|
503
503
|
if (since) filters.sinceMs = since.getTime();
|
|
504
504
|
if (until) filters.untilMs = until.getTime();
|
|
505
505
|
const attachments = db.searchAttachments(filters);
|
|
506
|
-
const { resolveInterpretConfig } = await import("./app-config-
|
|
506
|
+
const { resolveInterpretConfig } = await import("./app-config-BHA2RWD5.js");
|
|
507
507
|
const nudge = resolveInterpretConfig().nudge;
|
|
508
508
|
const firstMissing = attachments.find(
|
|
509
509
|
(a) => a.filename && !existsSync2(a.filename.replace(/^~/, homedir()))
|
|
510
510
|
);
|
|
511
511
|
if (nudge.enabled && firstMissing && chatIdentifier) {
|
|
512
|
-
const { ensureAttachmentDownloaded } = await import("./attachment-sync-
|
|
512
|
+
const { ensureAttachmentDownloaded } = await import("./attachment-sync-CIoN37ih.js");
|
|
513
513
|
log(` Nudging Messages to sync missing attachments…`, "dim");
|
|
514
514
|
await ensureAttachmentDownloaded(
|
|
515
515
|
{ filePath: firstMissing.filename.replace(/^~/, homedir()), chatId: chatIdentifier },
|
|
@@ -718,10 +718,10 @@ program.command("interpret <rowId>").description("Interpret one attachment (tran
|
|
|
718
718
|
process.exitCode = 1;
|
|
719
719
|
return;
|
|
720
720
|
}
|
|
721
|
-
const { getContactsDbPaths, getImsgDbPath, getSlugsDbPath } = await import("./shutdown-
|
|
722
|
-
const { IMessageDB } = await import("./imessage-db-
|
|
723
|
-
const { getInterpretRuntime, refForAttachment } = await import("./media-intel-runtime-
|
|
724
|
-
const { deleteMediaIntel } = await import("./media-intel-cache-
|
|
721
|
+
const { getContactsDbPaths, getImsgDbPath, getSlugsDbPath } = await import("./shutdown-CWS6IvCE.js").then((n) => n.ae);
|
|
722
|
+
const { IMessageDB } = await import("./imessage-db-BTy3p2Qk.js").then((n) => n.i);
|
|
723
|
+
const { getInterpretRuntime, refForAttachment } = await import("./media-intel-runtime-DwG4nhXt.js").then((n) => n.c);
|
|
724
|
+
const { deleteMediaIntel } = await import("./media-intel-cache-E3ejWhWo.js");
|
|
725
725
|
const db = new IMessageDB(getImsgDbPath(), getContactsDbPaths(), getSlugsDbPath());
|
|
726
726
|
try {
|
|
727
727
|
const rec = db.getAttachmentByRowId(rowId);
|
|
@@ -756,7 +756,7 @@ program.command("interpret <rowId>").description("Interpret one attachment (tran
|
|
|
756
756
|
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)").option("-i, --interactive", "Configure media interpretation (providers, chains, keys)").action(
|
|
757
757
|
async (opts) => {
|
|
758
758
|
if (opts.interactive) {
|
|
759
|
-
const { runSetupWizard } = await import("./setup-wizard-
|
|
759
|
+
const { runSetupWizard } = await import("./setup-wizard-CM_3dRi_.js");
|
|
760
760
|
try {
|
|
761
761
|
await runSetupWizard();
|
|
762
762
|
} catch (err) {
|
|
@@ -768,7 +768,7 @@ program.command("setup").description("Autodetect DB paths and emit an MCP host c
|
|
|
768
768
|
}
|
|
769
769
|
return;
|
|
770
770
|
}
|
|
771
|
-
const { probeMachine, buildMcpSnippet, writeHostConfig } = await import("./setup-
|
|
771
|
+
const { probeMachine, buildMcpSnippet, writeHostConfig } = await import("./setup-BRZC_tgU.js");
|
|
772
772
|
const report = probeMachine();
|
|
773
773
|
if (!report.imsgDb.readable) {
|
|
774
774
|
log(`✗ Messages DB is not readable: ${report.imsgDb.path}`, "err");
|
|
@@ -811,7 +811,7 @@ program.command("setup").description("Autodetect DB paths and emit an MCP host c
|
|
|
811
811
|
);
|
|
812
812
|
const configCmd = program.command("config").description("Manage TUI settings (theme, accent color)");
|
|
813
813
|
configCmd.command("show").description("Print resolved TUI settings and where each value came from").action(async () => {
|
|
814
|
-
const { resolveTuiConfig, defaultTuiConfigPath } = await import("./tui-config-
|
|
814
|
+
const { resolveTuiConfig, defaultTuiConfigPath } = await import("./tui-config-BPDDTKNp.js");
|
|
815
815
|
const cfg = resolveTuiConfig();
|
|
816
816
|
log(
|
|
817
817
|
`config file : ${cfg.configPath ?? `(none — defaults; would write to ${defaultTuiConfigPath()})`}`
|
|
@@ -824,7 +824,7 @@ configCmd.command("show").description("Print resolved TUI settings and where eac
|
|
|
824
824
|
for (const w of cfg.warnings) log(w, "warn");
|
|
825
825
|
});
|
|
826
826
|
configCmd.command("edit").description("Open the TUI config file in $EDITOR (creates it if missing)").action(async () => {
|
|
827
|
-
const { defaultTuiConfigPath, findTuiConfigPath, writeTuiConfig, DEFAULT_TUI_CONFIG } = await import("./tui-config-
|
|
827
|
+
const { defaultTuiConfigPath, findTuiConfigPath, writeTuiConfig, DEFAULT_TUI_CONFIG } = await import("./tui-config-BPDDTKNp.js");
|
|
828
828
|
const path = findTuiConfigPath() ?? defaultTuiConfigPath();
|
|
829
829
|
const { existsSync: existsSync2 } = await import("node:fs");
|
|
830
830
|
if (!existsSync2(path)) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createWriteStream, statSync } from "node:fs";
|
|
2
2
|
import { t as toMarkdown, a as toCSV, b as toNDJSONLine } from "./export-formats-B9v-Qh_H.js";
|
|
3
|
-
import { g as getInterpretRuntime, r as refsForMessages, e as embedInterpretations } from "./media-intel-runtime-
|
|
3
|
+
import { g as getInterpretRuntime, r as refsForMessages, e as embedInterpretations } from "./media-intel-runtime-DwG4nhXt.js";
|
|
4
4
|
class ExportInterpretGuardError extends Error {
|
|
5
5
|
constructor(uncachedCloud, threshold) {
|
|
6
6
|
super(
|
|
@@ -166,4 +166,4 @@ export {
|
|
|
166
166
|
ExportInterpretGuardError,
|
|
167
167
|
streamExport
|
|
168
168
|
};
|
|
169
|
-
//# sourceMappingURL=exportStream-
|
|
169
|
+
//# sourceMappingURL=exportStream-Bn0Ir89w.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"exportStream-BS14zWPF.js","sources":["../src/exportStream.ts"],"sourcesContent":["/**\n * Streaming exporter — writes a conversation to a file in pages so a 100k\n * message history doesn't blow up memory. Used by the `export_messages`\n * MCP tool.\n *\n * Strategy:\n * 1. Walk history chronologically with a stable `(date, ROWID)` cursor.\n * 2. Each page is formatted and written immediately to a writable stream.\n * 3. For JSON we emit a top-level `{ ..., messages: [...] }` shape, opening\n * the array, comma-separating page entries, then closing — that way we\n * never hold the full array in memory.\n * 4. NDJSON is the simplest streaming option (one JSON object per line) —\n * recommended for large exports.\n */\n\nimport { createWriteStream, statSync } from \"node:fs\";\nimport { toCSV, toMarkdown, toNDJSONLine } from \"./export-formats.js\";\nimport type { IMessageDB, MessageExportCursor, UnmergedSiblingChat } from \"./imessage-db.js\";\nimport {\n embedInterpretations,\n getInterpretRuntime,\n type InterpretRuntime,\n refsForMessages,\n} from \"./media-intel-runtime.js\";\n\nexport interface ExportOptions {\n db: IMessageDB;\n chatIdentifier: string;\n format: \"markdown\" | \"csv\" | \"json\" | \"ndjson\";\n outputPath: string;\n since: Date | null;\n until: Date | null;\n pageSize: number;\n signal?: AbortSignal;\n /**\n * Actively interpret media during export (transcribe voice notes, caption\n * images/videos) and embed the text. Cached + instant Apple transcripts are\n * embedded regardless; this only controls whether NEW (incl. paid) calls run.\n */\n interpret?: boolean;\n /** Skip the paid-call guard (see `exportConfirmThreshold`). */\n confirmCloudInterpret?: boolean;\n /** Injectable interpret runtime (defaults to the process singleton). */\n interpretRuntime?: InterpretRuntime;\n}\n\n/**\n * Thrown before any output is written when an `interpret` export would trigger\n * more uncached cloud calls than the configured threshold and the caller has\n * not confirmed. The handler turns this into an actionable message.\n */\nexport class ExportInterpretGuardError extends Error {\n constructor(\n public readonly uncachedCloud: number,\n public readonly threshold: number,\n ) {\n super(\n `This export would trigger ${uncachedCloud} uncached cloud interpretation call(s) (threshold ${threshold}). Re-run with confirmCloudInterpret:true to proceed, or narrow the range with since/until.`,\n );\n this.name = \"ExportInterpretGuardError\";\n }\n}\n\nexport interface ExportResult {\n savedTo: string;\n count: number;\n oldest: Date | null;\n newest: Date | null;\n sizeBytes: number;\n unmergedSiblings: UnmergedSiblingChat[];\n}\n\n/** Type guard — Node ESM filesystem stream */\ntype WritableStream = ReturnType<typeof createWriteStream>;\n\nfunction writeAndDrain(stream: WritableStream, chunk: string): Promise<void> {\n return new Promise((resolve, reject) => {\n if (stream.write(chunk)) {\n resolve();\n return;\n }\n // Back-pressure path: register both listeners and ensure either side\n // also removes the OTHER. Pre-fix only the firing side cleaned up\n // (via .once), so each back-pressure tick leaked a stale \"error\"\n // listener — exporting a 26k-msg thread triggered the MaxListeners\n // warning around the 11th tick.\n const onDrain = () => {\n stream.removeListener(\"error\", onError);\n resolve();\n };\n const onError = (err: Error) => {\n stream.removeListener(\"drain\", onDrain);\n reject(err);\n };\n stream.once(\"drain\", onDrain);\n stream.once(\"error\", onError);\n });\n}\n\nfunction endStream(stream: WritableStream): Promise<void> {\n return new Promise((resolve, reject) => {\n stream.end((err?: Error | null) => (err ? reject(err) : resolve()));\n });\n}\n\nexport async function streamExport(opts: ExportOptions): Promise<ExportResult> {\n const { db, chatIdentifier, format, outputPath, since, until, pageSize, signal } = opts;\n const interpret = opts.interpret ?? false;\n const runtime = opts.interpretRuntime ?? getInterpretRuntime();\n const embedActive = interpret;\n\n // Guard: an ACTIVE-interpret export can fan out into many paid calls. Count\n // the uncached-cloud work in a metadata-only pre-pass BEFORE truncating the\n // output file, and refuse (throwing) when it exceeds the threshold unless the\n // caller confirmed. Cached/instant embedding needs no guard.\n if (interpret && !opts.confirmCloudInterpret) {\n const threshold = runtime.config.exportConfirmThreshold;\n let uncached = 0;\n let guardCursor: MessageExportCursor | null = null;\n while (true) {\n if (signal?.aborted) throw new Error(\"Export cancelled by client.\");\n const page = await db.getMessagesForChatExportPage(chatIdentifier, pageSize, {\n afterCursor: guardCursor,\n since,\n until,\n });\n if (page.messages.length === 0) break;\n uncached += runtime.service.countUncachedCloud(refsForMessages(page.messages));\n guardCursor = page.nextCursor;\n if (!guardCursor || page.rawCount < pageSize) break;\n await new Promise((r) => setImmediate(r));\n }\n if (uncached > threshold) throw new ExportInterpretGuardError(uncached, threshold);\n }\n\n const stream = createWriteStream(outputPath, { encoding: \"utf8\" });\n\n let count = 0;\n let oldest: Date | null = null;\n let newest: Date | null = null;\n let cursor: MessageExportCursor | null = null;\n\n try {\n if (signal?.aborted) throw new Error(\"Export cancelled by client.\");\n\n // Header\n if (format === \"markdown\") {\n const conv = await db.findChatByHandle(chatIdentifier);\n const name = conv?.displayName ?? conv?.rawIdentifier ?? chatIdentifier;\n await writeAndDrain(stream, `# ${name}\\n\\n`);\n if (conv?.threadSlug) await writeAndDrain(stream, `**Slug**: \\`${conv.threadSlug}\\`\\n`);\n if (since) await writeAndDrain(stream, `**Since**: ${since.toISOString()}\\n`);\n if (until) await writeAndDrain(stream, `**Until**: ${until.toISOString()}\\n`);\n await writeAndDrain(stream, `**Exported**: ${new Date().toISOString()}\\n\\n---\\n\\n`);\n } else if (format === \"csv\") {\n await writeAndDrain(\n stream,\n \"id,date,sender,handle,is_from_me,is_read,is_reply,reply_to_text,text,has_attachments\\n\",\n );\n } else if (format === \"json\") {\n await writeAndDrain(\n stream,\n `{\\n \"chatIdentifier\": ${JSON.stringify(chatIdentifier)},\\n \"exportedAt\": ${JSON.stringify(new Date().toISOString())},\\n \"messages\": [\\n`,\n );\n }\n // ndjson: no header\n\n let firstJson = true;\n\n // Page through history in chronological order. The export-specific DB\n // method applies date bounds in SQL and dedupes merged chat rows before\n // LIMIT so every normal message gets one chance to be exported.\n while (true) {\n if (signal?.aborted) throw new Error(\"Export cancelled by client.\");\n const page = await db.getMessagesForChatExportPage(chatIdentifier, pageSize, {\n includeReactionDetails: true,\n afterCursor: cursor,\n since,\n until,\n });\n if (page.messages.length === 0) break;\n\n // Embed voice-note transcripts / captions: cached + instant always, plus\n // active (incl. paid) interpretation when requested. The guard above has\n // already cleared any cloud fan-out.\n await embedInterpretations(page.messages, runtime, embedActive);\n\n // Track running oldest/newest for the result\n for (const m of page.messages) {\n if (oldest == null || m.date < oldest) oldest = m.date;\n if (newest == null || m.date > newest) newest = m.date;\n }\n\n // Format and write\n if (page.messages.length > 0) {\n if (format === \"markdown\") {\n await writeAndDrain(\n stream,\n toMarkdown(page.messages, { thread: chatIdentifier }).split(\"\\n\").slice(6).join(\"\\n\"),\n );\n // ^ slice(6) skips the per-page header (6 lines without participants) — we wrote it once above\n } else if (format === \"csv\") {\n // toCSV emits its own header; skip the first line on each page\n const csvBody = toCSV(page.messages).split(\"\\n\").slice(1).join(\"\\n\");\n await writeAndDrain(stream, `${csvBody}\\n`);\n } else if (format === \"json\") {\n for (const m of page.messages) {\n if (!firstJson) await writeAndDrain(stream, \",\\n\");\n await writeAndDrain(stream, ` ${toNDJSONLine(m)}`);\n firstJson = false;\n }\n } else if (format === \"ndjson\") {\n for (const m of page.messages) {\n await writeAndDrain(stream, `${toNDJSONLine(m)}\\n`);\n }\n }\n }\n count += page.messages.length;\n\n cursor = page.nextCursor;\n if (!cursor || page.rawCount < pageSize) break; // last page\n\n // Yield so other tasks (e.g. health_check) can run\n await new Promise((r) => setImmediate(r));\n }\n\n // Footer\n if (format === \"json\") {\n await writeAndDrain(stream, `\\n ],\\n \"count\": ${count}\\n}\\n`);\n }\n\n await endStream(stream);\n const sizeBytes = (() => {\n try {\n return statSync(outputPath).size;\n } catch {\n return 0;\n }\n })();\n\n const unmergedSiblings = db.findUnmergedSiblingChats(chatIdentifier);\n\n return { savedTo: outputPath, count, oldest, newest, sizeBytes, unmergedSiblings };\n } catch (error) {\n stream.destroy();\n throw error;\n }\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"exportStream-Bn0Ir89w.js","sources":["../src/exportStream.ts"],"sourcesContent":["/**\n * Streaming exporter — writes a conversation to a file in pages so a 100k\n * message history doesn't blow up memory. Used by the `export_messages`\n * MCP tool.\n *\n * Strategy:\n * 1. Walk history chronologically with a stable `(date, ROWID)` cursor.\n * 2. Each page is formatted and written immediately to a writable stream.\n * 3. For JSON we emit a top-level `{ ..., messages: [...] }` shape, opening\n * the array, comma-separating page entries, then closing — that way we\n * never hold the full array in memory.\n * 4. NDJSON is the simplest streaming option (one JSON object per line) —\n * recommended for large exports.\n */\n\nimport { createWriteStream, statSync } from \"node:fs\";\nimport { toCSV, toMarkdown, toNDJSONLine } from \"./export-formats.js\";\nimport type { IMessageDB, MessageExportCursor, UnmergedSiblingChat } from \"./imessage-db.js\";\nimport {\n embedInterpretations,\n getInterpretRuntime,\n type InterpretRuntime,\n refsForMessages,\n} from \"./media-intel-runtime.js\";\n\nexport interface ExportOptions {\n db: IMessageDB;\n chatIdentifier: string;\n format: \"markdown\" | \"csv\" | \"json\" | \"ndjson\";\n outputPath: string;\n since: Date | null;\n until: Date | null;\n pageSize: number;\n signal?: AbortSignal;\n /**\n * Actively interpret media during export (transcribe voice notes, caption\n * images/videos) and embed the text. Cached + instant Apple transcripts are\n * embedded regardless; this only controls whether NEW (incl. paid) calls run.\n */\n interpret?: boolean;\n /** Skip the paid-call guard (see `exportConfirmThreshold`). */\n confirmCloudInterpret?: boolean;\n /** Injectable interpret runtime (defaults to the process singleton). */\n interpretRuntime?: InterpretRuntime;\n}\n\n/**\n * Thrown before any output is written when an `interpret` export would trigger\n * more uncached cloud calls than the configured threshold and the caller has\n * not confirmed. The handler turns this into an actionable message.\n */\nexport class ExportInterpretGuardError extends Error {\n constructor(\n public readonly uncachedCloud: number,\n public readonly threshold: number,\n ) {\n super(\n `This export would trigger ${uncachedCloud} uncached cloud interpretation call(s) (threshold ${threshold}). Re-run with confirmCloudInterpret:true to proceed, or narrow the range with since/until.`,\n );\n this.name = \"ExportInterpretGuardError\";\n }\n}\n\nexport interface ExportResult {\n savedTo: string;\n count: number;\n oldest: Date | null;\n newest: Date | null;\n sizeBytes: number;\n unmergedSiblings: UnmergedSiblingChat[];\n}\n\n/** Type guard — Node ESM filesystem stream */\ntype WritableStream = ReturnType<typeof createWriteStream>;\n\nfunction writeAndDrain(stream: WritableStream, chunk: string): Promise<void> {\n return new Promise((resolve, reject) => {\n if (stream.write(chunk)) {\n resolve();\n return;\n }\n // Back-pressure path: register both listeners and ensure either side\n // also removes the OTHER. Pre-fix only the firing side cleaned up\n // (via .once), so each back-pressure tick leaked a stale \"error\"\n // listener — exporting a 26k-msg thread triggered the MaxListeners\n // warning around the 11th tick.\n const onDrain = () => {\n stream.removeListener(\"error\", onError);\n resolve();\n };\n const onError = (err: Error) => {\n stream.removeListener(\"drain\", onDrain);\n reject(err);\n };\n stream.once(\"drain\", onDrain);\n stream.once(\"error\", onError);\n });\n}\n\nfunction endStream(stream: WritableStream): Promise<void> {\n return new Promise((resolve, reject) => {\n stream.end((err?: Error | null) => (err ? reject(err) : resolve()));\n });\n}\n\nexport async function streamExport(opts: ExportOptions): Promise<ExportResult> {\n const { db, chatIdentifier, format, outputPath, since, until, pageSize, signal } = opts;\n const interpret = opts.interpret ?? false;\n const runtime = opts.interpretRuntime ?? getInterpretRuntime();\n const embedActive = interpret;\n\n // Guard: an ACTIVE-interpret export can fan out into many paid calls. Count\n // the uncached-cloud work in a metadata-only pre-pass BEFORE truncating the\n // output file, and refuse (throwing) when it exceeds the threshold unless the\n // caller confirmed. Cached/instant embedding needs no guard.\n if (interpret && !opts.confirmCloudInterpret) {\n const threshold = runtime.config.exportConfirmThreshold;\n let uncached = 0;\n let guardCursor: MessageExportCursor | null = null;\n while (true) {\n if (signal?.aborted) throw new Error(\"Export cancelled by client.\");\n const page = await db.getMessagesForChatExportPage(chatIdentifier, pageSize, {\n afterCursor: guardCursor,\n since,\n until,\n });\n if (page.messages.length === 0) break;\n uncached += runtime.service.countUncachedCloud(refsForMessages(page.messages));\n guardCursor = page.nextCursor;\n if (!guardCursor || page.rawCount < pageSize) break;\n await new Promise((r) => setImmediate(r));\n }\n if (uncached > threshold) throw new ExportInterpretGuardError(uncached, threshold);\n }\n\n const stream = createWriteStream(outputPath, { encoding: \"utf8\" });\n\n let count = 0;\n let oldest: Date | null = null;\n let newest: Date | null = null;\n let cursor: MessageExportCursor | null = null;\n\n try {\n if (signal?.aborted) throw new Error(\"Export cancelled by client.\");\n\n // Header\n if (format === \"markdown\") {\n const conv = await db.findChatByHandle(chatIdentifier);\n const name = conv?.displayName ?? conv?.rawIdentifier ?? chatIdentifier;\n await writeAndDrain(stream, `# ${name}\\n\\n`);\n if (conv?.threadSlug) await writeAndDrain(stream, `**Slug**: \\`${conv.threadSlug}\\`\\n`);\n if (since) await writeAndDrain(stream, `**Since**: ${since.toISOString()}\\n`);\n if (until) await writeAndDrain(stream, `**Until**: ${until.toISOString()}\\n`);\n await writeAndDrain(stream, `**Exported**: ${new Date().toISOString()}\\n\\n---\\n\\n`);\n } else if (format === \"csv\") {\n await writeAndDrain(\n stream,\n \"id,date,sender,handle,is_from_me,is_read,is_reply,reply_to_text,text,has_attachments\\n\",\n );\n } else if (format === \"json\") {\n await writeAndDrain(\n stream,\n `{\\n \"chatIdentifier\": ${JSON.stringify(chatIdentifier)},\\n \"exportedAt\": ${JSON.stringify(new Date().toISOString())},\\n \"messages\": [\\n`,\n );\n }\n // ndjson: no header\n\n let firstJson = true;\n\n // Page through history in chronological order. The export-specific DB\n // method applies date bounds in SQL and dedupes merged chat rows before\n // LIMIT so every normal message gets one chance to be exported.\n while (true) {\n if (signal?.aborted) throw new Error(\"Export cancelled by client.\");\n const page = await db.getMessagesForChatExportPage(chatIdentifier, pageSize, {\n includeReactionDetails: true,\n afterCursor: cursor,\n since,\n until,\n });\n if (page.messages.length === 0) break;\n\n // Embed voice-note transcripts / captions: cached + instant always, plus\n // active (incl. paid) interpretation when requested. The guard above has\n // already cleared any cloud fan-out.\n await embedInterpretations(page.messages, runtime, embedActive);\n\n // Track running oldest/newest for the result\n for (const m of page.messages) {\n if (oldest == null || m.date < oldest) oldest = m.date;\n if (newest == null || m.date > newest) newest = m.date;\n }\n\n // Format and write\n if (page.messages.length > 0) {\n if (format === \"markdown\") {\n await writeAndDrain(\n stream,\n toMarkdown(page.messages, { thread: chatIdentifier }).split(\"\\n\").slice(6).join(\"\\n\"),\n );\n // ^ slice(6) skips the per-page header (6 lines without participants) — we wrote it once above\n } else if (format === \"csv\") {\n // toCSV emits its own header; skip the first line on each page\n const csvBody = toCSV(page.messages).split(\"\\n\").slice(1).join(\"\\n\");\n await writeAndDrain(stream, `${csvBody}\\n`);\n } else if (format === \"json\") {\n for (const m of page.messages) {\n if (!firstJson) await writeAndDrain(stream, \",\\n\");\n await writeAndDrain(stream, ` ${toNDJSONLine(m)}`);\n firstJson = false;\n }\n } else if (format === \"ndjson\") {\n for (const m of page.messages) {\n await writeAndDrain(stream, `${toNDJSONLine(m)}\\n`);\n }\n }\n }\n count += page.messages.length;\n\n cursor = page.nextCursor;\n if (!cursor || page.rawCount < pageSize) break; // last page\n\n // Yield so other tasks (e.g. health_check) can run\n await new Promise((r) => setImmediate(r));\n }\n\n // Footer\n if (format === \"json\") {\n await writeAndDrain(stream, `\\n ],\\n \"count\": ${count}\\n}\\n`);\n }\n\n await endStream(stream);\n const sizeBytes = (() => {\n try {\n return statSync(outputPath).size;\n } catch {\n return 0;\n }\n })();\n\n const unmergedSiblings = db.findUnmergedSiblingChats(chatIdentifier);\n\n return { savedTo: outputPath, count, oldest, newest, sizeBytes, unmergedSiblings };\n } catch (error) {\n stream.destroy();\n throw error;\n }\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { homedir } from "node:os";
|
|
2
2
|
import { join, dirname } from "node:path";
|
|
3
|
-
import { D as Database, O as OBJECT_REPLACEMENT_CHAR,
|
|
3
|
+
import { D as Database, O as OBJECT_REPLACEMENT_CHAR, v as perf, x as isGroupGuid, y as isGroupChatIdentifier, z as generateThreadSlug, T as Tables, B as AssociatedMessageType, C as macAutoTimestampToDate, E as isReactionType, M as MAC_EPOCH_OFFSET, N as NANOS_PER_SECOND, F as macTimestampToDate$1, G as parseAssociatedMessageGuid$1 } from "./shutdown-CWS6IvCE.js";
|
|
4
4
|
import { createRequire } from "node:module";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { existsSync, readdirSync, mkdirSync } from "node:fs";
|
|
@@ -45,7 +45,7 @@ function levenshteinRatio(a, b) {
|
|
|
45
45
|
return 1 - levenshtein(a, b) / maxLen;
|
|
46
46
|
}
|
|
47
47
|
function fuzzyScore(query, candidate) {
|
|
48
|
-
if (query && candidate
|
|
48
|
+
if (query && candidate?.toLowerCase().includes(query.toLowerCase())) {
|
|
49
49
|
return 0.95;
|
|
50
50
|
}
|
|
51
51
|
const q = cleanText(query);
|
|
@@ -722,7 +722,7 @@ class TypedStreamParser {
|
|
|
722
722
|
parseHeader() {
|
|
723
723
|
if (this.headerParsed) return;
|
|
724
724
|
const magic = this.reader.peekBytes(11);
|
|
725
|
-
if (
|
|
725
|
+
if (magic?.toString("ascii") !== "streamtyped") return;
|
|
726
726
|
this.reader.skip(11);
|
|
727
727
|
const maxSkip = Math.min(this.reader.remaining, 256);
|
|
728
728
|
let skipped = 0;
|
|
@@ -2133,7 +2133,7 @@ class IMessageDB {
|
|
|
2133
2133
|
for (const row of blobRows) {
|
|
2134
2134
|
if (!row.chat_identifier || seenIds.has(row.ROWID)) continue;
|
|
2135
2135
|
const text = this.parseMessageText(row);
|
|
2136
|
-
if (!text
|
|
2136
|
+
if (!text?.toLowerCase().includes(queryLower)) continue;
|
|
2137
2137
|
const ext = this.fetchExtendedMessageData(row.ROWID);
|
|
2138
2138
|
if (isHiddenSystemItem(ext.item_type)) continue;
|
|
2139
2139
|
const converted = this.convertMessage(row, text, row.chat_identifier, ext);
|
|
@@ -3303,4 +3303,4 @@ export {
|
|
|
3303
3303
|
normalizedPhoneVariants as n,
|
|
3304
3304
|
rankFuzzy as r
|
|
3305
3305
|
};
|
|
3306
|
-
//# sourceMappingURL=imessage-db-
|
|
3306
|
+
//# sourceMappingURL=imessage-db-BTy3p2Qk.js.map
|