@zaclinley/dsh-mailharbor 0.3.0 → 0.4.1

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/lib/index.js CHANGED
@@ -469,6 +469,7 @@ var init_imap = __esm({
469
469
 
470
470
  // src/host/api.ts
471
471
  init_address();
472
+ import { randomUUID as randomUUID2 } from "node:crypto";
472
473
 
473
474
  // src/host/config.ts
474
475
  import { createHash, randomUUID } from "node:crypto";
@@ -1503,6 +1504,36 @@ var TRANSLATE_SYSTEM = (target) => `\u4F60\u662F\u90AE\u4EF6\u7FFB\u8BD1\u3002\u
1503
1504
  - \u4FDD\u7559\u6BB5\u843D\u7ED3\u6784
1504
1505
  - \u5982\u679C\u6B63\u6587\u662F HTML\uFF0C\u4FDD\u7559\u539F\u6709\u6807\u7B7E\u3001\u94FE\u63A5\u548C\u56FE\u7247\uFF0C\u53EA\u7FFB\u8BD1\u53EF\u89C1\u6587\u5B57
1505
1506
  - \u4E13\u6709\u540D\u8BCD\u3001\u90AE\u7BB1\u5730\u5740\u3001\u94FE\u63A5\u3001\u5355\u53F7\u4FDD\u6301\u4E0D\u53D8`;
1507
+ var ASK_SYSTEM = `\u4F60\u662F\u90AE\u4EF6\u52A9\u624B\u3002\u57FA\u4E8E\u7528\u6237\u63D0\u4F9B\u7684\u90AE\u4EF6\u548C\u5DF2\u6709\u7684 AI \u8BB0\u5F55\u56DE\u7B54\u95EE\u9898\u3002
1508
+
1509
+ \u8981\u6C42\uFF1A
1510
+ - \u7528\u7B80\u4F53\u4E2D\u6587\u56DE\u7B54\uFF0C\u7B80\u6D01\u76F4\u63A5
1511
+ - \u53EA\u4F9D\u636E\u90AE\u4EF6\u5185\u5BB9\u548C\u5DF2\u6709\u8BB0\u5F55\uFF0C\u4E0D\u786E\u5B9A\u5C31\u8BF4\u660E\u300C\u90AE\u4EF6\u91CC\u6CA1\u6709\u63D0\u5230\u300D
1512
+ - \u4E0D\u8981\u590D\u8FF0\u95EE\u9898\uFF0C\u4E0D\u8981\u8F93\u51FA\u601D\u8003\u8FC7\u7A0B`;
1513
+ var COMPOSE_SYSTEM = (mode, language, style) => {
1514
+ const shared = `- \u8BED\u8A00\uFF1A${language === "en" ? "\u82F1\u6587" : "\u7B80\u4F53\u4E2D\u6587"}
1515
+ - \u4E0D\u8981\u5199\u4E3B\u9898\u884C\uFF0C\u4E0D\u8981\u8F93\u51FA Markdown \u6807\u8BB0\uFF0C\u4E0D\u8981\u89E3\u91CA\u4F60\u5728\u505A\u4EC0\u4E48
1516
+ - \u53EA\u8F93\u51FA\u6B63\u6587\u672C\u8EAB`;
1517
+ const styleLine = style === "" ? "" : `
1518
+ \u5199\u4F5C\u98CE\u683C\uFF08\u6A21\u4EFF\u4F46\u4E0D\u751F\u786C\uFF09\uFF1A
1519
+ ${style}`;
1520
+ if (mode === "polish") {
1521
+ return `\u4F60\u662F\u90AE\u4EF6\u5199\u4F5C\u52A9\u7406\u3002\u6DA6\u8272\u7528\u6237\u7ED9\u51FA\u7684\u90AE\u4EF6\u6B63\u6587\uFF0C\u4FDD\u6301\u539F\u610F\u548C\u4E8B\u5B9E\uFF0C\u8BA9\u8868\u8FBE\u66F4\u6E05\u6670\u81EA\u7136\u3002
1522
+
1523
+ \u8981\u6C42\uFF1A
1524
+ ${shared}${styleLine}`;
1525
+ }
1526
+ if (mode === "shorten") {
1527
+ return `\u4F60\u662F\u90AE\u4EF6\u5199\u4F5C\u52A9\u7406\u3002\u628A\u7528\u6237\u7ED9\u51FA\u7684\u90AE\u4EF6\u6B63\u6587\u7F29\u77ED\u5230\u7EA6\u4E00\u534A\uFF0C\u4FDD\u7559\u5173\u952E\u4FE1\u606F\u548C\u4E8B\u5B9E\u3002
1528
+
1529
+ \u8981\u6C42\uFF1A
1530
+ ${shared}${styleLine}`;
1531
+ }
1532
+ return `\u4F60\u662F\u90AE\u4EF6\u5199\u4F5C\u52A9\u7406\u3002\u6839\u636E\u7528\u6237\u7ED9\u51FA\u7684\u8981\u70B9\u5199\u4E00\u5C01\u90AE\u4EF6\u6B63\u6587\u3002
1533
+
1534
+ \u8981\u6C42\uFF1A
1535
+ ${shared}${styleLine}`;
1536
+ };
1506
1537
  var STYLE_SYSTEM = `\u4F60\u662F\u5199\u4F5C\u98CE\u683C\u5206\u6790\u5E08\u3002\u9605\u8BFB\u7528\u6237\u6700\u8FD1\u53D1\u51FA\u7684\u90AE\u4EF6\uFF0C\u7528\u7B80\u4F53\u4E2D\u6587\u5199\u4E00\u6BB5 150-300 \u5B57\u7684\u5199\u4F5C\u98CE\u683C\u6863\u6848\uFF0C\u8986\u76D6\uFF1A
1507
1538
  \u5E38\u7528\u79F0\u547C\u3001\u5F00\u5934\u65B9\u5F0F\u3001\u7ED3\u5C3E\u4E0E\u7F72\u540D\u3001\u8BED\u6C14\uFF08\u6B63\u5F0F/\u4EB2\u5207/\u76F4\u63A5\uFF09\u3001\u53E5\u5B50\u957F\u77ED\u3001\u5E38\u7528\u8868\u8FBE\u6216\u53E3\u5934\u7985\u3002
1508
1539
  \u53EA\u8F93\u51FA\u6863\u6848\u6B63\u6587\uFF0C\u4E0D\u8981\u8F93\u51FA\u6807\u9898\u3001\u5217\u8868\u7B26\u53F7\u6216\u5206\u6790\u8FC7\u7A0B\u3002`;
@@ -1664,6 +1695,31 @@ var AiService = class {
1664
1695
  if (text2 === "") throw new Error("\u6B63\u6587\u4E3A\u7A7A\uFF0C\u65E0\u6CD5\u7FFB\u8BD1");
1665
1696
  return this.complete(TRANSLATE_SYSTEM(target), clip(text2, 2e4), signal);
1666
1697
  }
1698
+ /** Free-form question about a mail, with up to three earlier AI outputs as context. */
1699
+ async ask(message, question, history, signal) {
1700
+ const prompt = (question ?? "").trim();
1701
+ if (prompt === "") throw new Error("\u95EE\u9898\u4E0D\u80FD\u4E3A\u7A7A");
1702
+ const earlier = history.filter((item) => item.trim() !== "").slice(-3).map((item, index) => `\u3010\u5DF2\u6709\u8BB0\u5F55 ${String(index + 1)}\u3011
1703
+ ${clip(item, 4e3)}`).join("\n\n");
1704
+ const user = [
1705
+ "\u90AE\u4EF6\uFF1A",
1706
+ contextOf(message),
1707
+ ...earlier === "" ? [] : ["", "\u5DF2\u6709 AI \u8BB0\u5F55\uFF1A", earlier],
1708
+ "",
1709
+ `\u7528\u6237\u7684\u95EE\u9898\uFF1A${prompt}`
1710
+ ].join("\n");
1711
+ return this.complete(ASK_SYSTEM, user, signal);
1712
+ }
1713
+ /** Compose-body assistance: draft from points, polish, or shorten. */
1714
+ async compose(account, options, signal) {
1715
+ const style = this.style(account)?.profile ?? "";
1716
+ const language = options.language ?? "zh";
1717
+ const body = options.html.trim();
1718
+ if (options.mode !== "draft" && body === "") throw new Error("\u6B63\u6587\u4E3A\u7A7A");
1719
+ const source = options.mode === "draft" ? `\u7528\u6237\u8981\u70B9\uFF1A${(options.instruction ?? "").trim() === "" ? "\u5199\u4E00\u5C01\u7B80\u77ED\u7684\u90AE\u4EF6" : (options.instruction ?? "").trim()}` : `\u9700\u8981\u5904\u7406\u7684\u6B63\u6587\uFF1A
1720
+ ${clip(htmlToPlain(body), 12e3)}`;
1721
+ return this.complete(COMPOSE_SYSTEM(options.mode, language, style), source, signal);
1722
+ }
1667
1723
  async learnStyle(account, samples, signal) {
1668
1724
  if (samples.length === 0) throw new Error("\u6CA1\u6709\u53EF\u7528\u7684\u5DF2\u53D1\u9001\u90AE\u4EF6\uFF0C\u5148\u5907\u4EFD\u4E00\u6B21\u90AE\u7BB1");
1669
1725
  const digest = samples.slice(0, 100).map((sample, index) => `\u3010${String(index + 1)}\u3011\u4E3B\u9898\uFF1A${sample.subject}
@@ -1697,6 +1753,21 @@ ${clip(sample.text, 800)}`).join("\n\n");
1697
1753
  return catalog;
1698
1754
  }
1699
1755
  };
1756
+ function normalizeTranslated(value) {
1757
+ const raw = value.trim();
1758
+ if (raw === "") return { text: "", html: "" };
1759
+ if (!/<[a-z][\s\S]*>/i.test(raw)) return { text: raw, html: textToHtml(raw) };
1760
+ let body = raw;
1761
+ const match = /<body[^>]*>([\s\S]*?)<\/body>/i.exec(raw);
1762
+ if (match !== null) body = match[1] ?? "";
1763
+ body = body.replace(/<!doctype[^>]*>/gi, "").replace(/<head[\s\S]*?<\/head>/gi, "").replace(/<style[\s\S]*?<\/style>/gi, "").replace(/<script[\s\S]*?<\/script>/gi, "").trim();
1764
+ const text2 = htmlToPlain(body);
1765
+ if (body === "" || text2 === "") return { text: raw, html: textToHtml(raw) };
1766
+ return { text: text2, html: body };
1767
+ }
1768
+ function htmlToPlain(html) {
1769
+ return html.replace(/<style[\s\S]*?<\/style>/gi, "").replace(/<script[\s\S]*?<\/script>/gi, "").replace(/<br\s*\/?>/gi, "\n").replace(/<\/(p|div|li|tr|h[1-6])>/gi, "\n").replace(/<[^>]+>/g, "").replace(/&nbsp;/g, " ").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/\n{3,}/g, "\n\n").trim();
1770
+ }
1700
1771
  function summaryBlocks(text2) {
1701
1772
  const blocks = [];
1702
1773
  let current = { title: "", lines: [] };
@@ -1724,7 +1795,7 @@ function textToHtml(text2) {
1724
1795
  }
1725
1796
 
1726
1797
  // src/host/version.ts
1727
- var VERSION = "0.3.0";
1798
+ var VERSION = "0.4.1";
1728
1799
 
1729
1800
  // src/host/api.ts
1730
1801
  init_smtp();
@@ -2022,6 +2093,109 @@ function buildRouter(runtime) {
2022
2093
  }
2023
2094
  return { ok: true, imap, smtp };
2024
2095
  });
2096
+ router.get("/ai/thread", ({ url }) => {
2097
+ const account = accountFromPayload(runtime, { accountId: url.searchParams.get("accountId") ?? void 0 });
2098
+ const key = (url.searchParams.get("key") ?? "").trim();
2099
+ if (key === "") throw new HttpError(400, "\u7F3A\u5C11\u7EBF\u7A0B key");
2100
+ return { thread: runtime.aiThreads(account).get(key) };
2101
+ });
2102
+ router.delete("/ai/thread", ({ url }) => {
2103
+ const account = accountFromPayload(runtime, { accountId: url.searchParams.get("accountId") ?? void 0 });
2104
+ const key = (url.searchParams.get("key") ?? "").trim();
2105
+ if (key === "") throw new HttpError(400, "\u7F3A\u5C11\u7EBF\u7A0B key");
2106
+ return { ok: runtime.aiThreads(account).clear(key) };
2107
+ });
2108
+ router.delete("/ai/cards", ({ url }) => {
2109
+ const account = accountFromPayload(runtime, { accountId: url.searchParams.get("accountId") ?? void 0 });
2110
+ const key = (url.searchParams.get("key") ?? "").trim();
2111
+ const cardId = (url.searchParams.get("cardId") ?? "").trim();
2112
+ if (key === "" || cardId === "") throw new HttpError(400, "\u7F3A\u5C11\u7EBF\u7A0B key \u6216\u5361\u7247 id");
2113
+ return { thread: runtime.aiThreads(account).removeCard(key, cardId) };
2114
+ });
2115
+ router.post("/ai/cards", async ({ body }) => {
2116
+ const payload = await body();
2117
+ const account = accountFromPayload(runtime, payload);
2118
+ const key = String(payload.key ?? "").trim();
2119
+ if (key === "" || key.length > 200) throw new HttpError(400, "\u7F3A\u5C11\u7EBF\u7A0B key");
2120
+ const kind = String(payload.kind ?? "");
2121
+ if (!["summary", "reply", "translate", "answer"].includes(kind)) {
2122
+ throw new HttpError(400, `\u672A\u77E5\u7684\u5361\u7247\u7C7B\u578B: ${kind}`);
2123
+ }
2124
+ const detail = await loadDetail(
2125
+ runtime,
2126
+ account,
2127
+ sourceOfPayload(payload),
2128
+ String(payload.id ?? ""),
2129
+ mailboxOfPayload(payload)
2130
+ );
2131
+ const config = runtime.current;
2132
+ const instruction = typeof payload.instruction === "string" ? payload.instruction : "";
2133
+ const language = payload.language === "en" ? "en" : "zh";
2134
+ const target = payload.target === "en" ? "en" : "zh";
2135
+ const ai = runtime.ai();
2136
+ try {
2137
+ let output = "";
2138
+ let html;
2139
+ let meta;
2140
+ if (kind === "summary") {
2141
+ output = await ai.summarize(detail);
2142
+ } else if (kind === "reply") {
2143
+ output = await ai.reply(account, detail, { instruction, language });
2144
+ html = textToHtml(output);
2145
+ meta = {
2146
+ to: detail.from.address,
2147
+ subject: /^re:/i.test(detail.subject) ? detail.subject : `Re: ${detail.subject}`,
2148
+ inReplyTo: detail.messageId ?? "",
2149
+ references: [detail.references.join(" "), detail.messageId ?? ""].join(" ").trim(),
2150
+ language
2151
+ };
2152
+ } else if (kind === "translate") {
2153
+ const source = detail.html !== null && detail.html !== "" ? detail.html : detail.text;
2154
+ const translated = normalizeTranslated(await ai.translate(source, target));
2155
+ output = translated.text;
2156
+ html = translated.html;
2157
+ meta = { target };
2158
+ } else {
2159
+ const earlier = runtime.aiThreads(account).get(key)?.cards.filter((card2) => card2.kind === "answer").map((card2) => card2.output) ?? [];
2160
+ output = await ai.ask(detail, instruction, earlier);
2161
+ }
2162
+ const card = {
2163
+ id: randomUUID2(),
2164
+ kind,
2165
+ instruction,
2166
+ output,
2167
+ ...html === void 0 ? {} : { html },
2168
+ ...meta === void 0 ? {} : { meta },
2169
+ provider: config.ai.provider,
2170
+ model: config.ai.model,
2171
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
2172
+ };
2173
+ const thread = runtime.aiThreads(account).appendCard({
2174
+ key,
2175
+ accountId: account.id,
2176
+ subject: detail.subject,
2177
+ messageId: detail.messageId ?? "",
2178
+ card
2179
+ });
2180
+ return { card, thread };
2181
+ } catch (error) {
2182
+ throw aiHttpError(error);
2183
+ }
2184
+ });
2185
+ router.post("/ai/compose", async ({ body }) => {
2186
+ const payload = await body();
2187
+ const account = accountFromPayload(runtime, payload);
2188
+ const mode = payload.mode === "polish" ? "polish" : payload.mode === "shorten" ? "shorten" : "draft";
2189
+ const html = typeof payload.html === "string" ? payload.html : "";
2190
+ const instruction = typeof payload.instruction === "string" ? payload.instruction : "";
2191
+ const language = payload.language === "en" ? "en" : "zh";
2192
+ try {
2193
+ const text2 = await runtime.ai().compose(account, { mode, instruction, html, language });
2194
+ return { text: text2, html: /<[a-z][\s\S]*>/i.test(text2) ? text2 : textToHtml(text2) };
2195
+ } catch (error) {
2196
+ throw aiHttpError(error);
2197
+ }
2198
+ });
2025
2199
  router.get("/ai/status", ({ url }) => {
2026
2200
  const requested = url.searchParams.get("accountId");
2027
2201
  const account = requested !== null && requested !== "" && requested !== "all" ? runtime.current.accounts.find((item) => item.id === requested) : runtime.active();
@@ -2069,8 +2243,8 @@ function buildRouter(runtime) {
2069
2243
  const html = typeof payload.html === "string" ? payload.html : "";
2070
2244
  const target = payload.target === "en" ? "en" : "zh";
2071
2245
  try {
2072
- const text2 = await runtime.ai().translate(html, target);
2073
- return { text: text2, html: /<[a-z][\s\S]*>/i.test(text2) ? text2 : textToHtml(text2) };
2246
+ const translated = normalizeTranslated(await runtime.ai().translate(html, target));
2247
+ return { text: translated.text, html: translated.html };
2074
2248
  } catch (error) {
2075
2249
  throw aiHttpError(error);
2076
2250
  }
@@ -2582,10 +2756,150 @@ var CredentialManager = class {
2582
2756
  }
2583
2757
  };
2584
2758
 
2759
+ // src/host/ai-store.ts
2760
+ import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync4, renameSync as renameSync3, writeFileSync as writeFileSync4 } from "node:fs";
2761
+ import { dirname as dirname2 } from "node:path";
2762
+ var MAX_THREADS = 50;
2763
+ var MAX_CARDS_PER_THREAD = 50;
2764
+ var MAX_FILE_BYTES = 2 * 1024 * 1024;
2765
+ var AiThreadStore = class {
2766
+ file;
2767
+ cache = null;
2768
+ constructor(file) {
2769
+ this.file = file;
2770
+ }
2771
+ load() {
2772
+ if (this.cache !== null) return this.cache;
2773
+ let parsed = null;
2774
+ try {
2775
+ if (existsSync5(this.file)) parsed = JSON.parse(readFileSync4(this.file, "utf8"));
2776
+ } catch {
2777
+ parsed = null;
2778
+ }
2779
+ const threads = parsed !== null && typeof parsed === "object" && parsed.threads !== void 0 ? parsed.threads : {};
2780
+ this.cache = { version: 1, threads: sanitize(threads) };
2781
+ return this.cache;
2782
+ }
2783
+ flush() {
2784
+ const data = this.load();
2785
+ prune(data);
2786
+ const text2 = `${JSON.stringify(data, null, 2)}
2787
+ `;
2788
+ mkdirSync4(dirname2(this.file), { recursive: true });
2789
+ const temp = `${this.file}.${String(process.pid)}.tmp`;
2790
+ writeFileSync4(temp, text2, { mode: 384 });
2791
+ renameSync3(temp, this.file);
2792
+ }
2793
+ get(key) {
2794
+ const thread = this.load().threads[key];
2795
+ return thread === void 0 ? null : { ...thread, cards: [...thread.cards] };
2796
+ }
2797
+ appendCard(input) {
2798
+ const data = this.load();
2799
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2800
+ const existing = data.threads[input.key];
2801
+ const thread = existing ?? {
2802
+ key: input.key,
2803
+ accountId: input.accountId,
2804
+ subject: input.subject,
2805
+ messageId: input.messageId ?? "",
2806
+ cards: [],
2807
+ updatedAt: now
2808
+ };
2809
+ thread.accountId = input.accountId;
2810
+ thread.subject = input.subject;
2811
+ if (input.messageId !== void 0 && input.messageId !== "") thread.messageId = input.messageId;
2812
+ thread.cards.push(input.card);
2813
+ thread.updatedAt = now;
2814
+ data.threads[input.key] = thread;
2815
+ this.flush();
2816
+ return { ...thread, cards: [...thread.cards] };
2817
+ }
2818
+ removeCard(key, cardId) {
2819
+ const data = this.load();
2820
+ const thread = data.threads[key];
2821
+ if (thread === void 0) return null;
2822
+ thread.cards = thread.cards.filter((card) => card.id !== cardId);
2823
+ thread.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
2824
+ if (thread.cards.length === 0) {
2825
+ delete data.threads[key];
2826
+ this.flush();
2827
+ return null;
2828
+ }
2829
+ this.flush();
2830
+ return { ...thread, cards: [...thread.cards] };
2831
+ }
2832
+ clear(key) {
2833
+ const data = this.load();
2834
+ if (data.threads[key] === void 0) return false;
2835
+ delete data.threads[key];
2836
+ this.flush();
2837
+ return true;
2838
+ }
2839
+ /** Number of threads, for the settings page. */
2840
+ size() {
2841
+ return Object.keys(this.load().threads).length;
2842
+ }
2843
+ };
2844
+ function sanitize(input) {
2845
+ if (input === null || typeof input !== "object") return {};
2846
+ const source = input;
2847
+ const result = {};
2848
+ for (const [key, raw] of Object.entries(source)) {
2849
+ if (raw === null || typeof raw !== "object") continue;
2850
+ const thread = raw;
2851
+ if (typeof thread.accountId !== "string" || !Array.isArray(thread.cards)) continue;
2852
+ const cards = thread.cards.filter(
2853
+ (card) => card !== null && typeof card === "object" && typeof card.id === "string"
2854
+ ).map((card) => ({
2855
+ id: card.id,
2856
+ kind: card.kind,
2857
+ instruction: String(card.instruction ?? ""),
2858
+ output: String(card.output ?? ""),
2859
+ ...typeof card.html === "string" ? { html: card.html } : {},
2860
+ ...card.meta !== void 0 ? { meta: card.meta } : {},
2861
+ provider: String(card.provider ?? ""),
2862
+ model: String(card.model ?? ""),
2863
+ createdAt: String(card.createdAt ?? "")
2864
+ }));
2865
+ result[key] = {
2866
+ key,
2867
+ accountId: thread.accountId,
2868
+ subject: String(thread.subject ?? ""),
2869
+ messageId: String(thread.messageId ?? ""),
2870
+ cards,
2871
+ updatedAt: String(thread.updatedAt ?? "")
2872
+ };
2873
+ }
2874
+ return result;
2875
+ }
2876
+ function prune(data) {
2877
+ const entries = Object.entries(data.threads).sort(
2878
+ (left, right) => right[1].updatedAt.localeCompare(left[1].updatedAt)
2879
+ );
2880
+ const kept = new Map(entries.slice(0, MAX_THREADS));
2881
+ for (const thread of kept.values()) {
2882
+ if (thread.cards.length > MAX_CARDS_PER_THREAD) {
2883
+ thread.cards = thread.cards.slice(-MAX_CARDS_PER_THREAD);
2884
+ }
2885
+ }
2886
+ data.threads = Object.fromEntries(kept);
2887
+ let size = JSON.stringify(data).length;
2888
+ while (size > MAX_FILE_BYTES && kept.size > 1) {
2889
+ const oldest = [...kept.entries()].sort(
2890
+ (left, right) => left[1].updatedAt.localeCompare(right[1].updatedAt)
2891
+ )[0];
2892
+ if (oldest === void 0) break;
2893
+ kept.delete(oldest[0]);
2894
+ data.threads = Object.fromEntries(kept);
2895
+ size = JSON.stringify(data).length;
2896
+ }
2897
+ }
2898
+
2585
2899
  // src/host/drafts.ts
2586
- import { randomUUID as randomUUID2 } from "node:crypto";
2587
- import { mkdirSync as mkdirSync4, readFileSync as readFileSync4, renameSync as renameSync3, writeFileSync as writeFileSync4 } from "node:fs";
2588
- import { dirname as dirname2, join as join4 } from "node:path";
2900
+ import { randomUUID as randomUUID3 } from "node:crypto";
2901
+ import { mkdirSync as mkdirSync5, readFileSync as readFileSync5, renameSync as renameSync4, writeFileSync as writeFileSync5 } from "node:fs";
2902
+ import { dirname as dirname3, join as join4 } from "node:path";
2589
2903
  function text(value) {
2590
2904
  return value === void 0 || value === null ? "" : String(value);
2591
2905
  }
@@ -2609,18 +2923,18 @@ var DraftStore = class {
2609
2923
  }
2610
2924
  load() {
2611
2925
  try {
2612
- const parsed = JSON.parse(readFileSync4(this.file, "utf8"));
2926
+ const parsed = JSON.parse(readFileSync5(this.file, "utf8"));
2613
2927
  this.items = Array.isArray(parsed) ? parsed.filter(isDraft) : [];
2614
2928
  } catch {
2615
2929
  this.items = [];
2616
2930
  }
2617
2931
  }
2618
2932
  flush() {
2619
- mkdirSync4(dirname2(this.file), { recursive: true });
2933
+ mkdirSync5(dirname3(this.file), { recursive: true });
2620
2934
  const temp = `${this.file}.${String(process.pid)}.${String(Date.now())}.tmp`;
2621
- writeFileSync4(temp, `${JSON.stringify(this.items, null, 2)}
2935
+ writeFileSync5(temp, `${JSON.stringify(this.items, null, 2)}
2622
2936
  `, { encoding: "utf8", mode: 384 });
2623
- renameSync3(temp, this.file);
2937
+ renameSync4(temp, this.file);
2624
2938
  }
2625
2939
  list() {
2626
2940
  return [...this.items].sort((left, right) => right.updatedAt.localeCompare(left.updatedAt)).map((draft) => ({ ...draft }));
@@ -2652,7 +2966,7 @@ var DraftStore = class {
2652
2966
  return { ...existing };
2653
2967
  }
2654
2968
  const draft = {
2655
- id: randomUUID2(),
2969
+ id: randomUUID3(),
2656
2970
  accountId: this.accountId,
2657
2971
  ...fields,
2658
2972
  updatedAt: now
@@ -2677,8 +2991,8 @@ var DraftStore = class {
2677
2991
  init_imap();
2678
2992
 
2679
2993
  // src/host/read-state.ts
2680
- import { mkdirSync as mkdirSync5, readFileSync as readFileSync5, renameSync as renameSync4, writeFileSync as writeFileSync5 } from "node:fs";
2681
- import { dirname as dirname3 } from "node:path";
2994
+ import { mkdirSync as mkdirSync6, readFileSync as readFileSync6, renameSync as renameSync5, writeFileSync as writeFileSync6 } from "node:fs";
2995
+ import { dirname as dirname4 } from "node:path";
2682
2996
  var ReadStateStore = class {
2683
2997
  file;
2684
2998
  keys = null;
@@ -2688,7 +3002,7 @@ var ReadStateStore = class {
2688
3002
  load() {
2689
3003
  if (this.keys !== null) return this.keys;
2690
3004
  try {
2691
- const parsed = JSON.parse(readFileSync5(this.file, "utf8"));
3005
+ const parsed = JSON.parse(readFileSync6(this.file, "utf8"));
2692
3006
  const list = Array.isArray(parsed) ? parsed : Array.isArray(parsed?.unread) ? parsed.unread : [];
2693
3007
  this.keys = new Set(list.filter((key) => typeof key === "string" && key !== ""));
2694
3008
  } catch {
@@ -2697,14 +3011,14 @@ var ReadStateStore = class {
2697
3011
  return this.keys;
2698
3012
  }
2699
3013
  flush() {
2700
- mkdirSync5(dirname3(this.file), { recursive: true });
3014
+ mkdirSync6(dirname4(this.file), { recursive: true });
2701
3015
  const temp = `${this.file}.${String(process.pid)}.tmp`;
2702
- writeFileSync5(temp, `${JSON.stringify({ unread: [...this.load()] }, null, 2)}
3016
+ writeFileSync6(temp, `${JSON.stringify({ unread: [...this.load()] }, null, 2)}
2703
3017
  `, {
2704
3018
  encoding: "utf8",
2705
3019
  mode: 384
2706
3020
  });
2707
- renameSync4(temp, this.file);
3021
+ renameSync5(temp, this.file);
2708
3022
  }
2709
3023
  isUnread(key) {
2710
3024
  return key !== "" && this.load().has(key);
@@ -2760,6 +3074,7 @@ var MailHarborRuntime = class {
2760
3074
  databases = /* @__PURE__ */ new Map();
2761
3075
  draftStores = /* @__PURE__ */ new Map();
2762
3076
  readStores = /* @__PURE__ */ new Map();
3077
+ aiStores = /* @__PURE__ */ new Map();
2763
3078
  backups = /* @__PURE__ */ new Map();
2764
3079
  llmProvider;
2765
3080
  aiService = null;
@@ -2823,6 +3138,7 @@ var MailHarborRuntime = class {
2823
3138
  }
2824
3139
  for (const id of [...this.draftStores.keys()]) if (!live.has(id)) this.draftStores.delete(id);
2825
3140
  for (const id of [...this.readStores.keys()]) if (!live.has(id)) this.readStores.delete(id);
3141
+ for (const id of [...this.aiStores.keys()]) if (!live.has(id)) this.aiStores.delete(id);
2826
3142
  }
2827
3143
  /** Drop cached connections and stores for one account after a settings change. */
2828
3144
  invalidate(accountId) {
@@ -2832,6 +3148,7 @@ var MailHarborRuntime = class {
2832
3148
  this.databases.delete(accountId);
2833
3149
  this.draftStores.delete(accountId);
2834
3150
  this.readStores.delete(accountId);
3151
+ this.aiStores.delete(accountId);
2835
3152
  this.refresh();
2836
3153
  }
2837
3154
  active() {
@@ -2890,6 +3207,14 @@ var MailHarborRuntime = class {
2890
3207
  this.draftStores.set(account.id, { signature: file, value: created });
2891
3208
  return created;
2892
3209
  }
3210
+ aiThreads(account) {
3211
+ const file = join5(this.paths(account).root, "ai-threads.json");
3212
+ const existing = this.aiStores.get(account.id);
3213
+ if (existing !== void 0 && existing.signature === file) return existing.value;
3214
+ const created = new AiThreadStore(file);
3215
+ this.aiStores.set(account.id, { signature: file, value: created });
3216
+ return created;
3217
+ }
2893
3218
  readState(account) {
2894
3219
  const file = join5(this.paths(account).root, "read-state.json");
2895
3220
  const existing = this.readStores.get(account.id);