@zaclinley/dsh-mailharbor 0.5.1 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.js CHANGED
@@ -214,7 +214,7 @@ var init_imap = __esm({
214
214
  "src/host/imap.ts"() {
215
215
  "use strict";
216
216
  init_address();
217
- ImapProvider = class {
217
+ ImapProvider = class _ImapProvider {
218
218
  settings;
219
219
  credentials;
220
220
  options;
@@ -222,6 +222,11 @@ var init_imap = __esm({
222
222
  connecting = null;
223
223
  queue = Promise.resolve();
224
224
  disposed = false;
225
+ mailboxCache = null;
226
+ countCache = /* @__PURE__ */ new Map();
227
+ pageCache = /* @__PURE__ */ new Map();
228
+ keepalive = null;
229
+ static MAILBOX_TTL_MS = 6e4;
225
230
  constructor(settings, credentials, options = {}) {
226
231
  this.settings = settings;
227
232
  this.credentials = credentials;
@@ -245,7 +250,10 @@ var init_imap = __esm({
245
250
  client.on("error", () => {
246
251
  });
247
252
  client.on("close", () => {
248
- if (this.client === client) this.client = null;
253
+ if (this.client === client) {
254
+ this.client = null;
255
+ this.stopKeepalive();
256
+ }
249
257
  });
250
258
  try {
251
259
  await client.connect();
@@ -255,6 +263,7 @@ var init_imap = __esm({
255
263
  );
256
264
  }
257
265
  this.client = client;
266
+ this.startKeepalive(client);
258
267
  return client;
259
268
  })();
260
269
  try {
@@ -263,6 +272,27 @@ var init_imap = __esm({
263
272
  this.connecting = null;
264
273
  }
265
274
  }
275
+ startKeepalive(client) {
276
+ this.stopKeepalive();
277
+ this.keepalive = setInterval(() => {
278
+ if (this.disposed || this.client !== client || !client.usable) return;
279
+ void this.run(async (active) => {
280
+ await active.noop();
281
+ }).catch(() => {
282
+ });
283
+ }, 24e4);
284
+ this.keepalive.unref?.();
285
+ }
286
+ stopKeepalive() {
287
+ if (this.keepalive !== null) {
288
+ clearInterval(this.keepalive);
289
+ this.keepalive = null;
290
+ }
291
+ }
292
+ /** Connect eagerly so the first list does not pay the handshake. */
293
+ async warm() {
294
+ await this.connect();
295
+ }
266
296
  async reset(reason) {
267
297
  const client = this.client;
268
298
  this.client = null;
@@ -303,34 +333,38 @@ var init_imap = __esm({
303
333
  return { ok: true, mailboxes: boxes.map((box) => ({ name: box.path, count: box.messages })) };
304
334
  });
305
335
  }
306
- async mailboxes() {
307
- return this.run((client) => this.listMailboxes(client));
336
+ async mailboxes(force = false) {
337
+ const cached = this.mailboxCache;
338
+ if (!force && cached !== null && Date.now() - cached.at < _ImapProvider.MAILBOX_TTL_MS) {
339
+ return cached.items;
340
+ }
341
+ const items = await this.run((client) => this.listMailboxes(client));
342
+ this.mailboxCache = { at: Date.now(), items };
343
+ return items;
308
344
  }
309
345
  async listMailboxes(client) {
310
346
  const listed = await client.list();
311
- const result = [];
312
- for (const box of listed) {
313
- if (box.flags?.has?.("\\Noselect") === true) continue;
314
- let messages = 0;
315
- try {
316
- const status = await client.status(box.path, { messages: true });
317
- if (status !== false) messages = Number(status.messages ?? 0);
318
- } catch {
319
- }
320
- result.push({
321
- path: box.path,
322
- name: box.name ?? box.path,
323
- specialUse: box.specialUse ?? null,
324
- messages
325
- });
326
- }
327
- return result;
347
+ const boxes = listed.filter((box) => box.flags?.has?.("\\Noselect") !== true);
348
+ return boxes.map((box) => ({
349
+ path: box.path,
350
+ name: box.name ?? box.path,
351
+ specialUse: box.specialUse ?? null,
352
+ messages: this.countCache.get(box.path) ?? 0
353
+ }));
328
354
  }
329
355
  async list(mailbox, limit, offset = 0) {
356
+ const cacheKey = `${mailbox}:${String(limit)}:${String(offset)}`;
357
+ const cached = this.pageCache.get(cacheKey);
358
+ if (cached !== void 0 && Date.now() - cached.at < 15e3) {
359
+ return { total: cached.total, items: cached.items };
360
+ }
330
361
  return this.run(async (client) => {
331
362
  const lock = await client.getMailboxLock(mailbox);
332
363
  try {
333
364
  const total = Number(client.mailbox?.exists ?? 0);
365
+ const cachedBox = this.mailboxCache?.items.find((box) => box.path === mailbox);
366
+ if (cachedBox !== void 0) cachedBox.messages = total;
367
+ this.countCache.set(mailbox, total);
334
368
  const end = total - offset;
335
369
  if (total === 0 || end < 1) return { total, items: [] };
336
370
  const start = Math.max(1, end - limit + 1);
@@ -345,12 +379,52 @@ var init_imap = __esm({
345
379
  items.push(toMessage(message));
346
380
  }
347
381
  items.sort((left, right) => right.date.localeCompare(left.date));
382
+ this.pageCache.set(cacheKey, { at: Date.now(), total, items });
383
+ if (this.pageCache.size > 30) {
384
+ const oldest = this.pageCache.keys().next().value;
385
+ if (oldest !== void 0) this.pageCache.delete(oldest);
386
+ }
348
387
  return { total, items };
349
388
  } finally {
350
389
  lock.release();
351
390
  }
352
391
  });
353
392
  }
393
+ async search(mailbox, options) {
394
+ return this.run(async (client) => {
395
+ const lock = await client.getMailboxLock(mailbox);
396
+ try {
397
+ const criteria = {};
398
+ if ((options.from ?? "") !== "") criteria.from = options.from;
399
+ if ((options.subject ?? "") !== "") criteria.subject = options.subject;
400
+ const keywords = (options.keywords ?? []).filter((keyword) => keyword !== "");
401
+ if (keywords.length > 0) criteria.text = keywords.join(" ");
402
+ if ((options.since ?? "") !== "") criteria.since = /* @__PURE__ */ new Date(`${String(options.since)}T00:00:00Z`);
403
+ if ((options.before ?? "") !== "") criteria.before = /* @__PURE__ */ new Date(`${String(options.before)}T23:59:59Z`);
404
+ if (Object.keys(criteria).length === 0) return { items: [], total: 0 };
405
+ const found = await client.search(criteria, { uid: true });
406
+ const uids = (Array.isArray(found) ? found : []).map((uid) => Number(uid)).filter((uid) => Number.isFinite(uid)).sort((left, right) => left - right);
407
+ const take = uids.slice(-Math.min(100, options.limit ?? 60));
408
+ if (take.length === 0) return { items: [], total: uids.length };
409
+ const wanted = new Set(take);
410
+ const items = [];
411
+ const first = take[0] ?? 0;
412
+ const last = take[take.length - 1] ?? 0;
413
+ for await (const message of client.fetch(
414
+ `${String(first)}:${String(last)}`,
415
+ { uid: true, envelope: true, flags: true, internalDate: true, size: true },
416
+ { uid: true }
417
+ )) {
418
+ const item = toMessage(message);
419
+ if (wanted.has(item.uid)) items.push(item);
420
+ }
421
+ items.sort((left, right) => right.date.localeCompare(left.date));
422
+ return { items, total: uids.length };
423
+ } finally {
424
+ lock.release();
425
+ }
426
+ });
427
+ }
354
428
  async read(mailbox, uid) {
355
429
  return this.run(async (client) => {
356
430
  const lock = await client.getMailboxLock(mailbox);
@@ -423,6 +497,7 @@ var init_imap = __esm({
423
497
  });
424
498
  }
425
499
  async setSeen(mailbox, uid, seen) {
500
+ this.pageCache.clear();
426
501
  return this.run(async (client) => {
427
502
  const lock = await client.getMailboxLock(mailbox);
428
503
  try {
@@ -461,6 +536,8 @@ var init_imap = __esm({
461
536
  }
462
537
  async dispose() {
463
538
  this.disposed = true;
539
+ this.pageCache.clear();
540
+ this.stopKeepalive();
464
541
  await this.reset("dispose");
465
542
  }
466
543
  };
@@ -742,6 +819,66 @@ function activeAccount(config) {
742
819
  return config.accounts.find((account) => account.id === config.activeAccountId) ?? null;
743
820
  }
744
821
 
822
+ // src/host/picker.ts
823
+ import { execFile } from "node:child_process";
824
+ import { createRequire } from "node:module";
825
+ import { promisify } from "node:util";
826
+ var run = promisify(execFile);
827
+ async function electronDialog(defaultPath) {
828
+ try {
829
+ const require2 = createRequire(import.meta.url);
830
+ const electron = require2("electron");
831
+ if (electron.dialog === void 0) return void 0;
832
+ const result = await electron.dialog.showOpenDialog({
833
+ title: "\u9009\u62E9\u9879\u76EE\u76EE\u5F55",
834
+ ...defaultPath === void 0 ? {} : { defaultPath },
835
+ properties: ["openDirectory", "createDirectory"]
836
+ });
837
+ return result.canceled ? null : result.filePaths[0] ?? null;
838
+ } catch {
839
+ return void 0;
840
+ }
841
+ }
842
+ async function pickDirectory(defaultPath) {
843
+ const electron = await electronDialog(defaultPath);
844
+ if (electron !== void 0) return electron;
845
+ if (process.platform === "darwin") {
846
+ try {
847
+ const { stdout } = await run(
848
+ "osascript",
849
+ ["-e", 'POSIX path of (choose folder with prompt "\u9009\u62E9\u9879\u76EE\u76EE\u5F55")'],
850
+ { timeout: 18e4 }
851
+ );
852
+ const path = stdout.trim();
853
+ return path === "" ? null : path.replace(/\/$/, "");
854
+ } catch {
855
+ return null;
856
+ }
857
+ }
858
+ if (process.platform === "win32") {
859
+ const script = [
860
+ "Add-Type -AssemblyName System.Windows.Forms",
861
+ "$d = New-Object System.Windows.Forms.FolderBrowserDialog",
862
+ '$d.Description = "\u9009\u62E9\u9879\u76EE\u76EE\u5F55"',
863
+ 'if ($d.ShowDialog() -eq "OK") { Write-Output $d.SelectedPath }'
864
+ ].join("; ");
865
+ try {
866
+ const { stdout } = await run("powershell", ["-NoProfile", "-Command", script], { timeout: 18e4 });
867
+ return stdout.trim() === "" ? null : stdout.trim();
868
+ } catch {
869
+ return null;
870
+ }
871
+ }
872
+ try {
873
+ const { stdout } = await run("zenity", ["--file-selection", "--directory", "--title=\u9009\u62E9\u9879\u76EE\u76EE\u5F55"], {
874
+ timeout: 18e4
875
+ });
876
+ return stdout.trim() === "" ? null : stdout.trim();
877
+ } catch {
878
+ throw new Error("\u5F53\u524D\u73AF\u5883\u65E0\u6CD5\u6253\u5F00\u76EE\u5F55\u9009\u62E9\u5668\uFF0C\u8BF7\u624B\u52A8\u8F93\u5165\u8DEF\u5F84");
879
+ }
880
+ }
881
+
745
882
  // src/host/backup.ts
746
883
  init_address();
747
884
  import { createHash as createHash2 } from "node:crypto";
@@ -929,6 +1066,58 @@ var MailDatabase = class {
929
1066
  ).all(...params, limit, offset);
930
1067
  return { items: rows.map((row) => rowToSummary(row)), total, offset, limit };
931
1068
  }
1069
+ /** Structured search used by AI search (keywords + sender + dates). */
1070
+ searchAdvanced(options) {
1071
+ const db = this.open();
1072
+ const where = [];
1073
+ const params = [];
1074
+ const mailbox = String(options.mailbox ?? "all");
1075
+ if (mailbox !== "" && mailbox !== "all") {
1076
+ where.push("e.mailbox = ?");
1077
+ params.push(mailbox);
1078
+ }
1079
+ const keywords = (options.keywords ?? []).filter((keyword) => keyword !== "");
1080
+ if (keywords.length > 0) {
1081
+ const clauses = [];
1082
+ for (const keyword of keywords) {
1083
+ clauses.push(
1084
+ "(e.subject LIKE ? OR e.from_name LIKE ? OR e.from_addr LIKE ? OR e.to_addr LIKE ? OR e.body_text LIKE ?)"
1085
+ );
1086
+ const like = `%${keyword}%`;
1087
+ params.push(like, like, like, like, like);
1088
+ }
1089
+ where.push(`(${clauses.join(" OR ")})`);
1090
+ }
1091
+ if ((options.from ?? "") !== "") {
1092
+ const like = `%${String(options.from)}%`;
1093
+ where.push("(e.from_name LIKE ? OR e.from_addr LIKE ?)");
1094
+ params.push(like, like);
1095
+ }
1096
+ if ((options.subject ?? "") !== "") {
1097
+ where.push("e.subject LIKE ?");
1098
+ params.push(`%${String(options.subject)}%`);
1099
+ }
1100
+ if ((options.since ?? "") !== "") {
1101
+ where.push("substr(e.date, 1, 10) >= ?");
1102
+ params.push(String(options.since));
1103
+ }
1104
+ if ((options.before ?? "") !== "") {
1105
+ where.push("substr(e.date, 1, 10) <= ?");
1106
+ params.push(String(options.before));
1107
+ }
1108
+ const limit = Math.min(100, Math.max(1, options.limit ?? 60));
1109
+ if (where.length === 0) return { items: [], total: 0, offset: 0, limit };
1110
+ const clause = `WHERE ${where.join(" AND ")}`;
1111
+ const columns = `SELECT e.id, e.mailbox, e.uid, e.subject, e.from_name, e.from_addr, e.to_addr, e.cc,
1112
+ e.message_id, e.date, length(e.body_text) AS text_length,
1113
+ (SELECT COUNT(*) FROM attachments a WHERE a.email_id = e.id) AS attachment_count,
1114
+ substr(COALESCE(e.body_text, ''), 1, 240) AS snippet`;
1115
+ const total = Number(
1116
+ db.prepare(`SELECT COUNT(*) AS count FROM emails e ${clause}`).get(...params)?.count ?? 0
1117
+ );
1118
+ const rows = db.prepare(`${columns} FROM emails e ${clause} ORDER BY e.date DESC, e.id DESC LIMIT ?`).all(...params, limit);
1119
+ return { items: rows.map((row) => rowToSummary(row)), total, offset: 0, limit };
1120
+ }
932
1121
  get(localId) {
933
1122
  const db = this.open();
934
1123
  const row = db.prepare(
@@ -1081,8 +1270,8 @@ function normalizeId(value) {
1081
1270
  function stamp(value) {
1082
1271
  const date = value instanceof Date ? value : value === void 0 || value === null ? void 0 : new Date(String(value));
1083
1272
  if (!(date instanceof Date) || Number.isNaN(date.getTime())) return "";
1084
- const pad2 = (number) => String(number).padStart(2, "0");
1085
- return `${String(date.getFullYear())}-${pad2(date.getMonth() + 1)}-${pad2(date.getDate())} ${pad2(date.getHours())}:${pad2(date.getMinutes())}:${pad2(date.getSeconds())}`;
1273
+ const pad = (number) => String(number).padStart(2, "0");
1274
+ return `${String(date.getFullYear())}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
1086
1275
  }
1087
1276
  function abortIfNeeded(signal) {
1088
1277
  if (signal?.aborted === true) {
@@ -1557,6 +1746,17 @@ ${shared}${styleLine}`;
1557
1746
  \u8981\u6C42\uFF1A
1558
1747
  ${shared}${styleLine}`;
1559
1748
  };
1749
+ var SEARCH_SYSTEM = `\u4F60\u662F\u90AE\u4EF6\u641C\u7D22\u52A9\u624B\u3002\u628A\u7528\u6237\u7684\u4E00\u53E5\u8BDD\u641C\u7D22\u9700\u6C42\u7FFB\u8BD1\u6210\u7ED3\u6784\u5316\u6761\u4EF6\u3002
1750
+
1751
+ \u53EA\u8F93\u51FA\u4E00\u4E2A JSON \u5BF9\u8C61\uFF0C\u4E0D\u8981\u89E3\u91CA\u6216\u4EE3\u7801\u5757\uFF1A
1752
+ {"keywords": ["\u5173\u952E\u8BCD"], "from": "\u53D1\u4EF6\u4EBA\u6216\u7A7A\u5B57\u7B26\u4E32", "subject": "\u4E3B\u9898\u5173\u952E\u8BCD\u6216\u7A7A\u5B57\u7B26\u4E32", "since": "YYYY-MM-DD \u6216\u7A7A\u5B57\u7B26\u4E32", "before": "YYYY-MM-DD \u6216\u7A7A\u5B57\u7B26\u4E32", "explanation": "\u4E00\u53E5\u4E2D\u6587\u8BF4\u660E\u4F60\u7406\u89E3\u6210\u4E86\u4EC0\u4E48"}
1753
+
1754
+ \u89C4\u5219\uFF1A
1755
+ - \u4ECA\u5929\u662F {{today}}\uFF0C\u76F8\u5BF9\u65F6\u95F4\uFF08\u4E0A\u5468\u3001\u4E0A\u4E2A\u6708\u3001\u6700\u8FD1\u4E09\u5929\uFF09\u6362\u7B97\u6210 since/before
1756
+ - keywords \u53EA\u653E\u6B63\u6587/\u4E3B\u9898\u91CC\u53EF\u80FD\u51FA\u73B0\u7684\u5B9E\u8BCD\uFF0C2-4 \u4E2A\u5373\u53EF
1757
+ - subject \u53EA\u5728\u7528\u6237\u660E\u786E\u8BF4\u201C\u4E3B\u9898\u662F/\u6807\u9898\u662F\u201D\u65F6\u586B\u5199\uFF0C\u5426\u5219\u7559\u7A7A\uFF1B\u4E0D\u8981\u628A\u666E\u901A\u5173\u952E\u8BCD\u585E\u8FDB subject
1758
+ - \u4E0D\u786E\u5B9A\u7684\u5B57\u6BB5\u7559\u7A7A\u5B57\u7B26\u4E32\uFF0C\u4E0D\u8981\u7F16\u9020
1759
+ - \u6CA1\u6709\u53EF\u7528\u6761\u4EF6\u65F6 keywords \u4E3A\u7A7A\u6570\u7EC4`;
1560
1760
  var TASK_SYSTEM = `\u4F60\u662F\u4EFB\u52A1\u63D0\u53D6\u52A9\u624B\u3002\u4ECE\u90AE\u4EF6\u4E2D\u63D0\u53D6\u9700\u8981\u7528\u6237\u5904\u7406\u7684\u5177\u4F53\u4E8B\u9879\u3002
1561
1761
 
1562
1762
  \u53EA\u8F93\u51FA\u4E00\u4E2A JSON \u6570\u7EC4\uFF0C\u4E0D\u8981\u8F93\u51FA\u89E3\u91CA\u3001Markdown \u6216\u4EE3\u7801\u5757\u6807\u8BB0\u3002\u6BCF\u4E2A\u5143\u7D20\uFF1A
@@ -1743,6 +1943,13 @@ ${clip(item, 4e3)}`).join("\n\n");
1743
1943
  ].join("\n");
1744
1944
  return this.complete(ASK_SYSTEM, user, signal);
1745
1945
  }
1946
+ /** Translate a natural-language search request into structured filters. */
1947
+ async parseSearch(query, signal) {
1948
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
1949
+ const system = SEARCH_SYSTEM.replace("{{today}}", today);
1950
+ const text2 = await this.complete(system, `\u641C\u7D22\u9700\u6C42\uFF1A${query.trim()}`, signal);
1951
+ return parseSearchJson(text2);
1952
+ }
1746
1953
  /** Extract actionable tasks from one mail. */
1747
1954
  async extractTasks(message, signal) {
1748
1955
  const text2 = await this.complete(TASK_SYSTEM, contextOf(message), signal);
@@ -1806,6 +2013,37 @@ function normalizeTranslated(value) {
1806
2013
  function htmlToPlain(html) {
1807
2014
  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();
1808
2015
  }
2016
+ function parseSearchJson(text2) {
2017
+ const start = text2.indexOf("{");
2018
+ const end = text2.lastIndexOf("}");
2019
+ const empty = {
2020
+ keywords: [],
2021
+ from: "",
2022
+ subject: "",
2023
+ since: "",
2024
+ before: "",
2025
+ explanation: ""
2026
+ };
2027
+ if (start < 0 || end <= start) return empty;
2028
+ let parsed;
2029
+ try {
2030
+ parsed = JSON.parse(text2.slice(start, end + 1));
2031
+ } catch {
2032
+ return empty;
2033
+ }
2034
+ if (parsed === null || typeof parsed !== "object") return empty;
2035
+ const value = parsed;
2036
+ const keywords = Array.isArray(value.keywords) ? value.keywords.map((item) => String(item).trim()).filter((item) => item !== "").slice(0, 4) : [];
2037
+ const date = (input) => /^\d{4}-\d{2}-\d{2}$/.test(String(input ?? "")) ? String(input) : "";
2038
+ return {
2039
+ keywords,
2040
+ from: String(value.from ?? "").trim().slice(0, 120),
2041
+ subject: String(value.subject ?? "").trim().slice(0, 120),
2042
+ since: date(value.since),
2043
+ before: date(value.before),
2044
+ explanation: String(value.explanation ?? "").trim().slice(0, 200)
2045
+ };
2046
+ }
1809
2047
  function parseTaskJson(text2) {
1810
2048
  const start = text2.indexOf("[");
1811
2049
  const end = text2.lastIndexOf("]");
@@ -1859,365 +2097,6 @@ function textToHtml(text2) {
1859
2097
  ).join("");
1860
2098
  }
1861
2099
 
1862
- // src/host/ics.ts
1863
- var CRLF = "\r\n";
1864
- function escapeText(value) {
1865
- return String(value ?? "").replaceAll("\\", "\\\\").replaceAll(";", "\\;").replaceAll(",", "\\,").replaceAll("\r\n", "\\n").replaceAll("\n", "\\n").replaceAll("\r", "\\n");
1866
- }
1867
- function unescapeText(value) {
1868
- return String(value ?? "").replaceAll("\\n", "\n").replaceAll("\\N", "\n").replaceAll("\\,", ",").replaceAll("\\;", ";").replaceAll("\\\\", "\\");
1869
- }
1870
- function fold(line2) {
1871
- const text2 = String(line2);
1872
- if (text2.length <= 74) return text2;
1873
- const chunks = [];
1874
- let rest = text2;
1875
- while (rest.length > 74) {
1876
- chunks.push(rest.slice(0, 74));
1877
- rest = ` ${rest.slice(74)}`;
1878
- }
1879
- chunks.push(rest);
1880
- return chunks.join(CRLF);
1881
- }
1882
- function utcStamp(date) {
1883
- const pad2 = (value) => String(value).padStart(2, "0");
1884
- return `${String(date.getUTCFullYear())}${pad2(date.getUTCMonth() + 1)}${pad2(date.getUTCDate())}T${pad2(date.getUTCHours())}${pad2(date.getUTCMinutes())}${pad2(date.getUTCSeconds())}Z`;
1885
- }
1886
- function pad(value) {
1887
- return String(value).padStart(2, "0");
1888
- }
1889
- function dueProperty(due) {
1890
- const text2 = String(due ?? "").trim();
1891
- if (text2 === "") return null;
1892
- if (/^\d{4}-\d{2}-\d{2}$/.test(text2)) return `DUE;VALUE=DATE:${text2.replaceAll("-", "")}`;
1893
- const parsed = new Date(text2);
1894
- if (Number.isNaN(parsed.getTime())) return null;
1895
- return `DUE:${utcStamp(parsed)}`;
1896
- }
1897
- function serializeTodos(tasks) {
1898
- const lines = [
1899
- "BEGIN:VCALENDAR",
1900
- "VERSION:2.0",
1901
- "PRODID:-//dsh-mailharbor//VTODO//EN",
1902
- "CALSCALE:GREGORIAN"
1903
- ];
1904
- for (const task of tasks) {
1905
- lines.push("BEGIN:VTODO");
1906
- lines.push(`UID:${String(task.uid ?? task.id ?? "")}`);
1907
- lines.push(`DTSTAMP:${utcStamp(/* @__PURE__ */ new Date())}`);
1908
- lines.push(`SUMMARY:${escapeText(task.title ?? "(\u65E0\u6807\u9898)")}`);
1909
- if (task.notes !== void 0 && task.notes !== null && String(task.notes) !== "") {
1910
- lines.push(`DESCRIPTION:${escapeText(task.notes)}`);
1911
- }
1912
- const due = dueProperty(String(task.due ?? ""));
1913
- if (due !== null) lines.push(due);
1914
- lines.push(`STATUS:${String(task.status ?? "NEEDS-ACTION").toUpperCase()}`);
1915
- if (Number(task.priority ?? 0) > 0)
1916
- lines.push(`PRIORITY:${String(Math.max(0, Math.min(9, Number(task.priority))))}`);
1917
- if (String(task.status ?? "").toUpperCase() === "COMPLETED") {
1918
- const completed = task.completedAt === void 0 || task.completedAt === null ? /* @__PURE__ */ new Date() : new Date(task.completedAt);
1919
- if (!Number.isNaN(completed.getTime())) lines.push(`COMPLETED:${utcStamp(completed)}`);
1920
- }
1921
- lines.push("END:VTODO");
1922
- }
1923
- lines.push("END:VCALENDAR");
1924
- return `${lines.map(fold).join(CRLF)}${CRLF}`;
1925
- }
1926
- function unfold(text2) {
1927
- return String(text2 ?? "").replace(/\r?\n[ \t]/g, "");
1928
- }
1929
- function decodeDue(value, params) {
1930
- const text2 = String(value ?? "").trim();
1931
- if (params.includes("VALUE=DATE") || /^\d{8}$/.test(text2)) {
1932
- if (/^\d{8}$/.test(text2)) return `${text2.slice(0, 4)}-${text2.slice(4, 6)}-${text2.slice(6, 8)}`;
1933
- return "";
1934
- }
1935
- const match = /^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})Z$/.exec(text2);
1936
- if (match !== null) {
1937
- const date = new Date(
1938
- Date.UTC(
1939
- Number(match[1]),
1940
- Number(match[2]) - 1,
1941
- Number(match[3]),
1942
- Number(match[4]),
1943
- Number(match[5]),
1944
- Number(match[6])
1945
- )
1946
- );
1947
- return `${String(date.getFullYear())}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}`;
1948
- }
1949
- return "";
1950
- }
1951
- function parseTodos(ics) {
1952
- const lines = unfold(ics).split(/\r?\n/);
1953
- const todos = [];
1954
- let current = null;
1955
- for (const line2 of lines) {
1956
- const trimmed = line2.trim();
1957
- if (trimmed === "BEGIN:VTODO") {
1958
- current = {};
1959
- continue;
1960
- }
1961
- if (trimmed === "END:VTODO") {
1962
- if (current !== null && String(current.uid ?? "") !== "") {
1963
- todos.push({
1964
- uid: String(current.uid),
1965
- title: String(current.title ?? "(\u65E0\u6807\u9898)"),
1966
- notes: String(current.notes ?? ""),
1967
- due: String(current.due ?? ""),
1968
- status: String(current.status ?? "NEEDS-ACTION"),
1969
- priority: Number(current.priority ?? 0),
1970
- completedAt: current.completedAt ?? null
1971
- });
1972
- }
1973
- current = null;
1974
- continue;
1975
- }
1976
- if (current === null) continue;
1977
- const colon = trimmed.indexOf(":");
1978
- if (colon < 0) continue;
1979
- const left = trimmed.slice(0, colon);
1980
- const value = trimmed.slice(colon + 1);
1981
- const semicolon = left.indexOf(";");
1982
- const name2 = (semicolon < 0 ? left : left.slice(0, semicolon)).toUpperCase();
1983
- const params = semicolon < 0 ? "" : left.slice(semicolon + 1).toUpperCase();
1984
- if (name2 === "UID") current.uid = unescapeText(value).trim();
1985
- else if (name2 === "SUMMARY") current.title = unescapeText(value);
1986
- else if (name2 === "DESCRIPTION") current.notes = unescapeText(value);
1987
- else if (name2 === "STATUS") current.status = value.trim().toUpperCase();
1988
- else if (name2 === "PRIORITY") current.priority = Number(value) || 0;
1989
- else if (name2 === "DUE") current.due = decodeDue(value, params);
1990
- else if (name2 === "COMPLETED") {
1991
- const match = /^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})Z$/.exec(value.trim());
1992
- if (match !== null) {
1993
- current.completedAt = new Date(
1994
- Date.UTC(
1995
- Number(match[1]),
1996
- Number(match[2]) - 1,
1997
- Number(match[3]),
1998
- Number(match[4]),
1999
- Number(match[5]),
2000
- Number(match[6])
2001
- )
2002
- ).toISOString();
2003
- }
2004
- }
2005
- }
2006
- return todos;
2007
- }
2008
-
2009
- // src/host/caldav.ts
2010
- function decodeXml(value) {
2011
- return String(value ?? "").replaceAll("&lt;", "<").replaceAll("&gt;", ">").replaceAll("&quot;", '"').replaceAll("&apos;", "'").replaceAll("&amp;", "&");
2012
- }
2013
- function tagValue(xml, name2) {
2014
- const pattern = new RegExp(`<(?:[\\w-]+:)?${name2}\\b[^>]*>([\\s\\S]*?)</(?:[\\w-]+:)?${name2}>`, "i");
2015
- const match = pattern.exec(String(xml ?? ""));
2016
- return match === null ? "" : decodeXml(match[1] ?? "");
2017
- }
2018
- function responseBlocks(xml) {
2019
- return [
2020
- ...String(xml ?? "").matchAll(/<(?:[\w-]+:)?response\b[^>]*>([\s\S]*?)<\/(?:[\w-]+:)?response>/gi)
2021
- ].map((match) => match[1] ?? "");
2022
- }
2023
- function resolveHref(base, href) {
2024
- const text2 = String(href ?? "").trim();
2025
- if (text2 === "") return "";
2026
- return new URL(text2, base).href;
2027
- }
2028
- function withSlash(url) {
2029
- return url.endsWith("/") ? url : `${url}/`;
2030
- }
2031
- var PROPFIND_ROOT = `<?xml version="1.0" encoding="utf-8"?>
2032
- <d:propfind xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav">
2033
- <d:prop><d:current-user-principal/><c:calendar-home-set/></d:prop>
2034
- </d:propfind>`;
2035
- var PROPFIND_HOME = `<?xml version="1.0" encoding="utf-8"?>
2036
- <d:propfind xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav">
2037
- <d:prop><d:resourcetype/><d:displayname/><c:supported-calendar-component-set/></d:prop>
2038
- </d:propfind>`;
2039
- var REPORT_TODOS = `<?xml version="1.0" encoding="utf-8"?>
2040
- <c:calendar-query xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav">
2041
- <d:prop><d:getetag/><c:calendar-data/></d:prop>
2042
- <c:filter><c:comp-filter name="VCALENDAR"><c:comp-filter name="VTODO"/></c:comp-filter></c:filter>
2043
- </c:calendar-query>`;
2044
- var CalDavClient = class {
2045
- url;
2046
- username;
2047
- password;
2048
- calendarHref;
2049
- constructor(config, password) {
2050
- this.url = withSlash(String(config.url ?? "").trim());
2051
- this.username = String(config.username ?? "");
2052
- this.password = String(password ?? "");
2053
- this.calendarHref = String(config.calendarHref ?? "").trim();
2054
- }
2055
- headers(extra = {}) {
2056
- const auth = Buffer.from(`${this.username}:${this.password}`, "utf8").toString("base64");
2057
- return { Authorization: `Basic ${auth}`, ...extra };
2058
- }
2059
- async request(method, url, options = {}) {
2060
- if (this.url === "") throw new Error("CalDAV \u5730\u5740\u672A\u914D\u7F6E");
2061
- const controller = new AbortController();
2062
- const timer = setTimeout(() => {
2063
- controller.abort();
2064
- }, 2e4);
2065
- let response;
2066
- try {
2067
- response = await fetch(url, {
2068
- method,
2069
- headers: this.headers({
2070
- ...options.depth === void 0 ? {} : { Depth: String(options.depth) },
2071
- ...options.body === void 0 ? {} : { "Content-Type": "application/xml; charset=utf-8" },
2072
- ...options.headers ?? {}
2073
- }),
2074
- body: options.body,
2075
- signal: controller.signal,
2076
- redirect: "follow"
2077
- });
2078
- } catch (error) {
2079
- if (error instanceof Error && error.name === "AbortError") throw new Error("CalDAV \u8FDE\u63A5\u8D85\u65F6");
2080
- throw new Error(`CalDAV \u8FDE\u63A5\u5931\u8D25\uFF1A${error instanceof Error ? error.message : String(error)}`);
2081
- } finally {
2082
- clearTimeout(timer);
2083
- }
2084
- const text2 = await response.text();
2085
- if (response.status === 401 || response.status === 403)
2086
- throw new Error("CalDAV \u8BA4\u8BC1\u5931\u8D25\uFF0C\u8BF7\u68C0\u67E5\u7528\u6237\u540D\u548C\u5E94\u7528\u5BC6\u7801");
2087
- if (response.status === 404) throw new Error("CalDAV \u5730\u5740\u6216\u65E5\u5386\u4E0D\u5B58\u5728");
2088
- if (response.status === 412) throw new Error("CalDAV \u4EFB\u52A1\u5DF2\u88AB\u5176\u4ED6\u5BA2\u6237\u7AEF\u4FEE\u6539\uFF0C\u8BF7\u5148\u540C\u6B65");
2089
- if (!response.ok && response.status !== 207) {
2090
- throw new Error(`CalDAV \u8FD4\u56DE ${String(response.status)}${text2 === "" ? "" : `\uFF1A${text2.slice(0, 160)}`}`);
2091
- }
2092
- return { status: response.status, text: text2, headers: response.headers };
2093
- }
2094
- async discover() {
2095
- if (this.calendarHref !== "") return this.calendarHref;
2096
- const root = await this.request("PROPFIND", this.url, { body: PROPFIND_ROOT, depth: 0 });
2097
- const principalBlock = tagValue(root.text, "current-user-principal");
2098
- const principal = tagValue(principalBlock, "href") || (principalBlock.trim().startsWith("/") ? principalBlock.trim() : "");
2099
- const homeBlock = tagValue(root.text, "calendar-home-set");
2100
- let home = tagValue(homeBlock, "href") || (homeBlock.trim().startsWith("/") ? homeBlock.trim() : "");
2101
- if (home === "") {
2102
- if (principal === "") throw new Error("CalDAV \u670D\u52A1\u6CA1\u6709\u8FD4\u56DE calendar-home-set");
2103
- const principalUrl = resolveHref(this.url, principal);
2104
- const principalResponse = await this.request("PROPFIND", principalUrl, {
2105
- body: PROPFIND_ROOT,
2106
- depth: 0
2107
- });
2108
- const principalHome = tagValue(principalResponse.text, "calendar-home-set");
2109
- home = tagValue(principalHome, "href") || (principalHome.trim().startsWith("/") ? principalHome.trim() : "");
2110
- if (home === "") throw new Error("CalDAV \u670D\u52A1\u6CA1\u6709\u8FD4\u56DE\u65E5\u5386\u76EE\u5F55");
2111
- }
2112
- const homeUrl = withSlash(resolveHref(this.url, home));
2113
- const listing = await this.request("PROPFIND", homeUrl, { body: PROPFIND_HOME, depth: 1 });
2114
- const calendars = [];
2115
- for (const block of responseBlocks(listing.text)) {
2116
- const href = tagValue(block, "href");
2117
- if (href === "") continue;
2118
- if (!/calendar/i.test(tagValue(block, "resourcetype"))) continue;
2119
- const supported = tagValue(block, "supported-calendar-component-set");
2120
- if (supported !== "" && !/VTODO/i.test(supported)) continue;
2121
- calendars.push({
2122
- href: withSlash(resolveHref(homeUrl, href)),
2123
- displayName: tagValue(block, "displayname") || href
2124
- });
2125
- }
2126
- if (calendars.length === 0) throw new Error("\u6CA1\u6709\u627E\u5230\u652F\u6301 VTODO \u7684\u4EFB\u52A1\u65E5\u5386");
2127
- this.calendarHref = calendars[0]?.href ?? "";
2128
- return this.calendarHref;
2129
- }
2130
- async listCalendars() {
2131
- if (this.calendarHref !== "") return [{ href: this.calendarHref, displayName: "" }];
2132
- const root = await this.request("PROPFIND", this.url, { body: PROPFIND_ROOT, depth: 0 });
2133
- const homeBlock = tagValue(root.text, "calendar-home-set");
2134
- const home = tagValue(homeBlock, "href") || (homeBlock.trim().startsWith("/") ? homeBlock.trim() : "");
2135
- if (home === "") throw new Error("CalDAV \u670D\u52A1\u6CA1\u6709\u8FD4\u56DE\u65E5\u5386\u76EE\u5F55");
2136
- const homeUrl = withSlash(resolveHref(this.url, home));
2137
- const listing = await this.request("PROPFIND", homeUrl, { body: PROPFIND_HOME, depth: 1 });
2138
- return responseBlocks(listing.text).filter((block) => /calendar/i.test(tagValue(block, "resourcetype"))).map((block) => ({
2139
- href: withSlash(resolveHref(homeUrl, tagValue(block, "href"))),
2140
- displayName: tagValue(block, "displayname")
2141
- }));
2142
- }
2143
- async fetchTodos(collectionHref) {
2144
- const collection = withSlash(resolveHref(this.url, collectionHref));
2145
- const response = await this.request("REPORT", collection, { body: REPORT_TODOS, depth: 1 });
2146
- const items = [];
2147
- for (const block of responseBlocks(response.text)) {
2148
- const href = tagValue(block, "href");
2149
- if (href === "") continue;
2150
- const etag = tagValue(block, "getetag") || null;
2151
- const data = tagValue(block, "calendar-data");
2152
- if (data === "") continue;
2153
- for (const todo of parseTodos(data)) {
2154
- if (todo.uid === "") continue;
2155
- items.push({ href: resolveHref(collection, href), etag, todo });
2156
- }
2157
- }
2158
- return items;
2159
- }
2160
- async putTodo(collectionHref, task) {
2161
- const collection = withSlash(resolveHref(this.url, collectionHref));
2162
- const href = task.href !== null && task.href !== "" ? task.href : new URL(`${encodeURIComponent(task.uid)}.ics`, collection).href;
2163
- const headers = { "Content-Type": "text/calendar; charset=utf-8" };
2164
- if (task.etag !== null && task.etag !== "") headers["If-Match"] = task.etag;
2165
- else headers["If-None-Match"] = "*";
2166
- const response = await this.request("PUT", href, { body: serializeTodos([task]), headers });
2167
- return { href, etag: response.headers.get("etag") ?? task.etag, uid: task.uid };
2168
- }
2169
- async deleteTodo(href, etag) {
2170
- const headers = {};
2171
- if (etag !== null && etag !== "") headers["If-Match"] = etag;
2172
- await this.request("DELETE", resolveHref(this.url, href), { headers });
2173
- }
2174
- /** Pull remote VTODOs, then push local dirty/tombstoned tasks. */
2175
- async sync(store, accountId) {
2176
- const collection = await this.discover();
2177
- const remote = await this.fetchTodos(collection);
2178
- let pulled = 0;
2179
- for (const item of remote) {
2180
- store.applyRemote({
2181
- accountId,
2182
- calendarHref: collection,
2183
- href: item.href,
2184
- etag: item.etag,
2185
- todo: item.todo
2186
- });
2187
- pulled += 1;
2188
- }
2189
- const removed = store.pruneMissingRemote(
2190
- accountId,
2191
- collection,
2192
- remote.map((item) => item.href)
2193
- );
2194
- let pushed = 0;
2195
- let deleted = 0;
2196
- for (const task of store.pending(accountId)) {
2197
- if (task.deleted) {
2198
- if (task.href !== null && task.href !== "") {
2199
- await this.deleteTodo(task.href, task.etag);
2200
- deleted += 1;
2201
- }
2202
- store.purge(task.id);
2203
- continue;
2204
- }
2205
- const result = await this.putTodo(
2206
- task.calendarHref !== null && task.calendarHref !== "" ? task.calendarHref : collection,
2207
- task
2208
- );
2209
- store.markPushed(task.id, result);
2210
- pushed += 1;
2211
- }
2212
- store.setCalendarState(accountId, {
2213
- calendarHref: collection,
2214
- lastSync: (/* @__PURE__ */ new Date()).toISOString(),
2215
- error: null
2216
- });
2217
- return { pulled, pushed, deleted, removed, calendarHref: collection, total: remote.length };
2218
- }
2219
- };
2220
-
2221
2100
  // src/host/projects.ts
2222
2101
  import { randomUUID as randomUUID2 } from "node:crypto";
2223
2102
  import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync4, renameSync as renameSync3, writeFileSync as writeFileSync4 } from "node:fs";
@@ -2430,7 +2309,7 @@ ${conversation.trim()}
2430
2309
  }
2431
2310
 
2432
2311
  // src/host/version.ts
2433
- var VERSION = "0.5.1";
2312
+ var VERSION = "0.7.0";
2434
2313
 
2435
2314
  // src/host/api.ts
2436
2315
  init_smtp();
@@ -2494,7 +2373,7 @@ function buildRouter(runtime) {
2494
2373
  const accounts = requested === "all" ? runtime.current.accounts.filter((item) => item.enabled !== false) : [accountFrom(runtime, url).account];
2495
2374
  if (source === "server") {
2496
2375
  const provider = runtime.provider(accounts[0]);
2497
- const boxes = await provider.mailboxes();
2376
+ const boxes = await provider.mailboxes(url.searchParams.get("refresh") === "1");
2498
2377
  return {
2499
2378
  items: boxes.map((box) => ({ name: box.path, label: box.name, count: box.messages }))
2500
2379
  };
@@ -2833,6 +2712,53 @@ function buildRouter(runtime) {
2833
2712
  throw aiHttpError(error);
2834
2713
  }
2835
2714
  });
2715
+ router.post("/ai/search", async ({ body }) => {
2716
+ const payload = await body();
2717
+ const account = accountFromPayload(runtime, payload);
2718
+ const query = String(payload.query ?? "").trim();
2719
+ if (query === "") throw new HttpError(400, "\u641C\u7D22\u5185\u5BB9\u4E0D\u80FD\u4E3A\u7A7A");
2720
+ const source = sourceOfPayload(payload);
2721
+ const mailbox = String(payload.mailbox ?? "all");
2722
+ try {
2723
+ const filters = await runtime.ai().parseSearch(query);
2724
+ const hasCondition = filters.keywords.length > 0 || filters.from !== "" || filters.subject !== "" || filters.since !== "" || filters.before !== "";
2725
+ if (!hasCondition) throw new HttpError(400, "\u6CA1\u6709\u7406\u89E3\u51FA\u53EF\u7528\u7684\u641C\u7D22\u6761\u4EF6\uFF0C\u8BF7\u6362\u4E2A\u8BF4\u6CD5");
2726
+ if (source === "server") {
2727
+ const provider = runtime.provider(account);
2728
+ const result = await provider.search(mailbox === "all" ? "INBOX" : mailbox, {
2729
+ ...filters,
2730
+ limit: 60
2731
+ });
2732
+ return {
2733
+ items: result.items.map((item) => ({ ...item, accountId: account.id })),
2734
+ total: result.total,
2735
+ offset: 0,
2736
+ limit: 60,
2737
+ ai: filters
2738
+ };
2739
+ }
2740
+ const database = runtime.database(account);
2741
+ const keywords = [...filters.keywords];
2742
+ let page = database.searchAdvanced({ ...filters, subject: "", keywords, mailbox: "all", limit: 60 });
2743
+ if (page.total === 0 && filters.subject !== "") {
2744
+ page = database.searchAdvanced({
2745
+ ...filters,
2746
+ subject: "",
2747
+ keywords: [...keywords, filters.subject],
2748
+ mailbox: "all",
2749
+ limit: 60
2750
+ });
2751
+ }
2752
+ return {
2753
+ ...page,
2754
+ items: page.items.map((item) => ({ ...item, accountId: account.id })),
2755
+ ai: filters
2756
+ };
2757
+ } catch (error) {
2758
+ if (error instanceof HttpError) throw error;
2759
+ throw aiHttpError(error);
2760
+ }
2761
+ });
2836
2762
  router.post("/ai/compose", async ({ body }) => {
2837
2763
  const payload = await body();
2838
2764
  const account = accountFromPayload(runtime, payload);
@@ -2933,7 +2859,7 @@ function buildRouter(runtime) {
2933
2859
  projectId: url.searchParams.get("projectId") ?? "",
2934
2860
  query: url.searchParams.get("query") ?? ""
2935
2861
  });
2936
- return { items: items.map(taskView), calendar: runtime.tasks().calendarState(account.id) };
2862
+ return { items: items.map(taskView) };
2937
2863
  });
2938
2864
  router.post("/tasks", async ({ body }) => {
2939
2865
  const payload = await body();
@@ -2987,58 +2913,6 @@ function buildRouter(runtime) {
2987
2913
  throw aiHttpError(error);
2988
2914
  }
2989
2915
  });
2990
- router.get("/tasks/caldav", ({ url }) => {
2991
- const account = accountFromPayload(runtime, { accountId: url.searchParams.get("accountId") ?? void 0 });
2992
- return {
2993
- caldav: account.caldav,
2994
- calendar: runtime.tasks().calendarState(account.id),
2995
- hasPassword: runtime.credentials.readSecret(`${account.keychainService}:caldav`, account.email) !== void 0
2996
- };
2997
- });
2998
- router.put("/tasks/caldav", async ({ body }) => {
2999
- const payload = await body();
3000
- const account = accountFromPayload(runtime, payload);
3001
- const url = String(payload.url ?? "").trim();
3002
- const username = String(payload.username ?? "").trim();
3003
- const calendarHref = String(payload.calendarHref ?? "").trim();
3004
- const caldav = url === "" ? null : { url, username, ...calendarHref === "" ? {} : { calendarHref } };
3005
- const accounts = runtime.current.accounts.map(
3006
- (item) => item.id === account.id ? { ...item, caldav } : item
3007
- );
3008
- saveAccounts(accounts, runtime.current.activeAccountId);
3009
- if (typeof payload.password === "string" && payload.password !== "") {
3010
- runtime.credentials.writeSecret(`${account.keychainService}:caldav`, account.email, payload.password);
3011
- }
3012
- runtime.refresh();
3013
- const updated = runtime.current.accounts.find((item) => item.id === account.id) ?? account;
3014
- return {
3015
- caldav: updated.caldav,
3016
- title: "",
3017
- hasPassword: runtime.credentials.readSecret(`${updated.keychainService}:caldav`, updated.email) !== void 0
3018
- };
3019
- });
3020
- router.post("/tasks/sync", async ({ body }) => {
3021
- const payload = await body();
3022
- const account = accountFromPayload(runtime, payload);
3023
- const caldav = account.caldav;
3024
- if (caldav === null) throw new HttpError(400, "\u8BF7\u5148\u914D\u7F6E CalDAV \u5730\u5740\u548C\u8D26\u53F7");
3025
- const password = runtime.credentials.readSecret(`${account.keychainService}:caldav`, account.email);
3026
- if (password === void 0 || password === "") throw new HttpError(400, "\u8BF7\u5148\u4FDD\u5B58 CalDAV \u5E94\u7528\u5BC6\u7801");
3027
- try {
3028
- const client = new CalDavClient(caldav, password);
3029
- const result = await runtime.tasks().sync(account.id, client);
3030
- if (result.calendarHref !== "" && result.calendarHref !== (caldav.calendarHref ?? "")) {
3031
- const accounts = runtime.current.accounts.map(
3032
- (item) => item.id === account.id ? { ...item, caldav: { ...caldav, calendarHref: result.calendarHref } } : item
3033
- );
3034
- saveAccounts(accounts, runtime.current.activeAccountId);
3035
- runtime.refresh();
3036
- }
3037
- return { result, calendar: runtime.tasks().calendarState(account.id) };
3038
- } catch (error) {
3039
- throw new HttpError(400, error instanceof Error ? error.message : String(error));
3040
- }
3041
- });
3042
2916
  router.get("/projects", () => ({ items: runtime.projects().list().map(projectView) }));
3043
2917
  router.get("/projects/for", ({ url }) => {
3044
2918
  const account = accountFromPayload(runtime, { accountId: url.searchParams.get("accountId") ?? void 0 });
@@ -3050,6 +2924,10 @@ function buildRouter(runtime) {
3050
2924
  })()
3051
2925
  };
3052
2926
  });
2927
+ router.post("/projects/pick-directory", async () => {
2928
+ const path = await pickDirectory();
2929
+ return { path };
2930
+ });
3053
2931
  router.post("/projects", async ({ body }) => {
3054
2932
  const payload = await body();
3055
2933
  const account = accountFromPayload(runtime, payload);
@@ -4065,41 +3943,6 @@ var TaskStore = class {
4065
3943
  }
4066
3944
  return removed;
4067
3945
  }
4068
- calendarState(accountId) {
4069
- const row = this.open().prepare("SELECT * FROM calendar_state WHERE account_id = ?").get(accountId);
4070
- if (row === void 0) return null;
4071
- return {
4072
- calendarHref: String(row.calendar_href ?? ""),
4073
- lastSync: String(row.last_sync ?? ""),
4074
- error: String(row.error ?? "")
4075
- };
4076
- }
4077
- setCalendarState(accountId, state) {
4078
- this.open().prepare(
4079
- `INSERT INTO calendar_state (account_id, calendar_href, display_name, ctag, last_sync, error)
4080
- VALUES (?, ?, NULL, NULL, ?, ?)
4081
- ON CONFLICT(account_id) DO UPDATE SET calendar_href = excluded.calendar_href,
4082
- last_sync = excluded.last_sync, error = excluded.error`
4083
- ).run(accountId, state.calendarHref ?? null, state.lastSync ?? null, state.error ?? null);
4084
- }
4085
- /** Pull remote tasks, push local edits, and record the sync result. */
4086
- async sync(accountId, client) {
4087
- try {
4088
- const result = await client.sync(this, accountId);
4089
- this.setCalendarState(accountId, {
4090
- calendarHref: result.calendarHref,
4091
- lastSync: (/* @__PURE__ */ new Date()).toISOString(),
4092
- error: null
4093
- });
4094
- return result;
4095
- } catch (error) {
4096
- this.setCalendarState(accountId, {
4097
- lastSync: (/* @__PURE__ */ new Date()).toISOString(),
4098
- error: error instanceof Error ? error.message : String(error)
4099
- });
4100
- throw error;
4101
- }
4102
- }
4103
3946
  };
4104
3947
 
4105
3948
  // src/host/drafts.ts