@zaclinley/dsh-mailharbor 0.6.0 → 0.7.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
@@ -390,6 +390,41 @@ var init_imap = __esm({
390
390
  }
391
391
  });
392
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
+ }
393
428
  async read(mailbox, uid) {
394
429
  return this.run(async (client) => {
395
430
  const lock = await client.getMailboxLock(mailbox);
@@ -784,6 +819,66 @@ function activeAccount(config) {
784
819
  return config.accounts.find((account) => account.id === config.activeAccountId) ?? null;
785
820
  }
786
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
+
787
882
  // src/host/backup.ts
788
883
  init_address();
789
884
  import { createHash as createHash2 } from "node:crypto";
@@ -971,6 +1066,58 @@ var MailDatabase = class {
971
1066
  ).all(...params, limit, offset);
972
1067
  return { items: rows.map((row) => rowToSummary(row)), total, offset, limit };
973
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
+ }
974
1121
  get(localId) {
975
1122
  const db = this.open();
976
1123
  const row = db.prepare(
@@ -1599,6 +1746,17 @@ ${shared}${styleLine}`;
1599
1746
  \u8981\u6C42\uFF1A
1600
1747
  ${shared}${styleLine}`;
1601
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`;
1602
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
1603
1761
 
1604
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
@@ -1785,6 +1943,13 @@ ${clip(item, 4e3)}`).join("\n\n");
1785
1943
  ].join("\n");
1786
1944
  return this.complete(ASK_SYSTEM, user, signal);
1787
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
+ }
1788
1953
  /** Extract actionable tasks from one mail. */
1789
1954
  async extractTasks(message, signal) {
1790
1955
  const text2 = await this.complete(TASK_SYSTEM, contextOf(message), signal);
@@ -1848,6 +2013,37 @@ function normalizeTranslated(value) {
1848
2013
  function htmlToPlain(html) {
1849
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();
1850
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
+ }
1851
2047
  function parseTaskJson(text2) {
1852
2048
  const start = text2.indexOf("[");
1853
2049
  const end = text2.lastIndexOf("]");
@@ -2113,7 +2309,7 @@ ${conversation.trim()}
2113
2309
  }
2114
2310
 
2115
2311
  // src/host/version.ts
2116
- var VERSION = "0.6.0";
2312
+ var VERSION = "0.7.1";
2117
2313
 
2118
2314
  // src/host/api.ts
2119
2315
  init_smtp();
@@ -2516,6 +2712,53 @@ function buildRouter(runtime) {
2516
2712
  throw aiHttpError(error);
2517
2713
  }
2518
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
+ });
2519
2762
  router.post("/ai/compose", async ({ body }) => {
2520
2763
  const payload = await body();
2521
2764
  const account = accountFromPayload(runtime, payload);
@@ -2681,6 +2924,10 @@ function buildRouter(runtime) {
2681
2924
  })()
2682
2925
  };
2683
2926
  });
2927
+ router.post("/projects/pick-directory", async () => {
2928
+ const path = await pickDirectory();
2929
+ return { path };
2930
+ });
2684
2931
  router.post("/projects", async ({ body }) => {
2685
2932
  const payload = await body();
2686
2933
  const account = accountFromPayload(runtime, payload);