@zaclinley/dsh-mailharbor 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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,9 @@ ${clip(sample.text, 800)}`).join("\n\n");
1697
1753
  return catalog;
1698
1754
  }
1699
1755
  };
1756
+ function htmlToPlain(html) {
1757
+ 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();
1758
+ }
1700
1759
  function summaryBlocks(text2) {
1701
1760
  const blocks = [];
1702
1761
  let current = { title: "", lines: [] };
@@ -1724,7 +1783,7 @@ function textToHtml(text2) {
1724
1783
  }
1725
1784
 
1726
1785
  // src/host/version.ts
1727
- var VERSION = "0.3.0";
1786
+ var VERSION = "0.4.0";
1728
1787
 
1729
1788
  // src/host/api.ts
1730
1789
  init_smtp();
@@ -2022,6 +2081,108 @@ function buildRouter(runtime) {
2022
2081
  }
2023
2082
  return { ok: true, imap, smtp };
2024
2083
  });
2084
+ router.get("/ai/thread", ({ url }) => {
2085
+ const account = accountFromPayload(runtime, { accountId: url.searchParams.get("accountId") ?? void 0 });
2086
+ const key = (url.searchParams.get("key") ?? "").trim();
2087
+ if (key === "") throw new HttpError(400, "\u7F3A\u5C11\u7EBF\u7A0B key");
2088
+ return { thread: runtime.aiThreads(account).get(key) };
2089
+ });
2090
+ router.delete("/ai/thread", ({ url }) => {
2091
+ const account = accountFromPayload(runtime, { accountId: url.searchParams.get("accountId") ?? void 0 });
2092
+ const key = (url.searchParams.get("key") ?? "").trim();
2093
+ if (key === "") throw new HttpError(400, "\u7F3A\u5C11\u7EBF\u7A0B key");
2094
+ return { ok: runtime.aiThreads(account).clear(key) };
2095
+ });
2096
+ router.delete("/ai/cards", ({ url }) => {
2097
+ const account = accountFromPayload(runtime, { accountId: url.searchParams.get("accountId") ?? void 0 });
2098
+ const key = (url.searchParams.get("key") ?? "").trim();
2099
+ const cardId = (url.searchParams.get("cardId") ?? "").trim();
2100
+ if (key === "" || cardId === "") throw new HttpError(400, "\u7F3A\u5C11\u7EBF\u7A0B key \u6216\u5361\u7247 id");
2101
+ return { thread: runtime.aiThreads(account).removeCard(key, cardId) };
2102
+ });
2103
+ router.post("/ai/cards", async ({ body }) => {
2104
+ const payload = await body();
2105
+ const account = accountFromPayload(runtime, payload);
2106
+ const key = String(payload.key ?? "").trim();
2107
+ if (key === "" || key.length > 200) throw new HttpError(400, "\u7F3A\u5C11\u7EBF\u7A0B key");
2108
+ const kind = String(payload.kind ?? "");
2109
+ if (!["summary", "reply", "translate", "answer"].includes(kind)) {
2110
+ throw new HttpError(400, `\u672A\u77E5\u7684\u5361\u7247\u7C7B\u578B: ${kind}`);
2111
+ }
2112
+ const detail = await loadDetail(
2113
+ runtime,
2114
+ account,
2115
+ sourceOfPayload(payload),
2116
+ String(payload.id ?? ""),
2117
+ mailboxOfPayload(payload)
2118
+ );
2119
+ const config = runtime.current;
2120
+ const instruction = typeof payload.instruction === "string" ? payload.instruction : "";
2121
+ const language = payload.language === "en" ? "en" : "zh";
2122
+ const target = payload.target === "en" ? "en" : "zh";
2123
+ const ai = runtime.ai();
2124
+ try {
2125
+ let output = "";
2126
+ let html;
2127
+ let meta;
2128
+ if (kind === "summary") {
2129
+ output = await ai.summarize(detail);
2130
+ } else if (kind === "reply") {
2131
+ output = await ai.reply(account, detail, { instruction, language });
2132
+ html = textToHtml(output);
2133
+ meta = {
2134
+ to: detail.from.address,
2135
+ subject: /^re:/i.test(detail.subject) ? detail.subject : `Re: ${detail.subject}`,
2136
+ inReplyTo: detail.messageId ?? "",
2137
+ references: [detail.references.join(" "), detail.messageId ?? ""].join(" ").trim(),
2138
+ language
2139
+ };
2140
+ } else if (kind === "translate") {
2141
+ const source = detail.html !== null && detail.html !== "" ? detail.html : detail.text;
2142
+ output = await ai.translate(source, target);
2143
+ html = /<[a-z][\s\S]*>/i.test(output) ? output : textToHtml(output);
2144
+ meta = { target };
2145
+ } else {
2146
+ const earlier = runtime.aiThreads(account).get(key)?.cards.filter((card2) => card2.kind === "answer").map((card2) => card2.output) ?? [];
2147
+ output = await ai.ask(detail, instruction, earlier);
2148
+ }
2149
+ const card = {
2150
+ id: randomUUID2(),
2151
+ kind,
2152
+ instruction,
2153
+ output,
2154
+ ...html === void 0 ? {} : { html },
2155
+ ...meta === void 0 ? {} : { meta },
2156
+ provider: config.ai.provider,
2157
+ model: config.ai.model,
2158
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
2159
+ };
2160
+ const thread = runtime.aiThreads(account).appendCard({
2161
+ key,
2162
+ accountId: account.id,
2163
+ subject: detail.subject,
2164
+ messageId: detail.messageId ?? "",
2165
+ card
2166
+ });
2167
+ return { card, thread };
2168
+ } catch (error) {
2169
+ throw aiHttpError(error);
2170
+ }
2171
+ });
2172
+ router.post("/ai/compose", async ({ body }) => {
2173
+ const payload = await body();
2174
+ const account = accountFromPayload(runtime, payload);
2175
+ const mode = payload.mode === "polish" ? "polish" : payload.mode === "shorten" ? "shorten" : "draft";
2176
+ const html = typeof payload.html === "string" ? payload.html : "";
2177
+ const instruction = typeof payload.instruction === "string" ? payload.instruction : "";
2178
+ const language = payload.language === "en" ? "en" : "zh";
2179
+ try {
2180
+ const text2 = await runtime.ai().compose(account, { mode, instruction, html, language });
2181
+ return { text: text2, html: /<[a-z][\s\S]*>/i.test(text2) ? text2 : textToHtml(text2) };
2182
+ } catch (error) {
2183
+ throw aiHttpError(error);
2184
+ }
2185
+ });
2025
2186
  router.get("/ai/status", ({ url }) => {
2026
2187
  const requested = url.searchParams.get("accountId");
2027
2188
  const account = requested !== null && requested !== "" && requested !== "all" ? runtime.current.accounts.find((item) => item.id === requested) : runtime.active();
@@ -2582,10 +2743,150 @@ var CredentialManager = class {
2582
2743
  }
2583
2744
  };
2584
2745
 
2746
+ // src/host/ai-store.ts
2747
+ import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync4, renameSync as renameSync3, writeFileSync as writeFileSync4 } from "node:fs";
2748
+ import { dirname as dirname2 } from "node:path";
2749
+ var MAX_THREADS = 50;
2750
+ var MAX_CARDS_PER_THREAD = 50;
2751
+ var MAX_FILE_BYTES = 2 * 1024 * 1024;
2752
+ var AiThreadStore = class {
2753
+ file;
2754
+ cache = null;
2755
+ constructor(file) {
2756
+ this.file = file;
2757
+ }
2758
+ load() {
2759
+ if (this.cache !== null) return this.cache;
2760
+ let parsed = null;
2761
+ try {
2762
+ if (existsSync5(this.file)) parsed = JSON.parse(readFileSync4(this.file, "utf8"));
2763
+ } catch {
2764
+ parsed = null;
2765
+ }
2766
+ const threads = parsed !== null && typeof parsed === "object" && parsed.threads !== void 0 ? parsed.threads : {};
2767
+ this.cache = { version: 1, threads: sanitize(threads) };
2768
+ return this.cache;
2769
+ }
2770
+ flush() {
2771
+ const data = this.load();
2772
+ prune(data);
2773
+ const text2 = `${JSON.stringify(data, null, 2)}
2774
+ `;
2775
+ mkdirSync4(dirname2(this.file), { recursive: true });
2776
+ const temp = `${this.file}.${String(process.pid)}.tmp`;
2777
+ writeFileSync4(temp, text2, { mode: 384 });
2778
+ renameSync3(temp, this.file);
2779
+ }
2780
+ get(key) {
2781
+ const thread = this.load().threads[key];
2782
+ return thread === void 0 ? null : { ...thread, cards: [...thread.cards] };
2783
+ }
2784
+ appendCard(input) {
2785
+ const data = this.load();
2786
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2787
+ const existing = data.threads[input.key];
2788
+ const thread = existing ?? {
2789
+ key: input.key,
2790
+ accountId: input.accountId,
2791
+ subject: input.subject,
2792
+ messageId: input.messageId ?? "",
2793
+ cards: [],
2794
+ updatedAt: now
2795
+ };
2796
+ thread.accountId = input.accountId;
2797
+ thread.subject = input.subject;
2798
+ if (input.messageId !== void 0 && input.messageId !== "") thread.messageId = input.messageId;
2799
+ thread.cards.push(input.card);
2800
+ thread.updatedAt = now;
2801
+ data.threads[input.key] = thread;
2802
+ this.flush();
2803
+ return { ...thread, cards: [...thread.cards] };
2804
+ }
2805
+ removeCard(key, cardId) {
2806
+ const data = this.load();
2807
+ const thread = data.threads[key];
2808
+ if (thread === void 0) return null;
2809
+ thread.cards = thread.cards.filter((card) => card.id !== cardId);
2810
+ thread.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
2811
+ if (thread.cards.length === 0) {
2812
+ delete data.threads[key];
2813
+ this.flush();
2814
+ return null;
2815
+ }
2816
+ this.flush();
2817
+ return { ...thread, cards: [...thread.cards] };
2818
+ }
2819
+ clear(key) {
2820
+ const data = this.load();
2821
+ if (data.threads[key] === void 0) return false;
2822
+ delete data.threads[key];
2823
+ this.flush();
2824
+ return true;
2825
+ }
2826
+ /** Number of threads, for the settings page. */
2827
+ size() {
2828
+ return Object.keys(this.load().threads).length;
2829
+ }
2830
+ };
2831
+ function sanitize(input) {
2832
+ if (input === null || typeof input !== "object") return {};
2833
+ const source = input;
2834
+ const result = {};
2835
+ for (const [key, raw] of Object.entries(source)) {
2836
+ if (raw === null || typeof raw !== "object") continue;
2837
+ const thread = raw;
2838
+ if (typeof thread.accountId !== "string" || !Array.isArray(thread.cards)) continue;
2839
+ const cards = thread.cards.filter(
2840
+ (card) => card !== null && typeof card === "object" && typeof card.id === "string"
2841
+ ).map((card) => ({
2842
+ id: card.id,
2843
+ kind: card.kind,
2844
+ instruction: String(card.instruction ?? ""),
2845
+ output: String(card.output ?? ""),
2846
+ ...typeof card.html === "string" ? { html: card.html } : {},
2847
+ ...card.meta !== void 0 ? { meta: card.meta } : {},
2848
+ provider: String(card.provider ?? ""),
2849
+ model: String(card.model ?? ""),
2850
+ createdAt: String(card.createdAt ?? "")
2851
+ }));
2852
+ result[key] = {
2853
+ key,
2854
+ accountId: thread.accountId,
2855
+ subject: String(thread.subject ?? ""),
2856
+ messageId: String(thread.messageId ?? ""),
2857
+ cards,
2858
+ updatedAt: String(thread.updatedAt ?? "")
2859
+ };
2860
+ }
2861
+ return result;
2862
+ }
2863
+ function prune(data) {
2864
+ const entries = Object.entries(data.threads).sort(
2865
+ (left, right) => right[1].updatedAt.localeCompare(left[1].updatedAt)
2866
+ );
2867
+ const kept = new Map(entries.slice(0, MAX_THREADS));
2868
+ for (const thread of kept.values()) {
2869
+ if (thread.cards.length > MAX_CARDS_PER_THREAD) {
2870
+ thread.cards = thread.cards.slice(-MAX_CARDS_PER_THREAD);
2871
+ }
2872
+ }
2873
+ data.threads = Object.fromEntries(kept);
2874
+ let size = JSON.stringify(data).length;
2875
+ while (size > MAX_FILE_BYTES && kept.size > 1) {
2876
+ const oldest = [...kept.entries()].sort(
2877
+ (left, right) => left[1].updatedAt.localeCompare(right[1].updatedAt)
2878
+ )[0];
2879
+ if (oldest === void 0) break;
2880
+ kept.delete(oldest[0]);
2881
+ data.threads = Object.fromEntries(kept);
2882
+ size = JSON.stringify(data).length;
2883
+ }
2884
+ }
2885
+
2585
2886
  // 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";
2887
+ import { randomUUID as randomUUID3 } from "node:crypto";
2888
+ import { mkdirSync as mkdirSync5, readFileSync as readFileSync5, renameSync as renameSync4, writeFileSync as writeFileSync5 } from "node:fs";
2889
+ import { dirname as dirname3, join as join4 } from "node:path";
2589
2890
  function text(value) {
2590
2891
  return value === void 0 || value === null ? "" : String(value);
2591
2892
  }
@@ -2609,18 +2910,18 @@ var DraftStore = class {
2609
2910
  }
2610
2911
  load() {
2611
2912
  try {
2612
- const parsed = JSON.parse(readFileSync4(this.file, "utf8"));
2913
+ const parsed = JSON.parse(readFileSync5(this.file, "utf8"));
2613
2914
  this.items = Array.isArray(parsed) ? parsed.filter(isDraft) : [];
2614
2915
  } catch {
2615
2916
  this.items = [];
2616
2917
  }
2617
2918
  }
2618
2919
  flush() {
2619
- mkdirSync4(dirname2(this.file), { recursive: true });
2920
+ mkdirSync5(dirname3(this.file), { recursive: true });
2620
2921
  const temp = `${this.file}.${String(process.pid)}.${String(Date.now())}.tmp`;
2621
- writeFileSync4(temp, `${JSON.stringify(this.items, null, 2)}
2922
+ writeFileSync5(temp, `${JSON.stringify(this.items, null, 2)}
2622
2923
  `, { encoding: "utf8", mode: 384 });
2623
- renameSync3(temp, this.file);
2924
+ renameSync4(temp, this.file);
2624
2925
  }
2625
2926
  list() {
2626
2927
  return [...this.items].sort((left, right) => right.updatedAt.localeCompare(left.updatedAt)).map((draft) => ({ ...draft }));
@@ -2652,7 +2953,7 @@ var DraftStore = class {
2652
2953
  return { ...existing };
2653
2954
  }
2654
2955
  const draft = {
2655
- id: randomUUID2(),
2956
+ id: randomUUID3(),
2656
2957
  accountId: this.accountId,
2657
2958
  ...fields,
2658
2959
  updatedAt: now
@@ -2677,8 +2978,8 @@ var DraftStore = class {
2677
2978
  init_imap();
2678
2979
 
2679
2980
  // 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";
2981
+ import { mkdirSync as mkdirSync6, readFileSync as readFileSync6, renameSync as renameSync5, writeFileSync as writeFileSync6 } from "node:fs";
2982
+ import { dirname as dirname4 } from "node:path";
2682
2983
  var ReadStateStore = class {
2683
2984
  file;
2684
2985
  keys = null;
@@ -2688,7 +2989,7 @@ var ReadStateStore = class {
2688
2989
  load() {
2689
2990
  if (this.keys !== null) return this.keys;
2690
2991
  try {
2691
- const parsed = JSON.parse(readFileSync5(this.file, "utf8"));
2992
+ const parsed = JSON.parse(readFileSync6(this.file, "utf8"));
2692
2993
  const list = Array.isArray(parsed) ? parsed : Array.isArray(parsed?.unread) ? parsed.unread : [];
2693
2994
  this.keys = new Set(list.filter((key) => typeof key === "string" && key !== ""));
2694
2995
  } catch {
@@ -2697,14 +2998,14 @@ var ReadStateStore = class {
2697
2998
  return this.keys;
2698
2999
  }
2699
3000
  flush() {
2700
- mkdirSync5(dirname3(this.file), { recursive: true });
3001
+ mkdirSync6(dirname4(this.file), { recursive: true });
2701
3002
  const temp = `${this.file}.${String(process.pid)}.tmp`;
2702
- writeFileSync5(temp, `${JSON.stringify({ unread: [...this.load()] }, null, 2)}
3003
+ writeFileSync6(temp, `${JSON.stringify({ unread: [...this.load()] }, null, 2)}
2703
3004
  `, {
2704
3005
  encoding: "utf8",
2705
3006
  mode: 384
2706
3007
  });
2707
- renameSync4(temp, this.file);
3008
+ renameSync5(temp, this.file);
2708
3009
  }
2709
3010
  isUnread(key) {
2710
3011
  return key !== "" && this.load().has(key);
@@ -2760,6 +3061,7 @@ var MailHarborRuntime = class {
2760
3061
  databases = /* @__PURE__ */ new Map();
2761
3062
  draftStores = /* @__PURE__ */ new Map();
2762
3063
  readStores = /* @__PURE__ */ new Map();
3064
+ aiStores = /* @__PURE__ */ new Map();
2763
3065
  backups = /* @__PURE__ */ new Map();
2764
3066
  llmProvider;
2765
3067
  aiService = null;
@@ -2823,6 +3125,7 @@ var MailHarborRuntime = class {
2823
3125
  }
2824
3126
  for (const id of [...this.draftStores.keys()]) if (!live.has(id)) this.draftStores.delete(id);
2825
3127
  for (const id of [...this.readStores.keys()]) if (!live.has(id)) this.readStores.delete(id);
3128
+ for (const id of [...this.aiStores.keys()]) if (!live.has(id)) this.aiStores.delete(id);
2826
3129
  }
2827
3130
  /** Drop cached connections and stores for one account after a settings change. */
2828
3131
  invalidate(accountId) {
@@ -2832,6 +3135,7 @@ var MailHarborRuntime = class {
2832
3135
  this.databases.delete(accountId);
2833
3136
  this.draftStores.delete(accountId);
2834
3137
  this.readStores.delete(accountId);
3138
+ this.aiStores.delete(accountId);
2835
3139
  this.refresh();
2836
3140
  }
2837
3141
  active() {
@@ -2890,6 +3194,14 @@ var MailHarborRuntime = class {
2890
3194
  this.draftStores.set(account.id, { signature: file, value: created });
2891
3195
  return created;
2892
3196
  }
3197
+ aiThreads(account) {
3198
+ const file = join5(this.paths(account).root, "ai-threads.json");
3199
+ const existing = this.aiStores.get(account.id);
3200
+ if (existing !== void 0 && existing.signature === file) return existing.value;
3201
+ const created = new AiThreadStore(file);
3202
+ this.aiStores.set(account.id, { signature: file, value: created });
3203
+ return created;
3204
+ }
2893
3205
  readState(account) {
2894
3206
  const file = join5(this.paths(account).root, "read-state.json");
2895
3207
  const existing = this.readStores.get(account.id);