@zaclinley/dsh-mailharbor 0.4.1 → 0.5.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/CHANGELOG.md +16 -0
- package/README.md +1 -1
- package/README.zh.md +1 -1
- package/lib/client.js +2023 -742
- package/lib/client.js.map +4 -4
- package/lib/index.js +1279 -40
- package/lib/index.js.map +4 -4
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -469,11 +469,13 @@ var init_imap = __esm({
|
|
|
469
469
|
|
|
470
470
|
// src/host/api.ts
|
|
471
471
|
init_address();
|
|
472
|
-
import { randomUUID as
|
|
472
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
473
473
|
|
|
474
474
|
// src/host/config.ts
|
|
475
475
|
import { createHash, randomUUID } from "node:crypto";
|
|
476
476
|
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
477
|
+
import { homedir as homedir2 } from "node:os";
|
|
478
|
+
import { join as join2 } from "node:path";
|
|
477
479
|
|
|
478
480
|
// src/host/paths.ts
|
|
479
481
|
import { homedir } from "node:os";
|
|
@@ -484,6 +486,14 @@ var ACCOUNTS_DIR = join(DATA_ROOT, "accounts");
|
|
|
484
486
|
var STATE_DIR = join(DATA_ROOT, "state");
|
|
485
487
|
var REPORTS_DIR = join(DATA_ROOT, "reports");
|
|
486
488
|
var LOGS_DIR = join(DATA_ROOT, "logs");
|
|
489
|
+
function resolveUserPath(value, label) {
|
|
490
|
+
const text2 = value.trim();
|
|
491
|
+
if (text2 === "") throw new Error(`${label}\u4E0D\u80FD\u4E3A\u7A7A`);
|
|
492
|
+
if (text2.length > 4096) throw new Error(`${label}\u8FC7\u957F`);
|
|
493
|
+
const expanded = text2 === "~" ? homedir() : text2.startsWith("~/") ? join(homedir(), text2.slice(2)) : text2;
|
|
494
|
+
if (!isAbsolute(expanded)) throw new Error(`${label}\u5FC5\u987B\u662F\u7EDD\u5BF9\u8DEF\u5F84`);
|
|
495
|
+
return expanded;
|
|
496
|
+
}
|
|
487
497
|
function accountRoot(id) {
|
|
488
498
|
const safe = id.replace(/[^A-Za-z0-9_-]/g, "_") || "unknown";
|
|
489
499
|
return join(ACCOUNTS_DIR, safe);
|
|
@@ -512,6 +522,7 @@ var DEFAULTS = {
|
|
|
512
522
|
},
|
|
513
523
|
read: { markOnOpen: true, pollSeconds: 60 },
|
|
514
524
|
render: { listLimit: 40 },
|
|
525
|
+
projects: { root: join2(homedir2(), "work", "projects") },
|
|
515
526
|
oauth: {
|
|
516
527
|
google: { clientId: "", redirectPort: 8765, scopes: ["https://mail.google.com/"] },
|
|
517
528
|
microsoft: {
|
|
@@ -625,6 +636,7 @@ function loadConfig() {
|
|
|
625
636
|
const aiRaw = file.ai ?? {};
|
|
626
637
|
const readRaw = file.read ?? {};
|
|
627
638
|
const renderRaw = file.render ?? {};
|
|
639
|
+
const projectsRaw = file.projects ?? {};
|
|
628
640
|
const oauthRaw = file.oauth ?? {};
|
|
629
641
|
const googleRaw = oauthRaw.google ?? {};
|
|
630
642
|
const microsoftRaw = oauthRaw.microsoft ?? {};
|
|
@@ -648,6 +660,9 @@ function loadConfig() {
|
|
|
648
660
|
render: {
|
|
649
661
|
listLimit: intOr(renderRaw.listLimit, DEFAULTS.render.listLimit, 10, 200)
|
|
650
662
|
},
|
|
663
|
+
projects: {
|
|
664
|
+
root: stringOr(projectsRaw.root, DEFAULTS.projects.root)
|
|
665
|
+
},
|
|
651
666
|
oauth: {
|
|
652
667
|
google: {
|
|
653
668
|
clientId: stringOr(googleRaw.clientId, DEFAULTS.oauth.google.clientId),
|
|
@@ -731,7 +746,7 @@ function activeAccount(config) {
|
|
|
731
746
|
init_address();
|
|
732
747
|
import { createHash as createHash2 } from "node:crypto";
|
|
733
748
|
import { existsSync as existsSync3, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
734
|
-
import { join as
|
|
749
|
+
import { join as join3 } from "node:path";
|
|
735
750
|
import { ImapFlow } from "imapflow";
|
|
736
751
|
import { simpleParser } from "mailparser";
|
|
737
752
|
import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
|
|
@@ -961,6 +976,14 @@ var MailDatabase = class {
|
|
|
961
976
|
data
|
|
962
977
|
};
|
|
963
978
|
}
|
|
979
|
+
/** Locate a local row by Message-ID, for links from tasks and projects. */
|
|
980
|
+
findByMessageId(messageId) {
|
|
981
|
+
const id = messageId.trim().replace(/^<|>$/g, "");
|
|
982
|
+
if (id === "") return void 0;
|
|
983
|
+
const row = this.open().prepare("SELECT id, mailbox FROM emails WHERE message_id = ? LIMIT 1").get(id);
|
|
984
|
+
if (row === void 0) return void 0;
|
|
985
|
+
return { id: Number(row.id), mailbox: String(row.mailbox ?? "") };
|
|
986
|
+
}
|
|
964
987
|
/** Whether the backup already holds a message with this Message-ID. */
|
|
965
988
|
hasMessageId(messageId) {
|
|
966
989
|
const id = messageId.trim().replace(/^<|>$/g, "");
|
|
@@ -1058,8 +1081,8 @@ function normalizeId(value) {
|
|
|
1058
1081
|
function stamp(value) {
|
|
1059
1082
|
const date = value instanceof Date ? value : value === void 0 || value === null ? void 0 : new Date(String(value));
|
|
1060
1083
|
if (!(date instanceof Date) || Number.isNaN(date.getTime())) return "";
|
|
1061
|
-
const
|
|
1062
|
-
return `${String(date.getFullYear())}-${
|
|
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())}`;
|
|
1063
1086
|
}
|
|
1064
1087
|
function abortIfNeeded(signal) {
|
|
1065
1088
|
if (signal?.aborted === true) {
|
|
@@ -1090,8 +1113,8 @@ var BackupEngine = class {
|
|
|
1090
1113
|
}
|
|
1091
1114
|
writeAttachment(data) {
|
|
1092
1115
|
const hash = sha256(data);
|
|
1093
|
-
const dir =
|
|
1094
|
-
const file =
|
|
1116
|
+
const dir = join3(this.attachmentsDir(), hash.slice(0, 2), hash.slice(2, 4));
|
|
1117
|
+
const file = join3(dir, hash);
|
|
1095
1118
|
mkdirSync2(dir, { recursive: true });
|
|
1096
1119
|
if (!existsSync3(file)) writeFileSync2(file, data, { mode: 384 });
|
|
1097
1120
|
return { hash, size: data.length };
|
|
@@ -1469,7 +1492,7 @@ var Router = class {
|
|
|
1469
1492
|
|
|
1470
1493
|
// src/host/ai.ts
|
|
1471
1494
|
import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync3, renameSync as renameSync2, writeFileSync as writeFileSync3 } from "node:fs";
|
|
1472
|
-
import { dirname, join as
|
|
1495
|
+
import { dirname, join as join4 } from "node:path";
|
|
1473
1496
|
var TIMEOUT_MS = 9e4;
|
|
1474
1497
|
var MAX_PROMPT_CHARS = 4e4;
|
|
1475
1498
|
var SUMMARY_SYSTEM = `\u4F60\u662F\u90AE\u4EF6\u52A9\u7406\uFF0C\u670D\u52A1\u4E8E\u4E2D\u6587\u7528\u6237\u3002\u8BF7\u7528\u7B80\u4F53\u4E2D\u6587\u603B\u7ED3\u90AE\u4EF6\uFF0C\u4E25\u683C\u6309\u4E0B\u9762\u56DB\u6BB5\u8F93\u51FA\uFF0C\u4F7F\u7528\u7ED9\u5B9A\u7684\u5C0F\u6807\u9898\uFF0C\u4E0D\u8981\u8F93\u51FA\u5176\u4ED6\u5185\u5BB9\uFF1A
|
|
@@ -1534,11 +1557,21 @@ ${shared}${styleLine}`;
|
|
|
1534
1557
|
\u8981\u6C42\uFF1A
|
|
1535
1558
|
${shared}${styleLine}`;
|
|
1536
1559
|
};
|
|
1560
|
+
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
|
+
|
|
1562
|
+
\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
|
|
1563
|
+
{"title": "\u4EFB\u52A1\u6807\u9898\uFF08\u52A8\u8BCD\u5F00\u5934\uFF0C\u5C3D\u91CF\u5177\u4F53\uFF09", "due": "YYYY-MM-DD \u6216\u7A7A\u5B57\u7B26\u4E32", "priority": 0 \u5230 9 \u7684\u6574\u6570, "owner": "\u8D1F\u8D23\u4EBA\u6216\u7A7A\u5B57\u7B26\u4E32", "notes": "\u4E00\u53E5\u6765\u6E90\u4F9D\u636E"}
|
|
1564
|
+
|
|
1565
|
+
\u89C4\u5219\uFF1A
|
|
1566
|
+
- \u53EA\u63D0\u53D6\u90AE\u4EF6\u91CC\u660E\u786E\u8981\u6C42\u3001\u6216\u660E\u663E\u9700\u8981\u8DDF\u8FDB\u7684\u4E8B\u9879
|
|
1567
|
+
- \u622A\u6B62\u65F6\u95F4\u80FD\u786E\u5B9A\u624D\u586B\uFF0C\u5426\u5219\u7559\u7A7A
|
|
1568
|
+
- \u6CA1\u6709\u53EF\u6267\u884C\u4E8B\u9879\u65F6\u8F93\u51FA []
|
|
1569
|
+
- \u4E0D\u8981\u7F16\u9020\u90AE\u4EF6\u91CC\u6CA1\u6709\u7684\u4FE1\u606F`;
|
|
1537
1570
|
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
|
|
1538
1571
|
\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
|
|
1539
1572
|
\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`;
|
|
1540
1573
|
function stylePath(root) {
|
|
1541
|
-
return
|
|
1574
|
+
return join4(root, "style.json");
|
|
1542
1575
|
}
|
|
1543
1576
|
function readStyle(root) {
|
|
1544
1577
|
try {
|
|
@@ -1710,6 +1743,11 @@ ${clip(item, 4e3)}`).join("\n\n");
|
|
|
1710
1743
|
].join("\n");
|
|
1711
1744
|
return this.complete(ASK_SYSTEM, user, signal);
|
|
1712
1745
|
}
|
|
1746
|
+
/** Extract actionable tasks from one mail. */
|
|
1747
|
+
async extractTasks(message, signal) {
|
|
1748
|
+
const text2 = await this.complete(TASK_SYSTEM, contextOf(message), signal);
|
|
1749
|
+
return parseTaskJson(text2);
|
|
1750
|
+
}
|
|
1713
1751
|
/** Compose-body assistance: draft from points, polish, or shorten. */
|
|
1714
1752
|
async compose(account, options, signal) {
|
|
1715
1753
|
const style = this.style(account)?.profile ?? "";
|
|
@@ -1768,6 +1806,33 @@ function normalizeTranslated(value) {
|
|
|
1768
1806
|
function htmlToPlain(html) {
|
|
1769
1807
|
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(/ /g, " ").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/\n{3,}/g, "\n\n").trim();
|
|
1770
1808
|
}
|
|
1809
|
+
function parseTaskJson(text2) {
|
|
1810
|
+
const start = text2.indexOf("[");
|
|
1811
|
+
const end = text2.lastIndexOf("]");
|
|
1812
|
+
if (start < 0 || end <= start) return [];
|
|
1813
|
+
let parsed;
|
|
1814
|
+
try {
|
|
1815
|
+
parsed = JSON.parse(text2.slice(start, end + 1));
|
|
1816
|
+
} catch {
|
|
1817
|
+
return [];
|
|
1818
|
+
}
|
|
1819
|
+
if (!Array.isArray(parsed)) return [];
|
|
1820
|
+
const result = [];
|
|
1821
|
+
for (const item of parsed) {
|
|
1822
|
+
if (item === null || typeof item !== "object") continue;
|
|
1823
|
+
const value = item;
|
|
1824
|
+
const title = String(value.title ?? "").trim();
|
|
1825
|
+
if (title === "") continue;
|
|
1826
|
+
result.push({
|
|
1827
|
+
title: title.slice(0, 300),
|
|
1828
|
+
due: /^\d{4}-\d{2}-\d{2}/.test(String(value.due ?? "")) ? String(value.due).slice(0, 10) : "",
|
|
1829
|
+
priority: Math.max(0, Math.min(9, Number(value.priority ?? 0) || 0)),
|
|
1830
|
+
owner: String(value.owner ?? "").slice(0, 120),
|
|
1831
|
+
notes: String(value.notes ?? "").slice(0, 1e3)
|
|
1832
|
+
});
|
|
1833
|
+
}
|
|
1834
|
+
return result.slice(0, 30);
|
|
1835
|
+
}
|
|
1771
1836
|
function summaryBlocks(text2) {
|
|
1772
1837
|
const blocks = [];
|
|
1773
1838
|
let current = { title: "", lines: [] };
|
|
@@ -1794,8 +1859,578 @@ function textToHtml(text2) {
|
|
|
1794
1859
|
).join("");
|
|
1795
1860
|
}
|
|
1796
1861
|
|
|
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("<", "<").replaceAll(">", ">").replaceAll(""", '"').replaceAll("'", "'").replaceAll("&", "&");
|
|
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
|
+
// src/host/projects.ts
|
|
2222
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
2223
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync4, renameSync as renameSync3, writeFileSync as writeFileSync4 } from "node:fs";
|
|
2224
|
+
import { join as join5 } from "node:path";
|
|
2225
|
+
var ProjectStore = class {
|
|
2226
|
+
file;
|
|
2227
|
+
cache = null;
|
|
2228
|
+
constructor(file) {
|
|
2229
|
+
this.file = file;
|
|
2230
|
+
}
|
|
2231
|
+
load() {
|
|
2232
|
+
if (this.cache !== null) return this.cache;
|
|
2233
|
+
let parsed = null;
|
|
2234
|
+
try {
|
|
2235
|
+
if (existsSync5(this.file)) parsed = JSON.parse(readFileSync4(this.file, "utf8"));
|
|
2236
|
+
} catch {
|
|
2237
|
+
parsed = null;
|
|
2238
|
+
}
|
|
2239
|
+
const projects = parsed !== null && typeof parsed === "object" && Array.isArray(parsed.projects) ? parsed.projects : [];
|
|
2240
|
+
this.cache = { version: 1, projects: projects.filter(isProject) };
|
|
2241
|
+
return this.cache;
|
|
2242
|
+
}
|
|
2243
|
+
flush() {
|
|
2244
|
+
const data = this.load();
|
|
2245
|
+
mkdirSync4(join5(this.file, ".."), { recursive: true });
|
|
2246
|
+
const temp = `${this.file}.${String(process.pid)}.tmp`;
|
|
2247
|
+
writeFileSync4(temp, `${JSON.stringify(data, null, 2)}
|
|
2248
|
+
`, { mode: 384 });
|
|
2249
|
+
renameSync3(temp, this.file);
|
|
2250
|
+
}
|
|
2251
|
+
list() {
|
|
2252
|
+
return [...this.load().projects].sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
|
|
2253
|
+
}
|
|
2254
|
+
get(id) {
|
|
2255
|
+
return this.load().projects.find((project) => project.id === id);
|
|
2256
|
+
}
|
|
2257
|
+
findByMessage(accountId, key) {
|
|
2258
|
+
return this.load().projects.find(
|
|
2259
|
+
(project) => project.messages.some((message) => message.accountId === accountId && message.key === key)
|
|
2260
|
+
);
|
|
2261
|
+
}
|
|
2262
|
+
create(record) {
|
|
2263
|
+
const data = this.load();
|
|
2264
|
+
data.projects.push(record);
|
|
2265
|
+
this.flush();
|
|
2266
|
+
return record;
|
|
2267
|
+
}
|
|
2268
|
+
update(id, patch) {
|
|
2269
|
+
const project = this.get(id);
|
|
2270
|
+
if (project === void 0) return void 0;
|
|
2271
|
+
Object.assign(project, patch, { updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
2272
|
+
this.flush();
|
|
2273
|
+
return project;
|
|
2274
|
+
}
|
|
2275
|
+
addMessage(id, message) {
|
|
2276
|
+
const project = this.get(id);
|
|
2277
|
+
if (project === void 0) return void 0;
|
|
2278
|
+
const exists = project.messages.some(
|
|
2279
|
+
(item) => item.accountId === message.accountId && item.key === message.key
|
|
2280
|
+
);
|
|
2281
|
+
if (!exists) project.messages.push(message);
|
|
2282
|
+
if (!project.accountIds.includes(message.accountId)) project.accountIds.push(message.accountId);
|
|
2283
|
+
project.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2284
|
+
this.flush();
|
|
2285
|
+
return project;
|
|
2286
|
+
}
|
|
2287
|
+
removeMessage(id, accountId, key) {
|
|
2288
|
+
const project = this.get(id);
|
|
2289
|
+
if (project === void 0) return void 0;
|
|
2290
|
+
project.messages = project.messages.filter((item) => !(item.accountId === accountId && item.key === key));
|
|
2291
|
+
project.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2292
|
+
this.flush();
|
|
2293
|
+
return project;
|
|
2294
|
+
}
|
|
2295
|
+
addThread(id, thread) {
|
|
2296
|
+
const project = this.get(id);
|
|
2297
|
+
if (project === void 0) return void 0;
|
|
2298
|
+
if (!project.threads.some((item) => item.accountId === thread.accountId && item.key === thread.key)) {
|
|
2299
|
+
project.threads.push(thread);
|
|
2300
|
+
}
|
|
2301
|
+
project.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2302
|
+
this.flush();
|
|
2303
|
+
return project;
|
|
2304
|
+
}
|
|
2305
|
+
};
|
|
2306
|
+
function isProject(value) {
|
|
2307
|
+
return value !== null && typeof value === "object" && typeof value.id === "string" && typeof value.path === "string";
|
|
2308
|
+
}
|
|
2309
|
+
function slugify(name2) {
|
|
2310
|
+
const text2 = name2.trim().toLowerCase().replace(/[^\p{L}\p{N}]+/gu, "-").replace(/^-+|-+$/g, "");
|
|
2311
|
+
return text2 === "" ? "project" : text2.slice(0, 60);
|
|
2312
|
+
}
|
|
2313
|
+
async function createProject(options) {
|
|
2314
|
+
const { store, input } = options;
|
|
2315
|
+
const name2 = input.name.trim();
|
|
2316
|
+
if (name2 === "") throw new Error("\u9879\u76EE\u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A");
|
|
2317
|
+
if (name2.length > 120) throw new Error("\u9879\u76EE\u540D\u79F0\u8FC7\u957F");
|
|
2318
|
+
const root = input.root.trim();
|
|
2319
|
+
if (root === "") throw new Error("\u9879\u76EE\u76EE\u5F55\u4E0D\u80FD\u4E3A\u7A7A");
|
|
2320
|
+
const warnings = [];
|
|
2321
|
+
const id = `prj_${randomUUID2().slice(0, 8)}`;
|
|
2322
|
+
const base = slugify(name2);
|
|
2323
|
+
let path = join5(root, base);
|
|
2324
|
+
let attempt = 2;
|
|
2325
|
+
while (existsSync5(path)) {
|
|
2326
|
+
path = join5(root, `${base}-${String(attempt)}`);
|
|
2327
|
+
attempt += 1;
|
|
2328
|
+
}
|
|
2329
|
+
mkdirSync4(path, { recursive: true });
|
|
2330
|
+
const created = (/* @__PURE__ */ new Date()).toISOString();
|
|
2331
|
+
const brief = renderBrief(name2, input);
|
|
2332
|
+
writeFileSync4(join5(path, "BRIEF.md"), brief, { mode: 384 });
|
|
2333
|
+
writeFileSync4(join5(path, "CONVERSATION.md"), renderConversation(input.conversation, created), {
|
|
2334
|
+
mode: 384
|
|
2335
|
+
});
|
|
2336
|
+
let workspaceId = "";
|
|
2337
|
+
if (options.registry !== void 0) {
|
|
2338
|
+
try {
|
|
2339
|
+
const workspace = await options.registry.create(path, name2);
|
|
2340
|
+
workspaceId = String(workspace?.id ?? "");
|
|
2341
|
+
} catch (error) {
|
|
2342
|
+
warnings.push(`\u5DE5\u4F5C\u7A7A\u95F4\u6CE8\u518C\u5931\u8D25\uFF1A${error instanceof Error ? error.message : String(error)}`);
|
|
2343
|
+
}
|
|
2344
|
+
} else {
|
|
2345
|
+
warnings.push("\u5F53\u524D DSH \u7EC4\u5408\u6CA1\u6709 workspaceRegistry\uFF0C\u9879\u76EE\u76EE\u5F55\u5DF2\u521B\u5EFA\u4F46\u6CA1\u6709\u6CE8\u518C\u5230\u4FA7\u680F");
|
|
2346
|
+
}
|
|
2347
|
+
let sessionId = "";
|
|
2348
|
+
if (options.sessions !== void 0) {
|
|
2349
|
+
try {
|
|
2350
|
+
sessionId = randomUUID2();
|
|
2351
|
+
const session = options.sessions.create(sessionId, { meta: { cwd: path } });
|
|
2352
|
+
session.append(
|
|
2353
|
+
"user/message",
|
|
2354
|
+
{
|
|
2355
|
+
role: "user",
|
|
2356
|
+
content: [{ type: "text", text: `\u6211\u521B\u5EFA\u4E86\u4E00\u4E2A\u90AE\u4EF6\u9879\u76EE\u300C${name2}\u300D\u3002
|
|
2357
|
+
|
|
2358
|
+
${brief}` }],
|
|
2359
|
+
source: { kind: "user" }
|
|
2360
|
+
},
|
|
2361
|
+
{ surfaceOp: "append" }
|
|
2362
|
+
);
|
|
2363
|
+
} catch (error) {
|
|
2364
|
+
warnings.push(`\u4F1A\u8BDD\u521B\u5EFA\u5931\u8D25\uFF1A${error instanceof Error ? error.message : String(error)}`);
|
|
2365
|
+
sessionId = "";
|
|
2366
|
+
}
|
|
2367
|
+
} else {
|
|
2368
|
+
warnings.push("\u5F53\u524D DSH \u7EC4\u5408\u6CA1\u6709 sessions \u670D\u52A1\uFF0C\u672A\u521B\u5EFA\u9879\u76EE\u4F1A\u8BDD");
|
|
2369
|
+
}
|
|
2370
|
+
const project = {
|
|
2371
|
+
id,
|
|
2372
|
+
name: name2,
|
|
2373
|
+
path,
|
|
2374
|
+
workspaceId,
|
|
2375
|
+
sessionId,
|
|
2376
|
+
accountIds: [input.accountId],
|
|
2377
|
+
messages: input.message === void 0 ? [] : [input.message],
|
|
2378
|
+
threads: input.thread === void 0 ? [] : [input.thread],
|
|
2379
|
+
createdAt: created,
|
|
2380
|
+
updatedAt: created
|
|
2381
|
+
};
|
|
2382
|
+
writeFileSync4(
|
|
2383
|
+
join5(path, "project.json"),
|
|
2384
|
+
`${JSON.stringify({ ...project, conversation: void 0 }, null, 2)}
|
|
2385
|
+
`,
|
|
2386
|
+
{
|
|
2387
|
+
mode: 384
|
|
2388
|
+
}
|
|
2389
|
+
);
|
|
2390
|
+
store.create(project);
|
|
2391
|
+
return { project, warnings };
|
|
2392
|
+
}
|
|
2393
|
+
function appendConversation(project, conversation) {
|
|
2394
|
+
const stamp3 = (/* @__PURE__ */ new Date()).toISOString();
|
|
2395
|
+
const file = join5(project.path, "CONVERSATION.md");
|
|
2396
|
+
const block = `
|
|
2397
|
+
|
|
2398
|
+
## \u8FFD\u52A0\u8BB0\u5F55 \xB7 ${stamp3}
|
|
2399
|
+
|
|
2400
|
+
${conversation.trim()}
|
|
2401
|
+
`;
|
|
2402
|
+
writeFileSync4(file, block, { flag: "a", mode: 384 });
|
|
2403
|
+
return file;
|
|
2404
|
+
}
|
|
2405
|
+
function renderBrief(name2, input) {
|
|
2406
|
+
const lines = [`# ${name2}`, "", `- \u521B\u5EFA\u65F6\u95F4\uFF1A${(/* @__PURE__ */ new Date()).toISOString()}`, `- \u6765\u6E90\u8D26\u53F7\uFF1A${input.accountId}`];
|
|
2407
|
+
if (input.message !== void 0) {
|
|
2408
|
+
lines.push(`- \u6765\u6E90\u90AE\u4EF6\uFF1A${input.message.subject}\uFF08${input.message.date}\uFF09`);
|
|
2409
|
+
}
|
|
2410
|
+
lines.push(
|
|
2411
|
+
"",
|
|
2412
|
+
"## \u80CC\u666F",
|
|
2413
|
+
"",
|
|
2414
|
+
"\u7531 MailHarbor \u4ECE\u90AE\u4EF6\u751F\u6210\u3002\u4EE5\u4E0B\u662F\u5F53\u65F6 AI \u5DE5\u4F5C\u53F0\u7684\u8BB0\u5F55\u3002",
|
|
2415
|
+
"",
|
|
2416
|
+
"## AI \u5DE5\u4F5C\u53F0\u8BB0\u5F55",
|
|
2417
|
+
"",
|
|
2418
|
+
input.conversation.trim(),
|
|
2419
|
+
""
|
|
2420
|
+
);
|
|
2421
|
+
return lines.join("\n");
|
|
2422
|
+
}
|
|
2423
|
+
function renderConversation(conversation, created) {
|
|
2424
|
+
return `# AI \u5DE5\u4F5C\u53F0\u8BB0\u5F55
|
|
2425
|
+
|
|
2426
|
+
- \u8F6C\u79FB\u5230\u9879\u76EE\uFF1A${created}
|
|
2427
|
+
|
|
2428
|
+
${conversation.trim()}
|
|
2429
|
+
`;
|
|
2430
|
+
}
|
|
2431
|
+
|
|
1797
2432
|
// src/host/version.ts
|
|
1798
|
-
var VERSION = "0.
|
|
2433
|
+
var VERSION = "0.5.0";
|
|
1799
2434
|
|
|
1800
2435
|
// src/host/api.ts
|
|
1801
2436
|
init_smtp();
|
|
@@ -1901,6 +2536,22 @@ function buildRouter(runtime) {
|
|
|
1901
2536
|
const page = runtime.database(account).list({ mailbox: mailbox === "" ? "all" : mailbox, query, offset, limit });
|
|
1902
2537
|
return { ...page, items: page.items.map((item) => localSummary(runtime, account, item)) };
|
|
1903
2538
|
});
|
|
2539
|
+
router.get("/messages/lookup", ({ url }) => {
|
|
2540
|
+
const account = accountFromPayload(runtime, { accountId: url.searchParams.get("accountId") ?? void 0 });
|
|
2541
|
+
const key = url.searchParams.get("key") ?? "";
|
|
2542
|
+
if (key.startsWith("local:")) {
|
|
2543
|
+
const id = Number(key.slice(6));
|
|
2544
|
+
const detail = runtime.database(account).get(id);
|
|
2545
|
+
if (detail === void 0) throw new HttpError(404, "\u90AE\u4EF6\u4E0D\u5B58\u5728");
|
|
2546
|
+
return { id: String(id), mailbox: detail.mailbox, subject: detail.subject };
|
|
2547
|
+
}
|
|
2548
|
+
if (key.startsWith("mid:")) {
|
|
2549
|
+
const found = runtime.database(account).findByMessageId(key.slice(4));
|
|
2550
|
+
if (found === void 0) throw new HttpError(404, "\u672C\u5730\u5907\u4EFD\u91CC\u627E\u4E0D\u5230\u8FD9\u5C01\u90AE\u4EF6");
|
|
2551
|
+
return { id: String(found.id), mailbox: found.mailbox, subject: "" };
|
|
2552
|
+
}
|
|
2553
|
+
throw new HttpError(400, "\u65E0\u6CD5\u5B9A\u4F4D\u8FD9\u5C01\u90AE\u4EF6");
|
|
2554
|
+
});
|
|
1904
2555
|
router.get("/messages/:id", async ({ url, params }) => {
|
|
1905
2556
|
const { account } = accountFrom(runtime, url);
|
|
1906
2557
|
const source = sourceOf(url);
|
|
@@ -2160,7 +2811,7 @@ function buildRouter(runtime) {
|
|
|
2160
2811
|
output = await ai.ask(detail, instruction, earlier);
|
|
2161
2812
|
}
|
|
2162
2813
|
const card = {
|
|
2163
|
-
id:
|
|
2814
|
+
id: randomUUID3(),
|
|
2164
2815
|
kind,
|
|
2165
2816
|
instruction,
|
|
2166
2817
|
output,
|
|
@@ -2274,6 +2925,192 @@ function buildRouter(runtime) {
|
|
|
2274
2925
|
throw aiHttpError(error);
|
|
2275
2926
|
}
|
|
2276
2927
|
});
|
|
2928
|
+
router.get("/tasks", ({ url }) => {
|
|
2929
|
+
const account = accountFromPayload(runtime, { accountId: url.searchParams.get("accountId") ?? void 0 });
|
|
2930
|
+
const items = runtime.tasks().list({
|
|
2931
|
+
accountId: account.id,
|
|
2932
|
+
status: url.searchParams.get("status") ?? "",
|
|
2933
|
+
projectId: url.searchParams.get("projectId") ?? "",
|
|
2934
|
+
query: url.searchParams.get("query") ?? ""
|
|
2935
|
+
});
|
|
2936
|
+
return { items: items.map(taskView), calendar: runtime.tasks().calendarState(account.id) };
|
|
2937
|
+
});
|
|
2938
|
+
router.post("/tasks", async ({ body }) => {
|
|
2939
|
+
const payload = await body();
|
|
2940
|
+
const account = accountFromPayload(runtime, payload);
|
|
2941
|
+
const task = runtime.tasks().create({
|
|
2942
|
+
accountId: account.id,
|
|
2943
|
+
title: String(payload.title ?? ""),
|
|
2944
|
+
notes: typeof payload.notes === "string" ? payload.notes : "",
|
|
2945
|
+
due: typeof payload.due === "string" ? payload.due : "",
|
|
2946
|
+
status: typeof payload.status === "string" ? payload.status : "NEEDS-ACTION",
|
|
2947
|
+
priority: Number(payload.priority ?? 0),
|
|
2948
|
+
owner: typeof payload.owner === "string" ? payload.owner : "",
|
|
2949
|
+
projectId: typeof payload.projectId === "string" ? payload.projectId : null,
|
|
2950
|
+
messageKey: typeof payload.messageKey === "string" ? payload.messageKey : "",
|
|
2951
|
+
messageSubject: typeof payload.messageSubject === "string" ? payload.messageSubject : ""
|
|
2952
|
+
});
|
|
2953
|
+
return { task: taskView(task) };
|
|
2954
|
+
});
|
|
2955
|
+
router.patch("/tasks/:id", async ({ params, body }) => {
|
|
2956
|
+
const payload = await body();
|
|
2957
|
+
const task = runtime.tasks().update(String(params.id ?? ""), {
|
|
2958
|
+
...typeof payload.title === "string" ? { title: payload.title } : {},
|
|
2959
|
+
...typeof payload.notes === "string" ? { notes: payload.notes } : {},
|
|
2960
|
+
...typeof payload.due === "string" ? { due: payload.due } : {},
|
|
2961
|
+
...typeof payload.status === "string" ? { status: payload.status } : {},
|
|
2962
|
+
...typeof payload.priority === "number" ? { priority: payload.priority } : {},
|
|
2963
|
+
...typeof payload.owner === "string" ? { owner: payload.owner } : {},
|
|
2964
|
+
...payload.projectId === null || typeof payload.projectId === "string" ? { projectId: payload.projectId } : {}
|
|
2965
|
+
});
|
|
2966
|
+
return { task: taskView(task) };
|
|
2967
|
+
});
|
|
2968
|
+
router.delete("/tasks/:id", ({ params }) => {
|
|
2969
|
+
const removed = runtime.tasks().remove(String(params.id ?? ""));
|
|
2970
|
+
if (!removed) throw new HttpError(404, "\u4EFB\u52A1\u4E0D\u5B58\u5728");
|
|
2971
|
+
return { ok: true };
|
|
2972
|
+
});
|
|
2973
|
+
router.post("/tasks/extract", async ({ body }) => {
|
|
2974
|
+
const payload = await body();
|
|
2975
|
+
const account = accountFromPayload(runtime, payload);
|
|
2976
|
+
const detail = await loadDetail(
|
|
2977
|
+
runtime,
|
|
2978
|
+
account,
|
|
2979
|
+
sourceOfPayload(payload),
|
|
2980
|
+
String(payload.id ?? ""),
|
|
2981
|
+
mailboxOfPayload(payload)
|
|
2982
|
+
);
|
|
2983
|
+
try {
|
|
2984
|
+
const suggestions = await runtime.ai().extractTasks(detail);
|
|
2985
|
+
return { suggestions };
|
|
2986
|
+
} catch (error) {
|
|
2987
|
+
throw aiHttpError(error);
|
|
2988
|
+
}
|
|
2989
|
+
});
|
|
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
|
+
router.get("/projects", () => ({ items: runtime.projects().list().map(projectView) }));
|
|
3043
|
+
router.get("/projects/for", ({ url }) => {
|
|
3044
|
+
const account = accountFromPayload(runtime, { accountId: url.searchParams.get("accountId") ?? void 0 });
|
|
3045
|
+
const key = url.searchParams.get("key") ?? "";
|
|
3046
|
+
return {
|
|
3047
|
+
project: key === "" ? null : (() => {
|
|
3048
|
+
const found = runtime.projects().findByMessage(account.id, key);
|
|
3049
|
+
return found === void 0 ? null : projectView(found);
|
|
3050
|
+
})()
|
|
3051
|
+
};
|
|
3052
|
+
});
|
|
3053
|
+
router.post("/projects", async ({ body }) => {
|
|
3054
|
+
const payload = await body();
|
|
3055
|
+
const account = accountFromPayload(runtime, payload);
|
|
3056
|
+
const name2 = String(payload.name ?? "").trim();
|
|
3057
|
+
const root = String(payload.root ?? "").trim() || runtime.current.projects.root;
|
|
3058
|
+
const conversation = typeof payload.conversation === "string" ? payload.conversation : "";
|
|
3059
|
+
const message = asObject(payload.message);
|
|
3060
|
+
const key = typeof message.key === "string" ? message.key : "";
|
|
3061
|
+
const result = await createProject({
|
|
3062
|
+
store: runtime.projects(),
|
|
3063
|
+
input: {
|
|
3064
|
+
name: name2,
|
|
3065
|
+
root,
|
|
3066
|
+
conversation,
|
|
3067
|
+
accountId: account.id,
|
|
3068
|
+
...key === "" ? {} : {
|
|
3069
|
+
message: {
|
|
3070
|
+
accountId: account.id,
|
|
3071
|
+
key,
|
|
3072
|
+
subject: String(message.subject ?? ""),
|
|
3073
|
+
date: String(message.date ?? "")
|
|
3074
|
+
}
|
|
3075
|
+
},
|
|
3076
|
+
...key === "" ? {} : { thread: { accountId: account.id, key } }
|
|
3077
|
+
},
|
|
3078
|
+
registry: runtime.workspaceRegistry,
|
|
3079
|
+
sessions: runtime.sessions
|
|
3080
|
+
});
|
|
3081
|
+
return { project: projectView(result.project), warnings: result.warnings };
|
|
3082
|
+
});
|
|
3083
|
+
router.post("/projects/:id/messages", async ({ params, body }) => {
|
|
3084
|
+
const payload = await body();
|
|
3085
|
+
const account = accountFromPayload(runtime, payload);
|
|
3086
|
+
const project = runtime.projects().addMessage(String(params.id ?? ""), {
|
|
3087
|
+
accountId: account.id,
|
|
3088
|
+
key: String(payload.key ?? ""),
|
|
3089
|
+
subject: String(payload.subject ?? ""),
|
|
3090
|
+
date: String(payload.date ?? "")
|
|
3091
|
+
});
|
|
3092
|
+
if (project === void 0) throw new HttpError(404, "\u9879\u76EE\u4E0D\u5B58\u5728");
|
|
3093
|
+
return { project: projectView(project) };
|
|
3094
|
+
});
|
|
3095
|
+
router.delete("/projects/:id/messages", ({ params, url }) => {
|
|
3096
|
+
const account = accountFromPayload(runtime, { accountId: url.searchParams.get("accountId") ?? void 0 });
|
|
3097
|
+
const key = url.searchParams.get("key") ?? "";
|
|
3098
|
+
const project = runtime.projects().removeMessage(String(params.id ?? ""), account.id, key);
|
|
3099
|
+
if (project === void 0) throw new HttpError(404, "\u9879\u76EE\u4E0D\u5B58\u5728");
|
|
3100
|
+
return { project: projectView(project) };
|
|
3101
|
+
});
|
|
3102
|
+
router.post("/projects/:id/transfer", async ({ params, body }) => {
|
|
3103
|
+
const payload = await body();
|
|
3104
|
+
const account = accountFromPayload(runtime, payload);
|
|
3105
|
+
const project = runtime.projects().get(String(params.id ?? ""));
|
|
3106
|
+
if (project === void 0) throw new HttpError(404, "\u9879\u76EE\u4E0D\u5B58\u5728");
|
|
3107
|
+
const conversation = typeof payload.conversation === "string" ? payload.conversation : "";
|
|
3108
|
+
if (conversation.trim() === "") throw new HttpError(400, "\u6CA1\u6709\u53EF\u8F6C\u79FB\u7684 AI \u8BB0\u5F55");
|
|
3109
|
+
appendConversation(project, conversation);
|
|
3110
|
+
const key = typeof payload.key === "string" ? payload.key : "";
|
|
3111
|
+
const updated = key === "" ? runtime.projects().update(project.id, {}) : runtime.projects().addThread(project.id, { accountId: account.id, key });
|
|
3112
|
+
return { project: projectView(updated ?? project) };
|
|
3113
|
+
});
|
|
2277
3114
|
router.get("/settings", () => settingsView(runtime.current));
|
|
2278
3115
|
router.patch("/settings", async ({ body }) => {
|
|
2279
3116
|
const payload = await body();
|
|
@@ -2282,6 +3119,11 @@ function buildRouter(runtime) {
|
|
|
2282
3119
|
if (payload.ai !== void 0) patch.ai = asObject(payload.ai);
|
|
2283
3120
|
if (payload.read !== void 0) patch.read = asObject(payload.read);
|
|
2284
3121
|
if (payload.render !== void 0) patch.render = asObject(payload.render);
|
|
3122
|
+
if (payload.projects !== void 0) {
|
|
3123
|
+
const root = String(asObject(payload.projects).root ?? "").trim();
|
|
3124
|
+
if (root === "") throw new HttpError(400, "\u9879\u76EE\u76EE\u5F55\u4E0D\u80FD\u4E3A\u7A7A");
|
|
3125
|
+
patch.projects = { root: resolveUserPath(root, "\u9879\u76EE\u76EE\u5F55") };
|
|
3126
|
+
}
|
|
2285
3127
|
saveSettings(patch);
|
|
2286
3128
|
runtime.refresh();
|
|
2287
3129
|
return { settings: settingsView(runtime.current) };
|
|
@@ -2423,7 +3265,41 @@ function settingsView(config) {
|
|
|
2423
3265
|
styleSampleCount: config.ai.styleSampleCount
|
|
2424
3266
|
},
|
|
2425
3267
|
read: { markOnOpen: config.read.markOnOpen, pollSeconds: config.read.pollSeconds },
|
|
2426
|
-
render: { listLimit: config.render.listLimit }
|
|
3268
|
+
render: { listLimit: config.render.listLimit },
|
|
3269
|
+
projects: { root: config.projects.root }
|
|
3270
|
+
};
|
|
3271
|
+
}
|
|
3272
|
+
function taskView(task) {
|
|
3273
|
+
return {
|
|
3274
|
+
id: task.id,
|
|
3275
|
+
accountId: task.accountId,
|
|
3276
|
+
projectId: task.projectId,
|
|
3277
|
+
title: task.title,
|
|
3278
|
+
notes: task.notes,
|
|
3279
|
+
due: task.due,
|
|
3280
|
+
status: task.status,
|
|
3281
|
+
priority: task.priority,
|
|
3282
|
+
owner: task.owner,
|
|
3283
|
+
messageKey: task.messageKey,
|
|
3284
|
+
messageSubject: task.messageSubject,
|
|
3285
|
+
dirty: task.dirty,
|
|
3286
|
+
createdAt: task.createdAt,
|
|
3287
|
+
updatedAt: task.updatedAt,
|
|
3288
|
+
completedAt: task.completedAt
|
|
3289
|
+
};
|
|
3290
|
+
}
|
|
3291
|
+
function projectView(project) {
|
|
3292
|
+
return {
|
|
3293
|
+
id: project.id,
|
|
3294
|
+
name: project.name,
|
|
3295
|
+
path: project.path,
|
|
3296
|
+
workspaceId: project.workspaceId,
|
|
3297
|
+
sessionId: project.sessionId,
|
|
3298
|
+
accountIds: [...project.accountIds],
|
|
3299
|
+
messages: project.messages.map((message) => ({ ...message })),
|
|
3300
|
+
threads: project.threads.map((thread) => ({ ...thread })),
|
|
3301
|
+
createdAt: project.createdAt,
|
|
3302
|
+
updatedAt: project.updatedAt
|
|
2427
3303
|
};
|
|
2428
3304
|
}
|
|
2429
3305
|
function localSummary(runtime, account, row) {
|
|
@@ -2577,7 +3453,7 @@ ${signature}`;
|
|
|
2577
3453
|
|
|
2578
3454
|
// src/host/runtime.ts
|
|
2579
3455
|
import { statSync } from "node:fs";
|
|
2580
|
-
import { join as
|
|
3456
|
+
import { join as join7 } from "node:path";
|
|
2581
3457
|
|
|
2582
3458
|
// src/host/secrets.ts
|
|
2583
3459
|
import { spawnSync } from "node:child_process";
|
|
@@ -2746,6 +3622,16 @@ var CredentialManager = class {
|
|
|
2746
3622
|
this.session.delete(account.id);
|
|
2747
3623
|
this.secrets.delete(account.keychainService, account.email);
|
|
2748
3624
|
}
|
|
3625
|
+
/** Read an arbitrary secret (for example a CalDAV app password). */
|
|
3626
|
+
readSecret(service, account) {
|
|
3627
|
+
return this.secrets.read(service, account);
|
|
3628
|
+
}
|
|
3629
|
+
writeSecret(service, account, secret) {
|
|
3630
|
+
return this.secrets.write(service, account, secret);
|
|
3631
|
+
}
|
|
3632
|
+
deleteSecret(service, account) {
|
|
3633
|
+
return this.secrets.delete(service, account);
|
|
3634
|
+
}
|
|
2749
3635
|
status(account) {
|
|
2750
3636
|
const resolved = this.resolve(account);
|
|
2751
3637
|
return {
|
|
@@ -2757,7 +3643,7 @@ var CredentialManager = class {
|
|
|
2757
3643
|
};
|
|
2758
3644
|
|
|
2759
3645
|
// src/host/ai-store.ts
|
|
2760
|
-
import { existsSync as
|
|
3646
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync5, renameSync as renameSync4, writeFileSync as writeFileSync5 } from "node:fs";
|
|
2761
3647
|
import { dirname as dirname2 } from "node:path";
|
|
2762
3648
|
var MAX_THREADS = 50;
|
|
2763
3649
|
var MAX_CARDS_PER_THREAD = 50;
|
|
@@ -2772,7 +3658,7 @@ var AiThreadStore = class {
|
|
|
2772
3658
|
if (this.cache !== null) return this.cache;
|
|
2773
3659
|
let parsed = null;
|
|
2774
3660
|
try {
|
|
2775
|
-
if (
|
|
3661
|
+
if (existsSync6(this.file)) parsed = JSON.parse(readFileSync5(this.file, "utf8"));
|
|
2776
3662
|
} catch {
|
|
2777
3663
|
parsed = null;
|
|
2778
3664
|
}
|
|
@@ -2785,10 +3671,10 @@ var AiThreadStore = class {
|
|
|
2785
3671
|
prune(data);
|
|
2786
3672
|
const text2 = `${JSON.stringify(data, null, 2)}
|
|
2787
3673
|
`;
|
|
2788
|
-
|
|
3674
|
+
mkdirSync5(dirname2(this.file), { recursive: true });
|
|
2789
3675
|
const temp = `${this.file}.${String(process.pid)}.tmp`;
|
|
2790
|
-
|
|
2791
|
-
|
|
3676
|
+
writeFileSync5(temp, text2, { mode: 384 });
|
|
3677
|
+
renameSync4(temp, this.file);
|
|
2792
3678
|
}
|
|
2793
3679
|
get(key) {
|
|
2794
3680
|
const thread = this.load().threads[key];
|
|
@@ -2796,7 +3682,7 @@ var AiThreadStore = class {
|
|
|
2796
3682
|
}
|
|
2797
3683
|
appendCard(input) {
|
|
2798
3684
|
const data = this.load();
|
|
2799
|
-
const
|
|
3685
|
+
const now2 = (/* @__PURE__ */ new Date()).toISOString();
|
|
2800
3686
|
const existing = data.threads[input.key];
|
|
2801
3687
|
const thread = existing ?? {
|
|
2802
3688
|
key: input.key,
|
|
@@ -2804,13 +3690,13 @@ var AiThreadStore = class {
|
|
|
2804
3690
|
subject: input.subject,
|
|
2805
3691
|
messageId: input.messageId ?? "",
|
|
2806
3692
|
cards: [],
|
|
2807
|
-
updatedAt:
|
|
3693
|
+
updatedAt: now2
|
|
2808
3694
|
};
|
|
2809
3695
|
thread.accountId = input.accountId;
|
|
2810
3696
|
thread.subject = input.subject;
|
|
2811
3697
|
if (input.messageId !== void 0 && input.messageId !== "") thread.messageId = input.messageId;
|
|
2812
3698
|
thread.cards.push(input.card);
|
|
2813
|
-
thread.updatedAt =
|
|
3699
|
+
thread.updatedAt = now2;
|
|
2814
3700
|
data.threads[input.key] = thread;
|
|
2815
3701
|
this.flush();
|
|
2816
3702
|
return { ...thread, cards: [...thread.cards] };
|
|
@@ -2896,10 +3782,330 @@ function prune(data) {
|
|
|
2896
3782
|
}
|
|
2897
3783
|
}
|
|
2898
3784
|
|
|
3785
|
+
// src/host/tasks.ts
|
|
3786
|
+
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
3787
|
+
import { mkdirSync as mkdirSync6 } from "node:fs";
|
|
3788
|
+
import { dirname as dirname3 } from "node:path";
|
|
3789
|
+
import { DatabaseSync as DatabaseSync3 } from "node:sqlite";
|
|
3790
|
+
var STATUSES = /* @__PURE__ */ new Set(["NEEDS-ACTION", "IN-PROCESS", "COMPLETED", "CANCELLED"]);
|
|
3791
|
+
function now() {
|
|
3792
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
3793
|
+
}
|
|
3794
|
+
function rowToTask(row) {
|
|
3795
|
+
if (row === void 0) return void 0;
|
|
3796
|
+
return {
|
|
3797
|
+
id: String(row.id),
|
|
3798
|
+
accountId: String(row.account_id),
|
|
3799
|
+
projectId: row.project_id === null || row.project_id === void 0 ? null : String(row.project_id),
|
|
3800
|
+
calendarHref: row.calendar_href === null || row.calendar_href === void 0 ? null : String(row.calendar_href),
|
|
3801
|
+
href: row.href === null || row.href === void 0 ? null : String(row.href),
|
|
3802
|
+
etag: row.etag === null || row.etag === void 0 ? null : String(row.etag),
|
|
3803
|
+
uid: String(row.uid),
|
|
3804
|
+
title: String(row.title),
|
|
3805
|
+
notes: String(row.notes ?? ""),
|
|
3806
|
+
due: String(row.due ?? ""),
|
|
3807
|
+
status: String(row.status ?? "NEEDS-ACTION"),
|
|
3808
|
+
priority: Number(row.priority ?? 0),
|
|
3809
|
+
owner: String(row.owner ?? ""),
|
|
3810
|
+
messageKey: String(row.message_key ?? ""),
|
|
3811
|
+
messageSubject: String(row.message_subject ?? ""),
|
|
3812
|
+
completedAt: row.completed_at === null || row.completed_at === void 0 ? null : String(row.completed_at),
|
|
3813
|
+
dirty: Number(row.dirty) === 1,
|
|
3814
|
+
deleted: Number(row.deleted) === 1,
|
|
3815
|
+
createdAt: String(row.created_at ?? ""),
|
|
3816
|
+
updatedAt: String(row.updated_at ?? "")
|
|
3817
|
+
};
|
|
3818
|
+
}
|
|
3819
|
+
function statusOf(value) {
|
|
3820
|
+
const text2 = String(value ?? "NEEDS-ACTION").toUpperCase();
|
|
3821
|
+
return STATUSES.has(text2) ? text2 : "NEEDS-ACTION";
|
|
3822
|
+
}
|
|
3823
|
+
var TaskStore = class {
|
|
3824
|
+
file;
|
|
3825
|
+
db = null;
|
|
3826
|
+
constructor(file) {
|
|
3827
|
+
this.file = file;
|
|
3828
|
+
}
|
|
3829
|
+
close() {
|
|
3830
|
+
if (this.db !== null) {
|
|
3831
|
+
this.db.close();
|
|
3832
|
+
this.db = null;
|
|
3833
|
+
}
|
|
3834
|
+
}
|
|
3835
|
+
open() {
|
|
3836
|
+
if (this.db !== null) return this.db;
|
|
3837
|
+
mkdirSync6(dirname3(this.file), { recursive: true });
|
|
3838
|
+
const db = new DatabaseSync3(this.file);
|
|
3839
|
+
db.exec("PRAGMA journal_mode=WAL");
|
|
3840
|
+
db.exec("PRAGMA busy_timeout=10000");
|
|
3841
|
+
db.exec(`
|
|
3842
|
+
CREATE TABLE IF NOT EXISTS tasks (
|
|
3843
|
+
id TEXT PRIMARY KEY,
|
|
3844
|
+
account_id TEXT NOT NULL,
|
|
3845
|
+
project_id TEXT,
|
|
3846
|
+
calendar_href TEXT,
|
|
3847
|
+
href TEXT,
|
|
3848
|
+
etag TEXT,
|
|
3849
|
+
uid TEXT NOT NULL,
|
|
3850
|
+
title TEXT NOT NULL,
|
|
3851
|
+
notes TEXT,
|
|
3852
|
+
due TEXT,
|
|
3853
|
+
status TEXT NOT NULL DEFAULT 'NEEDS-ACTION',
|
|
3854
|
+
priority INTEGER NOT NULL DEFAULT 0,
|
|
3855
|
+
owner TEXT,
|
|
3856
|
+
message_key TEXT,
|
|
3857
|
+
message_subject TEXT,
|
|
3858
|
+
completed_at TEXT,
|
|
3859
|
+
dirty INTEGER NOT NULL DEFAULT 1,
|
|
3860
|
+
deleted INTEGER NOT NULL DEFAULT 0,
|
|
3861
|
+
created_at TEXT NOT NULL,
|
|
3862
|
+
updated_at TEXT NOT NULL
|
|
3863
|
+
);
|
|
3864
|
+
CREATE INDEX IF NOT EXISTS tasks_account_due ON tasks (account_id, deleted, status, due);
|
|
3865
|
+
CREATE INDEX IF NOT EXISTS tasks_project ON tasks (project_id);
|
|
3866
|
+
CREATE TABLE IF NOT EXISTS calendar_state (
|
|
3867
|
+
account_id TEXT PRIMARY KEY,
|
|
3868
|
+
calendar_href TEXT,
|
|
3869
|
+
display_name TEXT,
|
|
3870
|
+
ctag TEXT,
|
|
3871
|
+
last_sync TEXT,
|
|
3872
|
+
error TEXT
|
|
3873
|
+
);
|
|
3874
|
+
`);
|
|
3875
|
+
this.db = db;
|
|
3876
|
+
return db;
|
|
3877
|
+
}
|
|
3878
|
+
list(options) {
|
|
3879
|
+
const db = this.open();
|
|
3880
|
+
const where = ["account_id = ?", "deleted = 0"];
|
|
3881
|
+
const params = [options.accountId];
|
|
3882
|
+
if (options.status !== void 0 && options.status !== "") {
|
|
3883
|
+
where.push("status = ?");
|
|
3884
|
+
params.push(statusOf(options.status));
|
|
3885
|
+
}
|
|
3886
|
+
if (options.projectId !== void 0 && options.projectId !== "") {
|
|
3887
|
+
where.push("project_id = ?");
|
|
3888
|
+
params.push(options.projectId);
|
|
3889
|
+
}
|
|
3890
|
+
const text2 = String(options.query ?? "").trim();
|
|
3891
|
+
if (text2 !== "") {
|
|
3892
|
+
where.push("(title LIKE ? OR notes LIKE ? OR owner LIKE ?)");
|
|
3893
|
+
const like = `%${text2}%`;
|
|
3894
|
+
params.push(like, like, like);
|
|
3895
|
+
}
|
|
3896
|
+
const rows = db.prepare(
|
|
3897
|
+
`SELECT * FROM tasks WHERE ${where.join(" AND ")}
|
|
3898
|
+
ORDER BY CASE WHEN completed_at IS NOT NULL THEN 1 ELSE 0 END,
|
|
3899
|
+
CASE WHEN due IS NULL OR due = '' THEN 1 ELSE 0 END, due ASC, created_at DESC
|
|
3900
|
+
LIMIT ?`
|
|
3901
|
+
).all(...params, Math.max(1, Math.min(1e3, options.limit ?? 500)));
|
|
3902
|
+
return rows.map((row) => rowToTask(row)).filter((task) => task !== void 0);
|
|
3903
|
+
}
|
|
3904
|
+
get(id) {
|
|
3905
|
+
return rowToTask(
|
|
3906
|
+
this.open().prepare("SELECT * FROM tasks WHERE id = ?").get(id)
|
|
3907
|
+
);
|
|
3908
|
+
}
|
|
3909
|
+
create(input) {
|
|
3910
|
+
const title = String(input.title ?? "").trim();
|
|
3911
|
+
if (title === "") throw new Error("\u4EFB\u52A1\u6807\u9898\u4E0D\u80FD\u4E3A\u7A7A");
|
|
3912
|
+
if (title.length > 300) throw new Error("\u4EFB\u52A1\u6807\u9898\u8FC7\u957F");
|
|
3913
|
+
const status = statusOf(input.status);
|
|
3914
|
+
const id = `task_${randomUUID4()}`;
|
|
3915
|
+
const stamp3 = now();
|
|
3916
|
+
this.open().prepare(
|
|
3917
|
+
`INSERT INTO tasks (id, account_id, project_id, uid, title, notes, due, status, priority, owner,
|
|
3918
|
+
message_key, message_subject, completed_at, dirty, deleted, created_at, updated_at)
|
|
3919
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, 0, ?, ?)`
|
|
3920
|
+
).run(
|
|
3921
|
+
id,
|
|
3922
|
+
input.accountId,
|
|
3923
|
+
input.projectId ?? null,
|
|
3924
|
+
`${randomUUID4()}@dsh-mailharbor`,
|
|
3925
|
+
title,
|
|
3926
|
+
String(input.notes ?? ""),
|
|
3927
|
+
String(input.due ?? ""),
|
|
3928
|
+
status,
|
|
3929
|
+
Math.max(0, Math.min(9, Number(input.priority ?? 0))),
|
|
3930
|
+
String(input.owner ?? ""),
|
|
3931
|
+
String(input.messageKey ?? ""),
|
|
3932
|
+
String(input.messageSubject ?? ""),
|
|
3933
|
+
status === "COMPLETED" ? stamp3 : null,
|
|
3934
|
+
stamp3,
|
|
3935
|
+
stamp3
|
|
3936
|
+
);
|
|
3937
|
+
const task = this.get(id);
|
|
3938
|
+
if (task === void 0) throw new Error("\u4EFB\u52A1\u521B\u5EFA\u5931\u8D25");
|
|
3939
|
+
return task;
|
|
3940
|
+
}
|
|
3941
|
+
update(id, fields) {
|
|
3942
|
+
const current = this.get(id);
|
|
3943
|
+
if (current === void 0) throw new Error("\u4EFB\u52A1\u4E0D\u5B58\u5728");
|
|
3944
|
+
const next = {
|
|
3945
|
+
title: fields.title === void 0 ? current.title : String(fields.title).trim(),
|
|
3946
|
+
notes: fields.notes === void 0 ? current.notes : String(fields.notes ?? ""),
|
|
3947
|
+
due: fields.due === void 0 ? current.due : String(fields.due ?? ""),
|
|
3948
|
+
status: fields.status === void 0 ? current.status : statusOf(fields.status),
|
|
3949
|
+
priority: fields.priority === void 0 ? current.priority : Number(fields.priority),
|
|
3950
|
+
owner: fields.owner === void 0 ? current.owner : String(fields.owner ?? ""),
|
|
3951
|
+
projectId: fields.projectId === void 0 ? current.projectId : fields.projectId,
|
|
3952
|
+
messageKey: fields.messageKey === void 0 ? current.messageKey : String(fields.messageKey),
|
|
3953
|
+
messageSubject: fields.messageSubject === void 0 ? current.messageSubject : String(fields.messageSubject)
|
|
3954
|
+
};
|
|
3955
|
+
if (next.title === "") throw new Error("\u4EFB\u52A1\u6807\u9898\u4E0D\u80FD\u4E3A\u7A7A");
|
|
3956
|
+
const stamp3 = now();
|
|
3957
|
+
const completedAt = next.status === "COMPLETED" ? current.completedAt ?? stamp3 : null;
|
|
3958
|
+
this.open().prepare(
|
|
3959
|
+
`UPDATE tasks SET title = ?, notes = ?, due = ?, status = ?, priority = ?, owner = ?, project_id = ?,
|
|
3960
|
+
message_key = ?, message_subject = ?, completed_at = ?, dirty = 1, updated_at = ? WHERE id = ?`
|
|
3961
|
+
).run(
|
|
3962
|
+
next.title,
|
|
3963
|
+
next.notes,
|
|
3964
|
+
next.due,
|
|
3965
|
+
next.status,
|
|
3966
|
+
Math.max(0, Math.min(9, Number(next.priority) || 0)),
|
|
3967
|
+
next.owner,
|
|
3968
|
+
next.projectId,
|
|
3969
|
+
next.messageKey,
|
|
3970
|
+
next.messageSubject,
|
|
3971
|
+
completedAt,
|
|
3972
|
+
stamp3,
|
|
3973
|
+
id
|
|
3974
|
+
);
|
|
3975
|
+
const task = this.get(id);
|
|
3976
|
+
if (task === void 0) throw new Error("\u4EFB\u52A1\u66F4\u65B0\u5931\u8D25");
|
|
3977
|
+
return task;
|
|
3978
|
+
}
|
|
3979
|
+
remove(id) {
|
|
3980
|
+
const current = this.get(id);
|
|
3981
|
+
if (current === void 0) return false;
|
|
3982
|
+
if (current.href !== null && current.href !== "") {
|
|
3983
|
+
this.open().prepare("UPDATE tasks SET deleted = 1, dirty = 1, updated_at = ? WHERE id = ?").run(now(), id);
|
|
3984
|
+
} else {
|
|
3985
|
+
this.open().prepare("DELETE FROM tasks WHERE id = ?").run(id);
|
|
3986
|
+
}
|
|
3987
|
+
return true;
|
|
3988
|
+
}
|
|
3989
|
+
/** Apply a VTODO fetched from CalDAV; local pending edits win over remote. */
|
|
3990
|
+
applyRemote(input) {
|
|
3991
|
+
const db = this.open();
|
|
3992
|
+
const existing = db.prepare("SELECT * FROM tasks WHERE account_id = ? AND href = ?").get(input.accountId, input.href) ?? db.prepare("SELECT * FROM tasks WHERE account_id = ? AND uid = ?").get(input.accountId, input.todo.uid);
|
|
3993
|
+
const status = statusOf(input.todo.status);
|
|
3994
|
+
const completedAt = status === "COMPLETED" ? input.todo.completedAt ?? now() : null;
|
|
3995
|
+
if (existing !== void 0) {
|
|
3996
|
+
if (Number(existing.dirty) === 1) return rowToTask(existing);
|
|
3997
|
+
db.prepare(
|
|
3998
|
+
`UPDATE tasks SET calendar_href = ?, href = ?, etag = ?, title = ?, notes = ?, due = ?, status = ?, priority = ?,
|
|
3999
|
+
completed_at = ?, dirty = 0, deleted = 0, updated_at = ? WHERE id = ?`
|
|
4000
|
+
).run(
|
|
4001
|
+
input.calendarHref,
|
|
4002
|
+
input.href,
|
|
4003
|
+
input.etag,
|
|
4004
|
+
String(input.todo.title || existing.title),
|
|
4005
|
+
String(input.todo.notes ?? ""),
|
|
4006
|
+
String(input.todo.due ?? ""),
|
|
4007
|
+
status,
|
|
4008
|
+
Math.max(0, Math.min(9, Number(input.todo.priority) || 0)),
|
|
4009
|
+
completedAt,
|
|
4010
|
+
now(),
|
|
4011
|
+
String(existing.id)
|
|
4012
|
+
);
|
|
4013
|
+
return this.get(String(existing.id));
|
|
4014
|
+
}
|
|
4015
|
+
const id = `task_${randomUUID4()}`;
|
|
4016
|
+
const stamp3 = now();
|
|
4017
|
+
db.prepare(
|
|
4018
|
+
`INSERT INTO tasks (id, account_id, calendar_href, href, etag, uid, title, notes, due, status, priority,
|
|
4019
|
+
completed_at, dirty, deleted, created_at, updated_at)
|
|
4020
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, 0, ?, ?)`
|
|
4021
|
+
).run(
|
|
4022
|
+
id,
|
|
4023
|
+
input.accountId,
|
|
4024
|
+
input.calendarHref,
|
|
4025
|
+
input.href,
|
|
4026
|
+
input.etag,
|
|
4027
|
+
input.todo.uid || `${randomUUID4()}@dsh-mailharbor`,
|
|
4028
|
+
String(input.todo.title || "(\u65E0\u6807\u9898)"),
|
|
4029
|
+
String(input.todo.notes ?? ""),
|
|
4030
|
+
String(input.todo.due ?? ""),
|
|
4031
|
+
status,
|
|
4032
|
+
Math.max(0, Math.min(9, Number(input.todo.priority) || 0)),
|
|
4033
|
+
completedAt,
|
|
4034
|
+
stamp3,
|
|
4035
|
+
stamp3
|
|
4036
|
+
);
|
|
4037
|
+
return this.get(id);
|
|
4038
|
+
}
|
|
4039
|
+
pending(accountId) {
|
|
4040
|
+
return this.open().prepare("SELECT * FROM tasks WHERE account_id = ? AND dirty = 1").all(accountId).map((row) => rowToTask(row)).filter((task) => task !== void 0);
|
|
4041
|
+
}
|
|
4042
|
+
markPushed(id, result) {
|
|
4043
|
+
this.open().prepare(
|
|
4044
|
+
"UPDATE tasks SET href = ?, etag = ?, uid = ?, dirty = 0, deleted = 0, updated_at = ? WHERE id = ?"
|
|
4045
|
+
).run(result.href, result.etag, result.uid, now(), id);
|
|
4046
|
+
return this.get(id);
|
|
4047
|
+
}
|
|
4048
|
+
purge(id) {
|
|
4049
|
+
this.open().prepare("DELETE FROM tasks WHERE id = ?").run(id);
|
|
4050
|
+
}
|
|
4051
|
+
/** Remove local copies whose remote VTODO disappeared (not dirty, not tombstoned). */
|
|
4052
|
+
pruneMissingRemote(accountId, calendarHref, hrefs) {
|
|
4053
|
+
const db = this.open();
|
|
4054
|
+
const present = new Set(hrefs);
|
|
4055
|
+
const rows = db.prepare(
|
|
4056
|
+
`SELECT id, href FROM tasks
|
|
4057
|
+
WHERE account_id = ? AND dirty = 0 AND deleted = 0 AND href IS NOT NULL
|
|
4058
|
+
AND (calendar_href = ? OR calendar_href IS NULL OR calendar_href = '')`
|
|
4059
|
+
).all(accountId, calendarHref);
|
|
4060
|
+
let removed = 0;
|
|
4061
|
+
for (const row of rows) {
|
|
4062
|
+
if (present.has(String(row.href))) continue;
|
|
4063
|
+
db.prepare("DELETE FROM tasks WHERE id = ?").run(String(row.id));
|
|
4064
|
+
removed += 1;
|
|
4065
|
+
}
|
|
4066
|
+
return removed;
|
|
4067
|
+
}
|
|
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
|
+
};
|
|
4104
|
+
|
|
2899
4105
|
// src/host/drafts.ts
|
|
2900
|
-
import { randomUUID as
|
|
2901
|
-
import { mkdirSync as
|
|
2902
|
-
import { dirname as
|
|
4106
|
+
import { randomUUID as randomUUID5 } from "node:crypto";
|
|
4107
|
+
import { mkdirSync as mkdirSync7, readFileSync as readFileSync6, renameSync as renameSync5, writeFileSync as writeFileSync6 } from "node:fs";
|
|
4108
|
+
import { dirname as dirname4, join as join6 } from "node:path";
|
|
2903
4109
|
function text(value) {
|
|
2904
4110
|
return value === void 0 || value === null ? "" : String(value);
|
|
2905
4111
|
}
|
|
@@ -2910,7 +4116,7 @@ function isDraft(value) {
|
|
|
2910
4116
|
return value !== null && typeof value === "object" && typeof value.id === "string";
|
|
2911
4117
|
}
|
|
2912
4118
|
function draftPath(root) {
|
|
2913
|
-
return
|
|
4119
|
+
return join6(root, "drafts.json");
|
|
2914
4120
|
}
|
|
2915
4121
|
var DraftStore = class {
|
|
2916
4122
|
file;
|
|
@@ -2923,18 +4129,18 @@ var DraftStore = class {
|
|
|
2923
4129
|
}
|
|
2924
4130
|
load() {
|
|
2925
4131
|
try {
|
|
2926
|
-
const parsed = JSON.parse(
|
|
4132
|
+
const parsed = JSON.parse(readFileSync6(this.file, "utf8"));
|
|
2927
4133
|
this.items = Array.isArray(parsed) ? parsed.filter(isDraft) : [];
|
|
2928
4134
|
} catch {
|
|
2929
4135
|
this.items = [];
|
|
2930
4136
|
}
|
|
2931
4137
|
}
|
|
2932
4138
|
flush() {
|
|
2933
|
-
|
|
4139
|
+
mkdirSync7(dirname4(this.file), { recursive: true });
|
|
2934
4140
|
const temp = `${this.file}.${String(process.pid)}.${String(Date.now())}.tmp`;
|
|
2935
|
-
|
|
4141
|
+
writeFileSync6(temp, `${JSON.stringify(this.items, null, 2)}
|
|
2936
4142
|
`, { encoding: "utf8", mode: 384 });
|
|
2937
|
-
|
|
4143
|
+
renameSync5(temp, this.file);
|
|
2938
4144
|
}
|
|
2939
4145
|
list() {
|
|
2940
4146
|
return [...this.items].sort((left, right) => right.updatedAt.localeCompare(left.updatedAt)).map((draft) => ({ ...draft }));
|
|
@@ -2944,7 +4150,7 @@ var DraftStore = class {
|
|
|
2944
4150
|
return found === void 0 ? void 0 : { ...found };
|
|
2945
4151
|
}
|
|
2946
4152
|
save(input) {
|
|
2947
|
-
const
|
|
4153
|
+
const now2 = (/* @__PURE__ */ new Date()).toISOString();
|
|
2948
4154
|
const existing = input.id === void 0 ? void 0 : this.items.find((draft2) => draft2.id === input.id);
|
|
2949
4155
|
const fields = {
|
|
2950
4156
|
to: text(input.to),
|
|
@@ -2961,15 +4167,15 @@ var DraftStore = class {
|
|
|
2961
4167
|
references: text(input.references)
|
|
2962
4168
|
};
|
|
2963
4169
|
if (existing !== void 0) {
|
|
2964
|
-
Object.assign(existing, fields, { updatedAt:
|
|
4170
|
+
Object.assign(existing, fields, { updatedAt: now2 });
|
|
2965
4171
|
this.flush();
|
|
2966
4172
|
return { ...existing };
|
|
2967
4173
|
}
|
|
2968
4174
|
const draft = {
|
|
2969
|
-
id:
|
|
4175
|
+
id: randomUUID5(),
|
|
2970
4176
|
accountId: this.accountId,
|
|
2971
4177
|
...fields,
|
|
2972
|
-
updatedAt:
|
|
4178
|
+
updatedAt: now2
|
|
2973
4179
|
};
|
|
2974
4180
|
this.items.push(draft);
|
|
2975
4181
|
this.flush();
|
|
@@ -2991,8 +4197,8 @@ var DraftStore = class {
|
|
|
2991
4197
|
init_imap();
|
|
2992
4198
|
|
|
2993
4199
|
// src/host/read-state.ts
|
|
2994
|
-
import { mkdirSync as
|
|
2995
|
-
import { dirname as
|
|
4200
|
+
import { mkdirSync as mkdirSync8, readFileSync as readFileSync7, renameSync as renameSync6, writeFileSync as writeFileSync7 } from "node:fs";
|
|
4201
|
+
import { dirname as dirname5 } from "node:path";
|
|
2996
4202
|
var ReadStateStore = class {
|
|
2997
4203
|
file;
|
|
2998
4204
|
keys = null;
|
|
@@ -3002,7 +4208,7 @@ var ReadStateStore = class {
|
|
|
3002
4208
|
load() {
|
|
3003
4209
|
if (this.keys !== null) return this.keys;
|
|
3004
4210
|
try {
|
|
3005
|
-
const parsed = JSON.parse(
|
|
4211
|
+
const parsed = JSON.parse(readFileSync7(this.file, "utf8"));
|
|
3006
4212
|
const list = Array.isArray(parsed) ? parsed : Array.isArray(parsed?.unread) ? parsed.unread : [];
|
|
3007
4213
|
this.keys = new Set(list.filter((key) => typeof key === "string" && key !== ""));
|
|
3008
4214
|
} catch {
|
|
@@ -3011,14 +4217,14 @@ var ReadStateStore = class {
|
|
|
3011
4217
|
return this.keys;
|
|
3012
4218
|
}
|
|
3013
4219
|
flush() {
|
|
3014
|
-
|
|
4220
|
+
mkdirSync8(dirname5(this.file), { recursive: true });
|
|
3015
4221
|
const temp = `${this.file}.${String(process.pid)}.tmp`;
|
|
3016
|
-
|
|
4222
|
+
writeFileSync7(temp, `${JSON.stringify({ unread: [...this.load()] }, null, 2)}
|
|
3017
4223
|
`, {
|
|
3018
4224
|
encoding: "utf8",
|
|
3019
4225
|
mode: 384
|
|
3020
4226
|
});
|
|
3021
|
-
|
|
4227
|
+
renameSync6(temp, this.file);
|
|
3022
4228
|
}
|
|
3023
4229
|
isUnread(key) {
|
|
3024
4230
|
return key !== "" && this.load().has(key);
|
|
@@ -3077,12 +4283,30 @@ var MailHarborRuntime = class {
|
|
|
3077
4283
|
aiStores = /* @__PURE__ */ new Map();
|
|
3078
4284
|
backups = /* @__PURE__ */ new Map();
|
|
3079
4285
|
llmProvider;
|
|
4286
|
+
workspaceRegistry;
|
|
4287
|
+
sessions;
|
|
3080
4288
|
aiService = null;
|
|
4289
|
+
projectStore = null;
|
|
4290
|
+
taskStore = null;
|
|
3081
4291
|
disposed = false;
|
|
3082
4292
|
constructor(options = {}) {
|
|
3083
4293
|
this.config = loadConfig();
|
|
3084
4294
|
this.credentials = options.credentials ?? new CredentialManager();
|
|
3085
4295
|
this.llmProvider = options.llm ?? (() => void 0);
|
|
4296
|
+
this.workspaceRegistry = options.workspaceRegistry;
|
|
4297
|
+
this.sessions = options.sessions;
|
|
4298
|
+
}
|
|
4299
|
+
projects() {
|
|
4300
|
+
if (this.projectStore === null) {
|
|
4301
|
+
this.projectStore = new ProjectStore(join7(this.config.dataDir, "projects.json"));
|
|
4302
|
+
}
|
|
4303
|
+
return this.projectStore;
|
|
4304
|
+
}
|
|
4305
|
+
tasks() {
|
|
4306
|
+
if (this.taskStore === null) {
|
|
4307
|
+
this.taskStore = new TaskStore(join7(this.config.dataDir, "state", "state.db"));
|
|
4308
|
+
}
|
|
4309
|
+
return this.taskStore;
|
|
3086
4310
|
}
|
|
3087
4311
|
/** The Host model service, when the composition provides one. */
|
|
3088
4312
|
llm() {
|
|
@@ -3208,7 +4432,7 @@ var MailHarborRuntime = class {
|
|
|
3208
4432
|
return created;
|
|
3209
4433
|
}
|
|
3210
4434
|
aiThreads(account) {
|
|
3211
|
-
const file =
|
|
4435
|
+
const file = join7(this.paths(account).root, "ai-threads.json");
|
|
3212
4436
|
const existing = this.aiStores.get(account.id);
|
|
3213
4437
|
if (existing !== void 0 && existing.signature === file) return existing.value;
|
|
3214
4438
|
const created = new AiThreadStore(file);
|
|
@@ -3216,7 +4440,7 @@ var MailHarborRuntime = class {
|
|
|
3216
4440
|
return created;
|
|
3217
4441
|
}
|
|
3218
4442
|
readState(account) {
|
|
3219
|
-
const file =
|
|
4443
|
+
const file = join7(this.paths(account).root, "read-state.json");
|
|
3220
4444
|
const existing = this.readStores.get(account.id);
|
|
3221
4445
|
if (existing !== void 0 && existing.signature === file) return existing.value;
|
|
3222
4446
|
const created = new ReadStateStore(file);
|
|
@@ -3295,6 +4519,7 @@ var MailHarborRuntime = class {
|
|
|
3295
4519
|
this.providers.clear();
|
|
3296
4520
|
for (const db of this.databases.values()) db.value.close();
|
|
3297
4521
|
this.databases.clear();
|
|
4522
|
+
this.taskStore?.close();
|
|
3298
4523
|
}
|
|
3299
4524
|
};
|
|
3300
4525
|
|
|
@@ -3677,8 +4902,22 @@ ${page.items.map(
|
|
|
3677
4902
|
var name = "mailharbor";
|
|
3678
4903
|
var inject = ["webServer", "tools"];
|
|
3679
4904
|
var ROUTE = "/mail-plugin";
|
|
4905
|
+
function optionalService(ctx, name2) {
|
|
4906
|
+
try {
|
|
4907
|
+
const direct = ctx[name2];
|
|
4908
|
+
if (direct !== void 0 && direct !== null) return direct;
|
|
4909
|
+
} catch {
|
|
4910
|
+
}
|
|
4911
|
+
try {
|
|
4912
|
+
return ctx.get?.(name2) ?? void 0;
|
|
4913
|
+
} catch {
|
|
4914
|
+
return void 0;
|
|
4915
|
+
}
|
|
4916
|
+
}
|
|
3680
4917
|
function apply(ctx) {
|
|
3681
4918
|
const runtime = new MailHarborRuntime({
|
|
4919
|
+
workspaceRegistry: optionalService(ctx, "workspaceRegistry"),
|
|
4920
|
+
sessions: optionalService(ctx, "sessions"),
|
|
3682
4921
|
llm: () => {
|
|
3683
4922
|
if (typeof ctx.get !== "function") return void 0;
|
|
3684
4923
|
try {
|