ofw-mcp 2.4.4 → 2.6.3
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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +18 -3
- package/dist/auth-password.js +8 -1
- package/dist/bundle.js +1177 -568
- package/dist/cache/node.js +85 -0
- package/dist/cache/store.js +480 -0
- package/dist/client.js +22 -4
- package/dist/config.js +42 -0
- package/dist/index.js +13 -2
- package/dist/ofw-auth.js +26 -0
- package/dist/sync.js +258 -61
- package/dist/tools/_shared.js +23 -6
- package/dist/tools/attachments.js +66 -0
- package/dist/tools/calendar.js +145 -29
- package/dist/tools/messages.js +95 -83
- package/package.json +14 -5
- package/server.json +8 -2
- package/skills/ofw/SKILL.md +2 -0
- package/skills/ofw-fpx/SKILL.md +106 -0
- package/skills/ofw-fpx/references/requests.md +252 -0
- package/dist/cache.js +0 -345
package/dist/bundle.js
CHANGED
|
@@ -34931,6 +34931,7 @@ var KNOWN_CAPABILITIES = /* @__PURE__ */ new Set([
|
|
|
34931
34931
|
"capture_request_header",
|
|
34932
34932
|
"capture_redirect",
|
|
34933
34933
|
"read_indexed_db",
|
|
34934
|
+
"read_dom",
|
|
34934
34935
|
"download"
|
|
34935
34936
|
]);
|
|
34936
34937
|
|
|
@@ -35232,6 +35233,44 @@ function assertIndexedDbScopesArray(value, label) {
|
|
|
35232
35233
|
}
|
|
35233
35234
|
}
|
|
35234
35235
|
}
|
|
35236
|
+
var DOM_SELECTOR_RE = /^[^-]{1,512}$/;
|
|
35237
|
+
var DOM_ATTRIBUTE_RE = /^[A-Za-z_:][A-Za-z0-9_:.\-]{0,127}$/;
|
|
35238
|
+
function assertDomSelectorsArray(value, label) {
|
|
35239
|
+
if (!Array.isArray(value)) {
|
|
35240
|
+
throw new ProtocolError(`${label}: expected array, got ${typeof value}`);
|
|
35241
|
+
}
|
|
35242
|
+
const seen = /* @__PURE__ */ new Set();
|
|
35243
|
+
for (let i = 0; i < value.length; i++) {
|
|
35244
|
+
const entry = value[i];
|
|
35245
|
+
assertObject(entry, `${label}[${i}]`);
|
|
35246
|
+
if (entry.name === void 0) {
|
|
35247
|
+
throw new ProtocolError(`${label}[${i}].name: missing`);
|
|
35248
|
+
}
|
|
35249
|
+
if (entry.selector === void 0) {
|
|
35250
|
+
throw new ProtocolError(`${label}[${i}].selector: missing`);
|
|
35251
|
+
}
|
|
35252
|
+
if (typeof entry.name !== "string" || !SCOPE_KEY_RE.test(entry.name)) {
|
|
35253
|
+
throw new ProtocolError(`${label}[${i}].name: invalid ${JSON.stringify(entry.name)}`);
|
|
35254
|
+
}
|
|
35255
|
+
if (typeof entry.selector !== "string" || !DOM_SELECTOR_RE.test(entry.selector)) {
|
|
35256
|
+
throw new ProtocolError(`${label}[${i}].selector: invalid ${JSON.stringify(entry.selector)}`);
|
|
35257
|
+
}
|
|
35258
|
+
if (entry.attribute !== void 0) {
|
|
35259
|
+
if (typeof entry.attribute !== "string" || !DOM_ATTRIBUTE_RE.test(entry.attribute)) {
|
|
35260
|
+
throw new ProtocolError(`${label}[${i}].attribute: invalid ${JSON.stringify(entry.attribute)}`);
|
|
35261
|
+
}
|
|
35262
|
+
}
|
|
35263
|
+
if (seen.has(entry.name)) {
|
|
35264
|
+
throw new ProtocolError(`${label}: duplicate name ${JSON.stringify(entry.name)}`);
|
|
35265
|
+
}
|
|
35266
|
+
seen.add(entry.name);
|
|
35267
|
+
for (const k of Object.keys(entry)) {
|
|
35268
|
+
if (k !== "name" && k !== "selector" && k !== "attribute") {
|
|
35269
|
+
throw new ProtocolError(`${label}[${i}]: unexpected field ${JSON.stringify(k)}`);
|
|
35270
|
+
}
|
|
35271
|
+
}
|
|
35272
|
+
}
|
|
35273
|
+
}
|
|
35235
35274
|
function validateFrame(raw) {
|
|
35236
35275
|
assertObject(raw, "frame");
|
|
35237
35276
|
const t = raw.type;
|
|
@@ -35307,6 +35346,9 @@ function validateHello(raw) {
|
|
|
35307
35346
|
if (raw.sessionStoragePointers !== void 0) {
|
|
35308
35347
|
assertStoragePointersArray(raw.sessionStoragePointers, "hello.sessionStoragePointers", raw.sessionStorageKeys);
|
|
35309
35348
|
}
|
|
35349
|
+
if (raw.domSelectors !== void 0) {
|
|
35350
|
+
assertDomSelectorsArray(raw.domSelectors, "hello.domSelectors");
|
|
35351
|
+
}
|
|
35310
35352
|
assertBase64(raw.identityX25519Pub, "hello.identityX25519Pub");
|
|
35311
35353
|
assertBase64(raw.identityEd25519Pub, "hello.identityEd25519Pub");
|
|
35312
35354
|
assertBase64(raw.sessionNonce, "hello.sessionNonce");
|
|
@@ -35541,6 +35583,21 @@ function validateInnerRequest(raw) {
|
|
|
35541
35583
|
}
|
|
35542
35584
|
return raw;
|
|
35543
35585
|
}
|
|
35586
|
+
if (raw.op === "read_dom") {
|
|
35587
|
+
assertObject(raw.init, "inner.init");
|
|
35588
|
+
if (raw.init.origin === void 0)
|
|
35589
|
+
throw new ProtocolError("inner.init.origin: missing");
|
|
35590
|
+
if (raw.init.names === void 0)
|
|
35591
|
+
throw new ProtocolError("inner.init.names: missing");
|
|
35592
|
+
assertHttpsOriginOnly(raw.init.origin, "inner.init.origin");
|
|
35593
|
+
assertNonEmptyKeyArray(raw.init.names, "inner.init.names");
|
|
35594
|
+
for (const k of Object.keys(raw.init)) {
|
|
35595
|
+
if (k !== "origin" && k !== "names") {
|
|
35596
|
+
throw new ProtocolError(`inner.init: unexpected field ${JSON.stringify(k)} on read_dom`);
|
|
35597
|
+
}
|
|
35598
|
+
}
|
|
35599
|
+
return raw;
|
|
35600
|
+
}
|
|
35544
35601
|
if (raw.op === "download") {
|
|
35545
35602
|
assertObject(raw.init, "inner.init");
|
|
35546
35603
|
if (raw.init.url === void 0) {
|
|
@@ -35566,7 +35623,7 @@ function validateInnerRequest(raw) {
|
|
|
35566
35623
|
}
|
|
35567
35624
|
return raw;
|
|
35568
35625
|
}
|
|
35569
|
-
throw new ProtocolError(`inner.op: must be one of "fetch", "read_cookies", "read_local_storage", "read_session_storage", "capture_request_header", "capture_redirect", "read_indexed_db", "download"; got ${JSON.stringify(raw.op)}`);
|
|
35626
|
+
throw new ProtocolError(`inner.op: must be one of "fetch", "read_cookies", "read_local_storage", "read_session_storage", "capture_request_header", "capture_redirect", "read_indexed_db", "read_dom", "download"; got ${JSON.stringify(raw.op)}`);
|
|
35570
35627
|
}
|
|
35571
35628
|
function assertNonEmptyKeyArray(value, label) {
|
|
35572
35629
|
if (!Array.isArray(value)) {
|
|
@@ -35651,6 +35708,13 @@ function validateInnerResponse(raw) {
|
|
|
35651
35708
|
assertObject(raw.values, "inner.values");
|
|
35652
35709
|
return raw;
|
|
35653
35710
|
}
|
|
35711
|
+
if (op === "read_dom") {
|
|
35712
|
+
if (raw.values === void 0) {
|
|
35713
|
+
throw new ProtocolError("inner.values: missing on read_dom response");
|
|
35714
|
+
}
|
|
35715
|
+
assertStringMap(raw.values, "inner.values");
|
|
35716
|
+
return raw;
|
|
35717
|
+
}
|
|
35654
35718
|
if (op === "download") {
|
|
35655
35719
|
assertObject(raw.value, "inner.value");
|
|
35656
35720
|
assertString(raw.value.path, "inner.value.path");
|
|
@@ -35990,6 +36054,13 @@ async function buildServerHello(opts) {
|
|
|
35990
36054
|
jsonPointer: d.jsonPointer
|
|
35991
36055
|
}));
|
|
35992
36056
|
}
|
|
36057
|
+
if (opts.domSelectors && opts.domSelectors.length > 0) {
|
|
36058
|
+
hello.domSelectors = opts.domSelectors.map((d) => ({
|
|
36059
|
+
name: d.name,
|
|
36060
|
+
selector: d.selector,
|
|
36061
|
+
...d.attribute !== void 0 ? { attribute: d.attribute } : {}
|
|
36062
|
+
}));
|
|
36063
|
+
}
|
|
35993
36064
|
return hello;
|
|
35994
36065
|
}
|
|
35995
36066
|
|
|
@@ -36079,7 +36150,8 @@ async function startHost(opts) {
|
|
|
36079
36150
|
captureHeaders: opts.ownCaptureHeaders,
|
|
36080
36151
|
indexedDbScopes: opts.ownIndexedDbScopes,
|
|
36081
36152
|
localStoragePointers: opts.ownLocalStoragePointers,
|
|
36082
|
-
sessionStoragePointers: opts.ownSessionStoragePointers
|
|
36153
|
+
sessionStoragePointers: opts.ownSessionStoragePointers,
|
|
36154
|
+
domSelectors: opts.ownDomSelectors
|
|
36083
36155
|
});
|
|
36084
36156
|
const ownSessionNonce = fromB64(ownHello.sessionNonce);
|
|
36085
36157
|
let extensionWs = null;
|
|
@@ -36314,6 +36386,7 @@ async function startPeer(opts) {
|
|
|
36314
36386
|
sessionStorageKeys: opts.sessionStorageKeys,
|
|
36315
36387
|
captureHeaders: opts.captureHeaders,
|
|
36316
36388
|
indexedDbScopes: opts.indexedDbScopes,
|
|
36389
|
+
domSelectors: opts.domSelectors,
|
|
36317
36390
|
localStoragePointers: opts.localStoragePointers,
|
|
36318
36391
|
sessionStoragePointers: opts.sessionStoragePointers
|
|
36319
36392
|
});
|
|
@@ -36721,6 +36794,11 @@ var FetchproxyServer = class {
|
|
|
36721
36794
|
key: d.key,
|
|
36722
36795
|
jsonPointer: d.jsonPointer
|
|
36723
36796
|
})),
|
|
36797
|
+
domSelectors: (opts.domSelectors ?? []).map((d) => ({
|
|
36798
|
+
name: d.name,
|
|
36799
|
+
selector: d.selector,
|
|
36800
|
+
...d.attribute !== void 0 ? { attribute: d.attribute } : {}
|
|
36801
|
+
})),
|
|
36724
36802
|
// 0.8.0+: timer + lazy-revive default to ON. Every realty MCP
|
|
36725
36803
|
// adapter was about to set these to the same numbers anyway; the
|
|
36726
36804
|
// back-door is `0` (explicit opt-out) if a caller genuinely wants
|
|
@@ -36841,6 +36919,7 @@ var FetchproxyServer = class {
|
|
|
36841
36919
|
ownIndexedDbScopes: this.opts.indexedDbScopes,
|
|
36842
36920
|
ownLocalStoragePointers: this.opts.localStoragePointers,
|
|
36843
36921
|
ownSessionStoragePointers: this.opts.sessionStoragePointers,
|
|
36922
|
+
ownDomSelectors: this.opts.domSelectors,
|
|
36844
36923
|
onPairCode: this.opts.onPairCode
|
|
36845
36924
|
});
|
|
36846
36925
|
this.hostHandle.onOwnInner((inner) => this.onInner(inner));
|
|
@@ -36868,7 +36947,8 @@ var FetchproxyServer = class {
|
|
|
36868
36947
|
captureHeaders: this.opts.captureHeaders,
|
|
36869
36948
|
indexedDbScopes: this.opts.indexedDbScopes,
|
|
36870
36949
|
localStoragePointers: this.opts.localStoragePointers,
|
|
36871
|
-
sessionStoragePointers: this.opts.sessionStoragePointers
|
|
36950
|
+
sessionStoragePointers: this.opts.sessionStoragePointers,
|
|
36951
|
+
domSelectors: this.opts.domSelectors
|
|
36872
36952
|
});
|
|
36873
36953
|
this.peerHandle.onInner((inner) => this.onInner(inner));
|
|
36874
36954
|
this.peerHandle.onRenegotiate(() => {
|
|
@@ -37843,6 +37923,46 @@ var FetchproxyServer = class {
|
|
|
37843
37923
|
await this.sendInnerFrame(inner);
|
|
37844
37924
|
return this._withVerbTimeout(pending, this.pendingIdb, id, origin);
|
|
37845
37925
|
}
|
|
37926
|
+
/**
|
|
37927
|
+
* 1.4.0+: read declared DOM values from the user's signed-in tab.
|
|
37928
|
+
* Requires `'read_dom'` in capabilities AND every requested `name` to
|
|
37929
|
+
* match a declared `domSelectors` entry. The extension reads each
|
|
37930
|
+
* declared selector from the matched tab's DOM (isolated-world
|
|
37931
|
+
* `querySelector`, value or attribute) — no page-JS execution.
|
|
37932
|
+
*
|
|
37933
|
+
* Returns a `Record<string, string>` of `name → value`, with names
|
|
37934
|
+
* whose element (or attribute) was absent omitted. Throws
|
|
37935
|
+
* `FetchproxyProtocolError` on bridge failures and a plain `Error` on
|
|
37936
|
+
* developer mistakes (undeclared capability, undeclared name).
|
|
37937
|
+
*/
|
|
37938
|
+
async readDom(opts) {
|
|
37939
|
+
if (!this.opts.capabilities.includes("read_dom")) {
|
|
37940
|
+
throw new Error('FetchproxyServer.readDom(): MCP did not declare "read_dom" in capabilities');
|
|
37941
|
+
}
|
|
37942
|
+
await this.ensureConnected();
|
|
37943
|
+
this.throwIfPendingPair();
|
|
37944
|
+
if (!Array.isArray(opts.names) || opts.names.length === 0) {
|
|
37945
|
+
throw new Error("FetchproxyServer.readDom: opts.names must be a non-empty array");
|
|
37946
|
+
}
|
|
37947
|
+
this.assertScopeSubset(opts.names, this.opts.domSelectors.map((d) => d.name), "domSelectors");
|
|
37948
|
+
if (opts.subdomain !== void 0)
|
|
37949
|
+
assertSubdomainLabel(opts.subdomain);
|
|
37950
|
+
const baseDomain = this.resolveBaseDomain(opts.domain);
|
|
37951
|
+
const host = opts.subdomain ? `${opts.subdomain}.${baseDomain}` : baseDomain;
|
|
37952
|
+
const origin = `https://${host}`;
|
|
37953
|
+
const id = this.nextRequestId++;
|
|
37954
|
+
const inner = {
|
|
37955
|
+
type: "request",
|
|
37956
|
+
id,
|
|
37957
|
+
op: "read_dom",
|
|
37958
|
+
init: { origin, names: [...opts.names] }
|
|
37959
|
+
};
|
|
37960
|
+
const pending = new Promise((resolve2, reject) => {
|
|
37961
|
+
this.pendingStorage.set(id, { resolve: resolve2, reject });
|
|
37962
|
+
});
|
|
37963
|
+
await this.sendInnerFrame(inner);
|
|
37964
|
+
return this._withVerbTimeout(pending, this.pendingStorage, id, origin);
|
|
37965
|
+
}
|
|
37846
37966
|
assertScopeSubset(requested, declared, label) {
|
|
37847
37967
|
const undeclared = undeclaredKeys(requested, declared);
|
|
37848
37968
|
if (undeclared.length > 0) {
|
|
@@ -37914,7 +38034,7 @@ var FetchproxyServer = class {
|
|
|
37914
38034
|
if (storageCb) {
|
|
37915
38035
|
this.pendingStorage.delete(inner.id);
|
|
37916
38036
|
if (inner.ok) {
|
|
37917
|
-
if ((inner.op === "read_local_storage" || inner.op === "read_session_storage") && inner.values) {
|
|
38037
|
+
if ((inner.op === "read_local_storage" || inner.op === "read_session_storage" || inner.op === "read_dom") && inner.values) {
|
|
37918
38038
|
storageCb.resolve({ ...inner.values });
|
|
37919
38039
|
} else {
|
|
37920
38040
|
storageCb.reject(new FetchproxyProtocolError(`unexpected ${String(inner.op)} response on storage awaiter`));
|
|
@@ -38272,8 +38392,13 @@ async function loginWithPassword(username, password) {
|
|
|
38272
38392
|
}
|
|
38273
38393
|
const contentType = response.headers.get("content-type") ?? "";
|
|
38274
38394
|
if (!contentType.includes("application/json")) {
|
|
38395
|
+
if (contentType.includes("text/html")) {
|
|
38396
|
+
throw new Error(
|
|
38397
|
+
"OFW login failed \u2014 your OurFamilyWizard email or password was not accepted. Check them and try again."
|
|
38398
|
+
);
|
|
38399
|
+
}
|
|
38275
38400
|
const body = await response.text();
|
|
38276
|
-
throw new Error(`OFW login returned unexpected response (${contentType}): ${body.substring(0, 200)}`);
|
|
38401
|
+
throw new Error(`OFW login returned unexpected response (${contentType || "no content-type"}): ${body.substring(0, 200)}`);
|
|
38277
38402
|
}
|
|
38278
38403
|
const data = await response.json();
|
|
38279
38404
|
return {
|
|
@@ -38285,7 +38410,7 @@ async function loginWithPassword(username, password) {
|
|
|
38285
38410
|
// package.json
|
|
38286
38411
|
var package_default = {
|
|
38287
38412
|
name: "ofw-mcp",
|
|
38288
|
-
version: "2.
|
|
38413
|
+
version: "2.6.3",
|
|
38289
38414
|
license: "MIT",
|
|
38290
38415
|
mcpName: "io.github.chrischall/ofw-mcp",
|
|
38291
38416
|
description: "OurFamilyWizard MCP server for Claude \u2014 developed and maintained by AI (Claude Code)",
|
|
@@ -38314,21 +38439,30 @@ var package_default = {
|
|
|
38314
38439
|
dev: "node --env-file=.env dist/index.js",
|
|
38315
38440
|
test: "vitest run",
|
|
38316
38441
|
"test:coverage": "vitest run --coverage",
|
|
38317
|
-
"test:watch": "vitest"
|
|
38442
|
+
"test:watch": "vitest",
|
|
38443
|
+
"worker:dev": "wrangler dev",
|
|
38444
|
+
"worker:deploy": "wrangler deploy",
|
|
38445
|
+
"worker:test": "vitest run --config vitest.workers.config.ts"
|
|
38318
38446
|
},
|
|
38319
38447
|
dependencies: {
|
|
38320
|
-
"@chrischall/mcp-utils": "^0.
|
|
38448
|
+
"@chrischall/mcp-utils": "^0.13.0",
|
|
38321
38449
|
"@fetchproxy/bootstrap": "^1.3.0",
|
|
38322
38450
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
38323
38451
|
dotenv: "^17.4.2",
|
|
38324
38452
|
zod: "^4.4.3"
|
|
38325
38453
|
},
|
|
38326
38454
|
devDependencies: {
|
|
38455
|
+
"@chrischall/mcp-connector": "^0.1.0",
|
|
38456
|
+
"@cloudflare/vitest-pool-workers": "^0.18.4",
|
|
38457
|
+
"@cloudflare/workers-oauth-provider": "^0.0.11",
|
|
38458
|
+
"@cloudflare/workers-types": "^5.20260708.1",
|
|
38327
38459
|
"@types/node": "^26.0.0",
|
|
38328
38460
|
"@vitest/coverage-v8": "^4.1.7",
|
|
38461
|
+
agents: "^0.17.3",
|
|
38329
38462
|
esbuild: "^0.28.0",
|
|
38330
|
-
typescript: "^
|
|
38331
|
-
vitest: "^4.1.7"
|
|
38463
|
+
typescript: "^7.0.2",
|
|
38464
|
+
vitest: "^4.1.7",
|
|
38465
|
+
wrangler: "^4.110.0"
|
|
38332
38466
|
}
|
|
38333
38467
|
};
|
|
38334
38468
|
|
|
@@ -38394,8 +38528,11 @@ async function resolveAuth() {
|
|
|
38394
38528
|
}
|
|
38395
38529
|
|
|
38396
38530
|
// src/client.ts
|
|
38397
|
-
|
|
38398
|
-
|
|
38531
|
+
try {
|
|
38532
|
+
const dir = dirname(fileURLToPath(import.meta.url));
|
|
38533
|
+
await loadDotenvSafely({ path: join3(dir, "..", ".env") });
|
|
38534
|
+
} catch {
|
|
38535
|
+
}
|
|
38399
38536
|
function parseContentDispositionFilename(cd) {
|
|
38400
38537
|
const extMatch = /filename\*=(?:UTF-8'')?([^;]+)/i.exec(cd);
|
|
38401
38538
|
if (extMatch) {
|
|
@@ -38428,6 +38565,16 @@ var OFWClient = class {
|
|
|
38428
38565
|
// already-expired placeholder token so the first request drives the refresh
|
|
38429
38566
|
// callback — i.e. the original "log in on first request" behavior.
|
|
38430
38567
|
tokenManager;
|
|
38568
|
+
// Optional injected auth resolver. When set, the refresh callback uses it
|
|
38569
|
+
// instead of the module-level global `resolveAuth` (env-var → fetchproxy
|
|
38570
|
+
// priority). A hosted per-user deployment injects its own resolver so each
|
|
38571
|
+
// request carries that user's credentials — see the Cloudflare Worker
|
|
38572
|
+
// deployment. Left undefined by the stdio path, which falls back to the
|
|
38573
|
+
// global resolver, keeping that behaviour byte-for-byte identical.
|
|
38574
|
+
authResolver;
|
|
38575
|
+
constructor(opts) {
|
|
38576
|
+
this.authResolver = opts?.resolveAuth;
|
|
38577
|
+
}
|
|
38431
38578
|
getTokenManager() {
|
|
38432
38579
|
if (!this.tokenManager) {
|
|
38433
38580
|
this.tokenManager = new TokenManager({
|
|
@@ -38439,7 +38586,7 @@ var OFWClient = class {
|
|
|
38439
38586
|
// path uses (the 401-replay covers a wrong guess). We re-arm the
|
|
38440
38587
|
// sentinel so the manager can refresh again later.
|
|
38441
38588
|
refresh: async () => {
|
|
38442
|
-
const { token, expiresAt } = await resolveAuth();
|
|
38589
|
+
const { token, expiresAt } = await (this.authResolver ?? resolveAuth)();
|
|
38443
38590
|
return {
|
|
38444
38591
|
accessToken: token,
|
|
38445
38592
|
refreshToken: OFW_REFRESH_SENTINEL,
|
|
@@ -38553,11 +38700,14 @@ var ApiRecipientSchema = external_exports.looseObject({
|
|
|
38553
38700
|
viewed: external_exports.looseObject({ dateTime: external_exports.string() }).nullable().optional()
|
|
38554
38701
|
});
|
|
38555
38702
|
function mapRecipients(items) {
|
|
38556
|
-
return (items ?? []).map((r) =>
|
|
38557
|
-
|
|
38558
|
-
|
|
38559
|
-
|
|
38560
|
-
})
|
|
38703
|
+
return (items ?? []).map((r) => {
|
|
38704
|
+
const dt = r.viewed?.dateTime;
|
|
38705
|
+
const viewedAt = typeof dt === "string" && !dt.startsWith("1970-01-01") ? dt : null;
|
|
38706
|
+
return { userId: r.user?.id ?? 0, name: r.user?.name ?? "", viewedAt };
|
|
38707
|
+
});
|
|
38708
|
+
}
|
|
38709
|
+
function hasRealView(recipients) {
|
|
38710
|
+
return recipients.some((r) => r.viewedAt !== null && !r.viewedAt.startsWith("1970-01-01"));
|
|
38561
38711
|
}
|
|
38562
38712
|
var expandPath2 = expandPath;
|
|
38563
38713
|
function verifyWriteLanded(kind, sent, persisted) {
|
|
@@ -38609,390 +38759,6 @@ function registerUserTools(server, client2) {
|
|
|
38609
38759
|
});
|
|
38610
38760
|
}
|
|
38611
38761
|
|
|
38612
|
-
// src/cache.ts
|
|
38613
|
-
import { DatabaseSync } from "node:sqlite";
|
|
38614
|
-
import { mkdirSync, chmodSync, existsSync } from "node:fs";
|
|
38615
|
-
import { dirname as dirname2 } from "node:path";
|
|
38616
|
-
|
|
38617
|
-
// src/config.ts
|
|
38618
|
-
import { createHash } from "node:crypto";
|
|
38619
|
-
import { homedir as homedir3 } from "node:os";
|
|
38620
|
-
import { join as join4 } from "node:path";
|
|
38621
|
-
function readCacheIdentity() {
|
|
38622
|
-
return readEnvVar("OFW_CACHE_IDENTITY") ?? readEnvVar("OFW_USERNAME") ?? "_default";
|
|
38623
|
-
}
|
|
38624
|
-
function getCacheDir() {
|
|
38625
|
-
const override = process.env.OFW_CACHE_DIR;
|
|
38626
|
-
if (override && override.trim().length > 0) return override.trim();
|
|
38627
|
-
return join4(homedir3(), ".cache", "ofw-mcp");
|
|
38628
|
-
}
|
|
38629
|
-
function getCacheDbPath() {
|
|
38630
|
-
const identity = readCacheIdentity();
|
|
38631
|
-
const hash2 = createHash("sha256").update(identity).digest("hex").slice(0, 16);
|
|
38632
|
-
return join4(getCacheDir(), `${hash2}.db`);
|
|
38633
|
-
}
|
|
38634
|
-
function getAttachmentsDir() {
|
|
38635
|
-
const override = process.env.OFW_ATTACHMENTS_DIR;
|
|
38636
|
-
if (override && override.trim().length > 0) return override.trim();
|
|
38637
|
-
return join4(homedir3(), "Downloads", "ofw-mcp");
|
|
38638
|
-
}
|
|
38639
|
-
function getWriteMode() {
|
|
38640
|
-
const raw = process.env.OFW_WRITE_MODE;
|
|
38641
|
-
if (typeof raw !== "string" || raw.trim().length === 0) return "all";
|
|
38642
|
-
const mode = raw.trim().toLowerCase();
|
|
38643
|
-
if (mode === "none" || mode === "drafts" || mode === "all") return mode;
|
|
38644
|
-
console.error(
|
|
38645
|
-
`[ofw-mcp] Unrecognized OFW_WRITE_MODE "${raw.trim()}" \u2014 failing closed to "none" (no write tools registered). Valid values: none, drafts, all.`
|
|
38646
|
-
);
|
|
38647
|
-
return "none";
|
|
38648
|
-
}
|
|
38649
|
-
function getDefaultInlineAttachments() {
|
|
38650
|
-
return parseBoolEnv("OFW_INLINE_ATTACHMENTS");
|
|
38651
|
-
}
|
|
38652
|
-
|
|
38653
|
-
// src/cache.ts
|
|
38654
|
-
var instance = null;
|
|
38655
|
-
var SCHEMA_V1 = `
|
|
38656
|
-
CREATE TABLE IF NOT EXISTS messages (
|
|
38657
|
-
id INTEGER PRIMARY KEY,
|
|
38658
|
-
folder TEXT NOT NULL,
|
|
38659
|
-
subject TEXT NOT NULL,
|
|
38660
|
-
from_user TEXT NOT NULL,
|
|
38661
|
-
sent_at TEXT NOT NULL,
|
|
38662
|
-
recipients_json TEXT NOT NULL,
|
|
38663
|
-
body TEXT,
|
|
38664
|
-
fetched_body_at TEXT,
|
|
38665
|
-
reply_to_id INTEGER,
|
|
38666
|
-
chain_root_id INTEGER,
|
|
38667
|
-
list_data_json TEXT NOT NULL,
|
|
38668
|
-
last_seen_at TEXT NOT NULL
|
|
38669
|
-
);
|
|
38670
|
-
CREATE INDEX IF NOT EXISTS idx_messages_folder_sent_at ON messages(folder, sent_at DESC);
|
|
38671
|
-
CREATE INDEX IF NOT EXISTS idx_messages_chain_root ON messages(chain_root_id);
|
|
38672
|
-
|
|
38673
|
-
CREATE TABLE IF NOT EXISTS drafts (
|
|
38674
|
-
id INTEGER PRIMARY KEY,
|
|
38675
|
-
subject TEXT NOT NULL,
|
|
38676
|
-
body TEXT NOT NULL,
|
|
38677
|
-
recipients_json TEXT NOT NULL,
|
|
38678
|
-
reply_to_id INTEGER,
|
|
38679
|
-
modified_at TEXT NOT NULL,
|
|
38680
|
-
list_data_json TEXT NOT NULL
|
|
38681
|
-
);
|
|
38682
|
-
|
|
38683
|
-
CREATE TABLE IF NOT EXISTS sync_state (
|
|
38684
|
-
folder TEXT PRIMARY KEY,
|
|
38685
|
-
last_sync_at TEXT NOT NULL,
|
|
38686
|
-
newest_id INTEGER
|
|
38687
|
-
);
|
|
38688
|
-
|
|
38689
|
-
CREATE TABLE IF NOT EXISTS meta (
|
|
38690
|
-
key TEXT PRIMARY KEY,
|
|
38691
|
-
value TEXT NOT NULL
|
|
38692
|
-
);
|
|
38693
|
-
`;
|
|
38694
|
-
var SCHEMA_V2 = `
|
|
38695
|
-
CREATE TABLE IF NOT EXISTS attachments (
|
|
38696
|
-
file_id INTEGER PRIMARY KEY,
|
|
38697
|
-
file_name TEXT NOT NULL,
|
|
38698
|
-
label TEXT NOT NULL,
|
|
38699
|
-
mime_type TEXT NOT NULL,
|
|
38700
|
-
size_bytes INTEGER,
|
|
38701
|
-
metadata_json TEXT NOT NULL,
|
|
38702
|
-
message_ids_json TEXT NOT NULL, -- JSON array of message ids that reference this file
|
|
38703
|
-
downloaded_path TEXT, -- absolute path on disk if/when downloaded
|
|
38704
|
-
downloaded_at TEXT,
|
|
38705
|
-
fetched_metadata_at TEXT NOT NULL
|
|
38706
|
-
);
|
|
38707
|
-
`;
|
|
38708
|
-
function migrate(db) {
|
|
38709
|
-
db.exec(SCHEMA_V1);
|
|
38710
|
-
db.exec(SCHEMA_V2);
|
|
38711
|
-
db.prepare(
|
|
38712
|
-
"INSERT INTO meta(key, value) VALUES(?, ?) ON CONFLICT(key) DO UPDATE SET value=excluded.value"
|
|
38713
|
-
).run("schema_version", "2");
|
|
38714
|
-
}
|
|
38715
|
-
function enforceCachePermissions(dbPath) {
|
|
38716
|
-
chmodSync(dirname2(dbPath), 448);
|
|
38717
|
-
chmodSync(dbPath, 384);
|
|
38718
|
-
for (const sibling of [`${dbPath}-wal`, `${dbPath}-shm`]) {
|
|
38719
|
-
if (existsSync(sibling)) chmodSync(sibling, 384);
|
|
38720
|
-
}
|
|
38721
|
-
}
|
|
38722
|
-
function openCache() {
|
|
38723
|
-
if (instance) return instance;
|
|
38724
|
-
const path = getCacheDbPath();
|
|
38725
|
-
mkdirSync(dirname2(path), { recursive: true });
|
|
38726
|
-
const db = new DatabaseSync(path);
|
|
38727
|
-
enforceCachePermissions(path);
|
|
38728
|
-
db.exec("PRAGMA journal_mode = WAL");
|
|
38729
|
-
db.exec("PRAGMA foreign_keys = ON");
|
|
38730
|
-
migrate(db);
|
|
38731
|
-
enforceCachePermissions(path);
|
|
38732
|
-
instance = { db };
|
|
38733
|
-
return instance;
|
|
38734
|
-
}
|
|
38735
|
-
function rowFromDb(r) {
|
|
38736
|
-
return {
|
|
38737
|
-
id: r.id,
|
|
38738
|
-
folder: r.folder,
|
|
38739
|
-
subject: r.subject,
|
|
38740
|
-
fromUser: r.from_user,
|
|
38741
|
-
sentAt: r.sent_at,
|
|
38742
|
-
recipients: JSON.parse(r.recipients_json),
|
|
38743
|
-
body: r.body,
|
|
38744
|
-
fetchedBodyAt: r.fetched_body_at,
|
|
38745
|
-
replyToId: r.reply_to_id,
|
|
38746
|
-
chainRootId: r.chain_root_id,
|
|
38747
|
-
listData: JSON.parse(r.list_data_json)
|
|
38748
|
-
};
|
|
38749
|
-
}
|
|
38750
|
-
function nullish3(v) {
|
|
38751
|
-
return v === void 0 ? null : v;
|
|
38752
|
-
}
|
|
38753
|
-
function requireString(field, v) {
|
|
38754
|
-
if (typeof v === "string") return v;
|
|
38755
|
-
throw new Error(`cache: ${field} is required (got ${v === void 0 ? "undefined" : "null"})`);
|
|
38756
|
-
}
|
|
38757
|
-
function upsertMessage(row) {
|
|
38758
|
-
const { db } = openCache();
|
|
38759
|
-
db.prepare(
|
|
38760
|
-
`INSERT INTO messages (
|
|
38761
|
-
id, folder, subject, from_user, sent_at, recipients_json,
|
|
38762
|
-
body, fetched_body_at, reply_to_id, chain_root_id, list_data_json, last_seen_at
|
|
38763
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
38764
|
-
ON CONFLICT(id) DO UPDATE SET
|
|
38765
|
-
folder=excluded.folder,
|
|
38766
|
-
subject=excluded.subject,
|
|
38767
|
-
from_user=excluded.from_user,
|
|
38768
|
-
sent_at=excluded.sent_at,
|
|
38769
|
-
recipients_json=excluded.recipients_json,
|
|
38770
|
-
body=excluded.body,
|
|
38771
|
-
fetched_body_at=excluded.fetched_body_at,
|
|
38772
|
-
reply_to_id=excluded.reply_to_id,
|
|
38773
|
-
chain_root_id=excluded.chain_root_id,
|
|
38774
|
-
list_data_json=excluded.list_data_json,
|
|
38775
|
-
last_seen_at=excluded.last_seen_at`
|
|
38776
|
-
).run(
|
|
38777
|
-
row.id,
|
|
38778
|
-
requireString("messages.folder", row.folder),
|
|
38779
|
-
requireString("messages.subject", row.subject),
|
|
38780
|
-
requireString("messages.fromUser", row.fromUser),
|
|
38781
|
-
requireString("messages.sentAt", row.sentAt),
|
|
38782
|
-
JSON.stringify(row.recipients ?? []),
|
|
38783
|
-
nullish3(row.body),
|
|
38784
|
-
nullish3(row.fetchedBodyAt),
|
|
38785
|
-
nullish3(row.replyToId),
|
|
38786
|
-
nullish3(row.chainRootId),
|
|
38787
|
-
JSON.stringify(row.listData ?? null),
|
|
38788
|
-
(/* @__PURE__ */ new Date()).toISOString()
|
|
38789
|
-
);
|
|
38790
|
-
}
|
|
38791
|
-
function getMessage(id) {
|
|
38792
|
-
const { db } = openCache();
|
|
38793
|
-
const r = db.prepare("SELECT * FROM messages WHERE id = ?").get(id);
|
|
38794
|
-
return r ? rowFromDb(r) : null;
|
|
38795
|
-
}
|
|
38796
|
-
function deleteMessage(id) {
|
|
38797
|
-
const { db } = openCache();
|
|
38798
|
-
db.prepare("DELETE FROM messages WHERE id = ?").run(id);
|
|
38799
|
-
}
|
|
38800
|
-
function buildMessageFilter(opts) {
|
|
38801
|
-
const wheres = [];
|
|
38802
|
-
const params = [];
|
|
38803
|
-
if (opts.folder !== void 0) {
|
|
38804
|
-
wheres.push("folder = ?");
|
|
38805
|
-
params.push(opts.folder);
|
|
38806
|
-
}
|
|
38807
|
-
if (opts.since !== void 0) {
|
|
38808
|
-
wheres.push("sent_at >= ?");
|
|
38809
|
-
params.push(opts.since);
|
|
38810
|
-
}
|
|
38811
|
-
if (opts.until !== void 0) {
|
|
38812
|
-
wheres.push("sent_at < ?");
|
|
38813
|
-
params.push(opts.until);
|
|
38814
|
-
}
|
|
38815
|
-
if (opts.q !== void 0 && opts.q.length > 0) {
|
|
38816
|
-
const pattern = `%${opts.q}%`;
|
|
38817
|
-
wheres.push("(subject LIKE ? OR body LIKE ?)");
|
|
38818
|
-
params.push(pattern, pattern);
|
|
38819
|
-
}
|
|
38820
|
-
return {
|
|
38821
|
-
where: wheres.length > 0 ? `WHERE ${wheres.join(" AND ")}` : "",
|
|
38822
|
-
params
|
|
38823
|
-
};
|
|
38824
|
-
}
|
|
38825
|
-
function listMessages(opts) {
|
|
38826
|
-
const { db } = openCache();
|
|
38827
|
-
const { where, params } = buildMessageFilter(opts);
|
|
38828
|
-
const offset = (opts.page - 1) * opts.size;
|
|
38829
|
-
const rows = db.prepare(
|
|
38830
|
-
`SELECT * FROM messages ${where}
|
|
38831
|
-
ORDER BY sent_at DESC, id DESC
|
|
38832
|
-
LIMIT ? OFFSET ?`
|
|
38833
|
-
).all(...params, opts.size, offset);
|
|
38834
|
-
return rows.map(rowFromDb);
|
|
38835
|
-
}
|
|
38836
|
-
function countMessages(opts) {
|
|
38837
|
-
const { db } = openCache();
|
|
38838
|
-
const { where, params } = buildMessageFilter(opts);
|
|
38839
|
-
const r = db.prepare(`SELECT COUNT(*) as n FROM messages ${where}`).get(...params);
|
|
38840
|
-
return r?.n ?? 0;
|
|
38841
|
-
}
|
|
38842
|
-
function draftFromDb(r) {
|
|
38843
|
-
return {
|
|
38844
|
-
id: r.id,
|
|
38845
|
-
subject: r.subject,
|
|
38846
|
-
body: r.body,
|
|
38847
|
-
recipients: JSON.parse(r.recipients_json),
|
|
38848
|
-
replyToId: r.reply_to_id,
|
|
38849
|
-
modifiedAt: r.modified_at,
|
|
38850
|
-
listData: JSON.parse(r.list_data_json)
|
|
38851
|
-
};
|
|
38852
|
-
}
|
|
38853
|
-
function upsertDraft(row) {
|
|
38854
|
-
const { db } = openCache();
|
|
38855
|
-
db.prepare(
|
|
38856
|
-
`INSERT INTO drafts (id, subject, body, recipients_json, reply_to_id, modified_at, list_data_json)
|
|
38857
|
-
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
38858
|
-
ON CONFLICT(id) DO UPDATE SET
|
|
38859
|
-
subject=excluded.subject,
|
|
38860
|
-
body=excluded.body,
|
|
38861
|
-
recipients_json=excluded.recipients_json,
|
|
38862
|
-
reply_to_id=excluded.reply_to_id,
|
|
38863
|
-
modified_at=excluded.modified_at,
|
|
38864
|
-
list_data_json=excluded.list_data_json`
|
|
38865
|
-
).run(
|
|
38866
|
-
row.id,
|
|
38867
|
-
requireString("drafts.subject", row.subject),
|
|
38868
|
-
requireString("drafts.body", row.body),
|
|
38869
|
-
JSON.stringify(row.recipients ?? []),
|
|
38870
|
-
nullish3(row.replyToId),
|
|
38871
|
-
requireString("drafts.modifiedAt", row.modifiedAt),
|
|
38872
|
-
JSON.stringify(row.listData ?? null)
|
|
38873
|
-
);
|
|
38874
|
-
}
|
|
38875
|
-
function getDraft(id) {
|
|
38876
|
-
const { db } = openCache();
|
|
38877
|
-
const r = db.prepare("SELECT * FROM drafts WHERE id = ?").get(id);
|
|
38878
|
-
return r ? draftFromDb(r) : null;
|
|
38879
|
-
}
|
|
38880
|
-
function listDrafts(opts) {
|
|
38881
|
-
const { db } = openCache();
|
|
38882
|
-
const offset = (opts.page - 1) * opts.size;
|
|
38883
|
-
const rows = db.prepare(
|
|
38884
|
-
"SELECT * FROM drafts ORDER BY modified_at DESC, id DESC LIMIT ? OFFSET ?"
|
|
38885
|
-
).all(opts.size, offset);
|
|
38886
|
-
return rows.map(draftFromDb);
|
|
38887
|
-
}
|
|
38888
|
-
function deleteDraft(id) {
|
|
38889
|
-
const { db } = openCache();
|
|
38890
|
-
db.prepare("DELETE FROM drafts WHERE id = ?").run(id);
|
|
38891
|
-
}
|
|
38892
|
-
function listDraftIds() {
|
|
38893
|
-
const { db } = openCache();
|
|
38894
|
-
const rows = db.prepare("SELECT id FROM drafts").all();
|
|
38895
|
-
return rows.map((r) => r.id);
|
|
38896
|
-
}
|
|
38897
|
-
function setSyncState(folder, state) {
|
|
38898
|
-
const { db } = openCache();
|
|
38899
|
-
db.prepare(
|
|
38900
|
-
`INSERT INTO sync_state (folder, last_sync_at, newest_id) VALUES (?, ?, ?)
|
|
38901
|
-
ON CONFLICT(folder) DO UPDATE SET
|
|
38902
|
-
last_sync_at = excluded.last_sync_at,
|
|
38903
|
-
newest_id = excluded.newest_id`
|
|
38904
|
-
).run(folder, state.lastSyncAt, state.newestId);
|
|
38905
|
-
}
|
|
38906
|
-
function setMeta(key, value) {
|
|
38907
|
-
const { db } = openCache();
|
|
38908
|
-
db.prepare(
|
|
38909
|
-
"INSERT INTO meta (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value=excluded.value"
|
|
38910
|
-
).run(key, value);
|
|
38911
|
-
}
|
|
38912
|
-
function findLatestReplyTip(replyToId) {
|
|
38913
|
-
const { db } = openCache();
|
|
38914
|
-
const parent = db.prepare(
|
|
38915
|
-
"SELECT id, folder, chain_root_id FROM messages WHERE id = ?"
|
|
38916
|
-
).get(replyToId);
|
|
38917
|
-
if (!parent) return replyToId;
|
|
38918
|
-
const chainRoot = parent.chain_root_id ?? parent.id;
|
|
38919
|
-
const tip = db.prepare(
|
|
38920
|
-
`SELECT id FROM messages
|
|
38921
|
-
WHERE folder = 'sent' AND chain_root_id = ?
|
|
38922
|
-
ORDER BY id DESC LIMIT 1`
|
|
38923
|
-
).get(chainRoot);
|
|
38924
|
-
return tip ? tip.id : replyToId;
|
|
38925
|
-
}
|
|
38926
|
-
function attachmentFromDb(r) {
|
|
38927
|
-
return {
|
|
38928
|
-
fileId: r.file_id,
|
|
38929
|
-
fileName: r.file_name,
|
|
38930
|
-
label: r.label,
|
|
38931
|
-
mimeType: r.mime_type,
|
|
38932
|
-
sizeBytes: r.size_bytes,
|
|
38933
|
-
metadata: JSON.parse(r.metadata_json),
|
|
38934
|
-
messageIds: JSON.parse(r.message_ids_json),
|
|
38935
|
-
downloadedPath: r.downloaded_path,
|
|
38936
|
-
downloadedAt: r.downloaded_at
|
|
38937
|
-
};
|
|
38938
|
-
}
|
|
38939
|
-
function getAttachment(fileId) {
|
|
38940
|
-
const { db } = openCache();
|
|
38941
|
-
const r = db.prepare("SELECT * FROM attachments WHERE file_id = ?").get(fileId);
|
|
38942
|
-
return r ? attachmentFromDb(r) : null;
|
|
38943
|
-
}
|
|
38944
|
-
function listAttachmentsForMessage(messageId) {
|
|
38945
|
-
const { db } = openCache();
|
|
38946
|
-
const rows = db.prepare(
|
|
38947
|
-
`SELECT * FROM attachments
|
|
38948
|
-
WHERE EXISTS (SELECT 1 FROM json_each(message_ids_json) WHERE value = ?)
|
|
38949
|
-
ORDER BY file_id`
|
|
38950
|
-
).all(messageId);
|
|
38951
|
-
return rows.map(attachmentFromDb);
|
|
38952
|
-
}
|
|
38953
|
-
function upsertAttachmentForMessage(input) {
|
|
38954
|
-
const { db } = openCache();
|
|
38955
|
-
const existing = db.prepare("SELECT message_ids_json FROM attachments WHERE file_id = ?").get(input.fileId);
|
|
38956
|
-
const prior = existing ? JSON.parse(existing.message_ids_json) : [];
|
|
38957
|
-
let messageIds;
|
|
38958
|
-
if (input.messageId === 0) {
|
|
38959
|
-
messageIds = prior;
|
|
38960
|
-
} else if (prior.includes(input.messageId)) {
|
|
38961
|
-
messageIds = prior;
|
|
38962
|
-
} else {
|
|
38963
|
-
messageIds = [...prior, input.messageId];
|
|
38964
|
-
}
|
|
38965
|
-
db.prepare(
|
|
38966
|
-
`INSERT INTO attachments (
|
|
38967
|
-
file_id, file_name, label, mime_type, size_bytes,
|
|
38968
|
-
metadata_json, message_ids_json, fetched_metadata_at
|
|
38969
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
38970
|
-
ON CONFLICT(file_id) DO UPDATE SET
|
|
38971
|
-
file_name=excluded.file_name,
|
|
38972
|
-
label=excluded.label,
|
|
38973
|
-
mime_type=excluded.mime_type,
|
|
38974
|
-
size_bytes=excluded.size_bytes,
|
|
38975
|
-
metadata_json=excluded.metadata_json,
|
|
38976
|
-
message_ids_json=excluded.message_ids_json,
|
|
38977
|
-
fetched_metadata_at=excluded.fetched_metadata_at`
|
|
38978
|
-
).run(
|
|
38979
|
-
input.fileId,
|
|
38980
|
-
requireString("attachments.fileName", input.fileName),
|
|
38981
|
-
requireString("attachments.label", input.label),
|
|
38982
|
-
requireString("attachments.mimeType", input.mimeType),
|
|
38983
|
-
nullish3(input.sizeBytes),
|
|
38984
|
-
JSON.stringify(input.metadata ?? null),
|
|
38985
|
-
JSON.stringify(messageIds),
|
|
38986
|
-
(/* @__PURE__ */ new Date()).toISOString()
|
|
38987
|
-
);
|
|
38988
|
-
}
|
|
38989
|
-
function markAttachmentDownloaded(fileId, path) {
|
|
38990
|
-
const { db } = openCache();
|
|
38991
|
-
db.prepare(
|
|
38992
|
-
"UPDATE attachments SET downloaded_path = ?, downloaded_at = ? WHERE file_id = ?"
|
|
38993
|
-
).run(path, (/* @__PURE__ */ new Date()).toISOString(), fileId);
|
|
38994
|
-
}
|
|
38995
|
-
|
|
38996
38762
|
// src/sync.ts
|
|
38997
38763
|
var FileMetaSchema = external_exports.looseObject({
|
|
38998
38764
|
fileId: external_exports.number(),
|
|
@@ -39002,13 +38768,13 @@ var FileMetaSchema = external_exports.looseObject({
|
|
|
39002
38768
|
// MIME
|
|
39003
38769
|
fileSize: external_exports.number().optional()
|
|
39004
38770
|
});
|
|
39005
|
-
async function fetchAttachmentMeta(client2, fileId, messageId) {
|
|
38771
|
+
async function fetchAttachmentMeta(client2, fileId, messageId, store) {
|
|
39006
38772
|
const meta3 = parseLenient(
|
|
39007
38773
|
FileMetaSchema,
|
|
39008
38774
|
await client2.request("GET", `/pub/v1/myfiles/${fileId}`),
|
|
39009
38775
|
{ label: "ofw-mcp", context: "GET /pub/v1/myfiles/{fileId}" }
|
|
39010
38776
|
);
|
|
39011
|
-
upsertAttachmentForMessage({
|
|
38777
|
+
await store.upsertAttachmentForMessage({
|
|
39012
38778
|
fileId: meta3.fileId ?? fileId,
|
|
39013
38779
|
fileName: meta3.fileName ?? `file-${fileId}`,
|
|
39014
38780
|
label: meta3.label ?? meta3.fileName ?? `file-${fileId}`,
|
|
@@ -39018,13 +38784,33 @@ async function fetchAttachmentMeta(client2, fileId, messageId) {
|
|
|
39018
38784
|
messageId
|
|
39019
38785
|
});
|
|
39020
38786
|
}
|
|
39021
|
-
async function fetchAttachmentMetaForMessage(client2, messageId, fileIds) {
|
|
39022
|
-
await Promise.allSettled(fileIds.map((fid) => fetchAttachmentMeta(client2, fid, messageId)));
|
|
38787
|
+
async function fetchAttachmentMetaForMessage(client2, messageId, fileIds, store) {
|
|
38788
|
+
await Promise.allSettled(fileIds.map((fid) => fetchAttachmentMeta(client2, fid, messageId, store)));
|
|
38789
|
+
}
|
|
38790
|
+
function makeBudget(max) {
|
|
38791
|
+
let remaining = max;
|
|
38792
|
+
return {
|
|
38793
|
+
take() {
|
|
38794
|
+
if (remaining <= 0) return false;
|
|
38795
|
+
remaining -= 1;
|
|
38796
|
+
return true;
|
|
38797
|
+
}
|
|
38798
|
+
};
|
|
38799
|
+
}
|
|
38800
|
+
async function fetchAttachmentMetaBudgeted(client2, messageId, fileIds, store, budget) {
|
|
38801
|
+
const affordable = [];
|
|
38802
|
+
for (const fid of fileIds) {
|
|
38803
|
+
if (!budget.take()) break;
|
|
38804
|
+
affordable.push(fid);
|
|
38805
|
+
}
|
|
38806
|
+
if (affordable.length > 0) {
|
|
38807
|
+
await fetchAttachmentMetaForMessage(client2, messageId, affordable, store);
|
|
38808
|
+
}
|
|
39023
38809
|
}
|
|
39024
38810
|
var FoldersSchema = external_exports.looseObject({
|
|
39025
38811
|
systemFolders: external_exports.array(external_exports.looseObject({ id: external_exports.string(), folderType: external_exports.string() })).optional()
|
|
39026
38812
|
});
|
|
39027
|
-
async function resolveFolderIds(client2) {
|
|
38813
|
+
async function resolveFolderIds(client2, store) {
|
|
39028
38814
|
const data = parseLenient(
|
|
39029
38815
|
FoldersSchema,
|
|
39030
38816
|
await client2.request("GET", "/pub/v1/messageFolders?includeFolderCounts=true"),
|
|
@@ -39041,7 +38827,8 @@ async function resolveFolderIds(client2) {
|
|
|
39041
38827
|
sent: find("SENT_MESSAGES"),
|
|
39042
38828
|
drafts: find("DRAFTS")
|
|
39043
38829
|
};
|
|
39044
|
-
setMeta("drafts_folder_id", ids.drafts);
|
|
38830
|
+
await store.setMeta("drafts_folder_id", ids.drafts);
|
|
38831
|
+
await store.setMeta("sent_folder_id", ids.sent);
|
|
39045
38832
|
return ids;
|
|
39046
38833
|
}
|
|
39047
38834
|
var ListItemSchema = external_exports.looseObject({
|
|
@@ -39055,14 +38842,22 @@ var ListItemSchema = external_exports.looseObject({
|
|
|
39055
38842
|
var ListResponseSchema = external_exports.looseObject({ data: external_exports.array(ListItemSchema).optional() });
|
|
39056
38843
|
var DetailResponseSchema = external_exports.looseObject({
|
|
39057
38844
|
body: external_exports.string().optional(),
|
|
39058
|
-
files: external_exports.array(external_exports.number()).optional()
|
|
38845
|
+
files: external_exports.array(external_exports.number()).optional(),
|
|
38846
|
+
// The detail endpoint carries the REAL recipient view timestamps (the list
|
|
38847
|
+
// endpoint only has an epoch placeholder) — used by the view-status refresh.
|
|
38848
|
+
recipients: external_exports.array(ApiRecipientSchema).optional()
|
|
39059
38849
|
});
|
|
39060
|
-
|
|
39061
|
-
|
|
39062
|
-
|
|
38850
|
+
var maxId = (a, b) => a === null ? b : b === null ? a : Math.max(a, b);
|
|
38851
|
+
async function walkPages(client2, folder, folderId, opts, store) {
|
|
38852
|
+
const budget = opts.budget;
|
|
38853
|
+
let page = opts.startPage;
|
|
39063
38854
|
let newestId = null;
|
|
38855
|
+
let synced = 0;
|
|
39064
38856
|
const unread = [];
|
|
39065
38857
|
while (true) {
|
|
38858
|
+
if (!budget.take()) {
|
|
38859
|
+
return { synced, unread, newestId, done: false, nextPage: page };
|
|
38860
|
+
}
|
|
39066
38861
|
const path = `/pub/v3/messages?folders=${encodeURIComponent(folderId)}&page=${page}&size=50&sort=date&sortDirection=desc`;
|
|
39067
38862
|
const list = parseLenient(
|
|
39068
38863
|
ListResponseSchema,
|
|
@@ -39070,19 +38865,46 @@ async function syncMessageFolder(client2, folder, folderId, opts) {
|
|
|
39070
38865
|
{ label: "ofw-mcp", context: `GET /pub/v3/messages?folders={${folder}}` }
|
|
39071
38866
|
);
|
|
39072
38867
|
const items = list.data ?? [];
|
|
39073
|
-
if (items.length === 0)
|
|
38868
|
+
if (items.length === 0) {
|
|
38869
|
+
return { synced, unread, newestId, done: true, nextPage: null };
|
|
38870
|
+
}
|
|
38871
|
+
const existingById = new Map(
|
|
38872
|
+
(await store.getMessages(items.map((it) => it.id))).map((row) => [row.id, row])
|
|
38873
|
+
);
|
|
38874
|
+
const toUpsert = [];
|
|
39074
38875
|
let pageHadNewItem = false;
|
|
38876
|
+
let pageBudgetHit = false;
|
|
39075
38877
|
for (const item of items) {
|
|
39076
38878
|
if (newestId === null || item.id > newestId) newestId = item.id;
|
|
39077
|
-
const existing =
|
|
39078
|
-
if (existing)
|
|
38879
|
+
const existing = existingById.get(item.id);
|
|
38880
|
+
if (existing) {
|
|
38881
|
+
if (folder === "sent" && item.showNeverViewed === false && !hasRealView(existing.recipients)) {
|
|
38882
|
+
if (!budget.take()) {
|
|
38883
|
+
pageBudgetHit = true;
|
|
38884
|
+
break;
|
|
38885
|
+
}
|
|
38886
|
+
const detail = parseLenient(
|
|
38887
|
+
DetailResponseSchema,
|
|
38888
|
+
await client2.request("GET", `/pub/v3/messages/${item.id}`),
|
|
38889
|
+
{ label: "ofw-mcp", context: "GET /pub/v3/messages/{id} (view-status refresh)" }
|
|
38890
|
+
);
|
|
38891
|
+
toUpsert.push({ ...existing, recipients: mapRecipients(detail.recipients), listData: item });
|
|
38892
|
+
synced++;
|
|
38893
|
+
}
|
|
38894
|
+
continue;
|
|
38895
|
+
}
|
|
39079
38896
|
pageHadNewItem = true;
|
|
39080
38897
|
const isInboxUnread = folder === "inbox" && item.showNeverViewed === true;
|
|
39081
38898
|
const shouldFetchBody = !isInboxUnread || opts.fetchUnreadBodies;
|
|
39082
38899
|
let body = null;
|
|
39083
38900
|
let fetchedBodyAt = null;
|
|
39084
38901
|
let detailFileIds = [];
|
|
38902
|
+
let detailRecipients;
|
|
39085
38903
|
if (shouldFetchBody) {
|
|
38904
|
+
if (!budget.take()) {
|
|
38905
|
+
pageBudgetHit = true;
|
|
38906
|
+
break;
|
|
38907
|
+
}
|
|
39086
38908
|
const detail = parseLenient(
|
|
39087
38909
|
DetailResponseSchema,
|
|
39088
38910
|
await client2.request("GET", `/pub/v3/messages/${item.id}`),
|
|
@@ -39090,6 +38912,7 @@ async function syncMessageFolder(client2, folder, folderId, opts) {
|
|
|
39090
38912
|
);
|
|
39091
38913
|
body = detail.body ?? "";
|
|
39092
38914
|
fetchedBodyAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
38915
|
+
detailRecipients = detail.recipients;
|
|
39093
38916
|
if (Array.isArray(detail.files) && detail.files.length > 0) {
|
|
39094
38917
|
detailFileIds = detail.files;
|
|
39095
38918
|
}
|
|
@@ -39107,27 +38930,72 @@ async function syncMessageFolder(client2, folder, folderId, opts) {
|
|
|
39107
38930
|
subject: item.subject ?? "(no subject)",
|
|
39108
38931
|
fromUser: item.from?.name ?? "",
|
|
39109
38932
|
sentAt: item.date?.dateTime ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
39110
|
-
recipients: mapRecipients(item.recipients),
|
|
38933
|
+
recipients: mapRecipients(detailRecipients ?? item.recipients),
|
|
39111
38934
|
body,
|
|
39112
38935
|
fetchedBodyAt,
|
|
39113
38936
|
replyToId: null,
|
|
39114
38937
|
chainRootId: null,
|
|
39115
38938
|
listData: item
|
|
39116
38939
|
};
|
|
39117
|
-
|
|
38940
|
+
toUpsert.push(row);
|
|
39118
38941
|
synced++;
|
|
39119
38942
|
if (detailFileIds.length > 0) {
|
|
39120
|
-
await
|
|
38943
|
+
await fetchAttachmentMetaBudgeted(client2, item.id, detailFileIds, store, budget);
|
|
39121
38944
|
}
|
|
39122
38945
|
}
|
|
39123
|
-
|
|
38946
|
+
await store.upsertMessages(toUpsert);
|
|
38947
|
+
if (pageBudgetHit) {
|
|
38948
|
+
return { synced, unread, newestId, done: false, nextPage: page };
|
|
38949
|
+
}
|
|
38950
|
+
if (opts.stopAtCachedPage && !pageHadNewItem) {
|
|
38951
|
+
return { synced, unread, newestId, done: true, nextPage: page };
|
|
38952
|
+
}
|
|
39124
38953
|
page++;
|
|
39125
38954
|
}
|
|
39126
|
-
|
|
38955
|
+
}
|
|
38956
|
+
async function syncMessageFolder(client2, folder, folderId, opts, store) {
|
|
38957
|
+
const budget = opts.budget ?? makeBudget(Number.POSITIVE_INFINITY);
|
|
38958
|
+
const saved = await store.getSyncState(folder);
|
|
38959
|
+
const savedResume = saved?.resumePage ?? null;
|
|
38960
|
+
const fwd = await walkPages(client2, folder, folderId, {
|
|
38961
|
+
startPage: 1,
|
|
38962
|
+
stopAtCachedPage: true,
|
|
38963
|
+
fetchUnreadBodies: opts.fetchUnreadBodies,
|
|
38964
|
+
budget
|
|
38965
|
+
}, store);
|
|
38966
|
+
let synced = fwd.synced;
|
|
38967
|
+
const unread = [...fwd.unread];
|
|
38968
|
+
let newestId = maxId(saved?.newestId ?? null, fwd.newestId);
|
|
38969
|
+
let done;
|
|
38970
|
+
let resumePage;
|
|
38971
|
+
if (!fwd.done) {
|
|
38972
|
+
done = false;
|
|
38973
|
+
resumePage = savedResume === null ? fwd.nextPage : Math.min(fwd.nextPage, savedResume);
|
|
38974
|
+
} else if (fwd.nextPage === null) {
|
|
38975
|
+
done = true;
|
|
38976
|
+
resumePage = null;
|
|
38977
|
+
} else if (savedResume === null && !opts.deep) {
|
|
38978
|
+
done = true;
|
|
38979
|
+
resumePage = null;
|
|
38980
|
+
} else {
|
|
38981
|
+
const bf = await walkPages(client2, folder, folderId, {
|
|
38982
|
+
startPage: savedResume ?? fwd.nextPage,
|
|
38983
|
+
stopAtCachedPage: false,
|
|
38984
|
+
fetchUnreadBodies: opts.fetchUnreadBodies,
|
|
38985
|
+
budget
|
|
38986
|
+
}, store);
|
|
38987
|
+
synced += bf.synced;
|
|
38988
|
+
unread.push(...bf.unread);
|
|
38989
|
+
newestId = maxId(newestId, bf.newestId);
|
|
38990
|
+
done = bf.done;
|
|
38991
|
+
resumePage = bf.done ? null : bf.nextPage;
|
|
38992
|
+
}
|
|
38993
|
+
await store.setSyncState(folder, {
|
|
39127
38994
|
lastSyncAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
39128
|
-
newestId
|
|
38995
|
+
newestId,
|
|
38996
|
+
resumePage
|
|
39129
38997
|
});
|
|
39130
|
-
return { synced, unread };
|
|
38998
|
+
return { synced, unread, done };
|
|
39131
38999
|
}
|
|
39132
39000
|
var DraftListItemSchema = external_exports.looseObject({
|
|
39133
39001
|
id: external_exports.number(),
|
|
@@ -39141,10 +39009,12 @@ var DraftDetailSchema = external_exports.looseObject({
|
|
|
39141
39009
|
body: external_exports.string().optional(),
|
|
39142
39010
|
subject: external_exports.string().optional()
|
|
39143
39011
|
});
|
|
39144
|
-
async function syncDrafts(client2, draftsFolderId) {
|
|
39012
|
+
async function syncDrafts(client2, draftsFolderId, store, budget) {
|
|
39013
|
+
const b = budget ?? makeBudget(Number.POSITIVE_INFINITY);
|
|
39145
39014
|
const items = [];
|
|
39146
39015
|
let page = 1;
|
|
39147
39016
|
while (true) {
|
|
39017
|
+
if (!b.take()) return { synced: 0, done: false };
|
|
39148
39018
|
const path = `/pub/v3/messages?folders=${encodeURIComponent(draftsFolderId)}&page=${page}&size=50&sort=date&sortDirection=desc`;
|
|
39149
39019
|
const list = parseLenient(
|
|
39150
39020
|
DraftListResponseSchema,
|
|
@@ -39156,81 +39026,149 @@ async function syncDrafts(client2, draftsFolderId) {
|
|
|
39156
39026
|
if (pageItems.length < 50) break;
|
|
39157
39027
|
page++;
|
|
39158
39028
|
}
|
|
39159
|
-
const
|
|
39160
|
-
let synced = 0;
|
|
39029
|
+
const rows = [];
|
|
39161
39030
|
for (const item of items) {
|
|
39162
|
-
|
|
39163
|
-
const modifiedAt = item.date?.dateTime ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
39164
|
-
const existing = getDraft(item.id);
|
|
39031
|
+
if (!b.take()) return { synced: 0, done: false };
|
|
39165
39032
|
const detail = parseLenient(
|
|
39166
39033
|
DraftDetailSchema,
|
|
39167
39034
|
await client2.request("GET", `/pub/v3/messages/${item.id}`),
|
|
39168
39035
|
{ label: "ofw-mcp", context: "GET /pub/v3/messages/{id} (drafts sync)" }
|
|
39169
39036
|
);
|
|
39170
|
-
|
|
39037
|
+
rows.push({
|
|
39171
39038
|
id: item.id,
|
|
39172
39039
|
subject: detail.subject ?? item.subject ?? "(no subject)",
|
|
39173
39040
|
body: detail.body ?? "",
|
|
39174
39041
|
recipients: mapRecipients(item.recipients),
|
|
39175
39042
|
replyToId: item.replyToId ?? null,
|
|
39176
|
-
modifiedAt,
|
|
39043
|
+
modifiedAt: item.date?.dateTime ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
39177
39044
|
listData: item
|
|
39178
|
-
};
|
|
39179
|
-
|
|
39180
|
-
|
|
39045
|
+
});
|
|
39046
|
+
}
|
|
39047
|
+
const ids = items.map((it) => it.id);
|
|
39048
|
+
const existingById = new Map((await store.getDrafts(ids)).map((d) => [d.id, d]));
|
|
39049
|
+
await store.upsertDrafts(rows);
|
|
39050
|
+
for (const stale of await store.getMessages(ids)) {
|
|
39051
|
+
await store.deleteMessage(stale.id);
|
|
39052
|
+
}
|
|
39053
|
+
let synced = 0;
|
|
39054
|
+
for (const row of rows) {
|
|
39055
|
+
const existing = existingById.get(row.id);
|
|
39181
39056
|
if (!existing || existing.body !== row.body || existing.subject !== row.subject || existing.replyToId !== row.replyToId) {
|
|
39182
39057
|
synced++;
|
|
39183
39058
|
}
|
|
39184
39059
|
}
|
|
39185
|
-
|
|
39186
|
-
|
|
39060
|
+
const seenIds = new Set(ids);
|
|
39061
|
+
for (const id of await store.listDraftIds()) {
|
|
39062
|
+
if (!seenIds.has(id)) await store.deleteDraft(id);
|
|
39187
39063
|
}
|
|
39188
|
-
return { synced };
|
|
39064
|
+
return { synced, done: true };
|
|
39189
39065
|
}
|
|
39190
|
-
async function syncAll(client2, opts) {
|
|
39066
|
+
async function syncAll(client2, opts, store) {
|
|
39191
39067
|
const folders = opts.folders ?? ["inbox", "sent", "drafts"];
|
|
39192
|
-
const
|
|
39068
|
+
const budget = makeBudget(opts.maxRequests ?? Number.POSITIVE_INFINITY);
|
|
39069
|
+
budget.take();
|
|
39070
|
+
const ids = await resolveFolderIds(client2, store);
|
|
39193
39071
|
const synced = {};
|
|
39194
39072
|
let unreadInbox = [];
|
|
39073
|
+
let done = true;
|
|
39195
39074
|
for (const folder of folders) {
|
|
39196
39075
|
if (folder === "inbox") {
|
|
39197
39076
|
const r = await syncMessageFolder(client2, "inbox", ids.inbox, {
|
|
39198
39077
|
fetchUnreadBodies: opts.fetchUnreadBodies ?? false,
|
|
39199
|
-
deep: opts.deep ?? false
|
|
39200
|
-
|
|
39078
|
+
deep: opts.deep ?? false,
|
|
39079
|
+
budget
|
|
39080
|
+
}, store);
|
|
39201
39081
|
synced.inbox = r.synced;
|
|
39202
39082
|
unreadInbox = r.unread;
|
|
39083
|
+
if (!r.done) done = false;
|
|
39203
39084
|
} else if (folder === "sent") {
|
|
39204
39085
|
const r = await syncMessageFolder(client2, "sent", ids.sent, {
|
|
39205
39086
|
fetchUnreadBodies: false,
|
|
39206
|
-
deep: opts.deep ?? false
|
|
39207
|
-
|
|
39087
|
+
deep: opts.deep ?? false,
|
|
39088
|
+
budget
|
|
39089
|
+
}, store);
|
|
39208
39090
|
synced.sent = r.synced;
|
|
39091
|
+
if (!r.done) done = false;
|
|
39209
39092
|
} else if (folder === "drafts") {
|
|
39210
|
-
const r = await syncDrafts(client2, ids.drafts);
|
|
39093
|
+
const r = await syncDrafts(client2, ids.drafts, store, budget);
|
|
39211
39094
|
synced.drafts = r.synced;
|
|
39095
|
+
if (!r.done) done = false;
|
|
39212
39096
|
}
|
|
39213
39097
|
}
|
|
39214
|
-
const
|
|
39215
|
-
|
|
39098
|
+
const notes = [];
|
|
39099
|
+
if (unreadInbox.length > 0) {
|
|
39100
|
+
notes.push(`${unreadInbox.length} unread inbox messages cached without bodies. Call ofw_get_message(id) to read them \u2014 this will mark them as read on OFW.`);
|
|
39101
|
+
}
|
|
39102
|
+
if (!done) {
|
|
39103
|
+
notes.push("Paused after the request budget to stay within the hosting limit; more pages remain \u2014 call ofw_sync_messages again with the same arguments to resume where it left off and continue the backfill.");
|
|
39104
|
+
}
|
|
39105
|
+
const note = notes.length > 0 ? notes.join("\n\n") : void 0;
|
|
39106
|
+
return { synced, unreadInbox, done, ...note ? { note } : {} };
|
|
39216
39107
|
}
|
|
39217
39108
|
|
|
39218
|
-
// src/
|
|
39219
|
-
import {
|
|
39220
|
-
import {
|
|
39221
|
-
|
|
39222
|
-
|
|
39223
|
-
|
|
39224
|
-
|
|
39225
|
-
|
|
39226
|
-
|
|
39227
|
-
|
|
39228
|
-
|
|
39229
|
-
|
|
39230
|
-
|
|
39231
|
-
|
|
39232
|
-
|
|
39233
|
-
|
|
39109
|
+
// src/config.ts
|
|
39110
|
+
import { createHash } from "node:crypto";
|
|
39111
|
+
import { homedir as homedir3 } from "node:os";
|
|
39112
|
+
import { join as join4 } from "node:path";
|
|
39113
|
+
function readCacheIdentity() {
|
|
39114
|
+
return readEnvVar("OFW_CACHE_IDENTITY") ?? readEnvVar("OFW_USERNAME") ?? "_default";
|
|
39115
|
+
}
|
|
39116
|
+
function getCacheDir() {
|
|
39117
|
+
const override = process.env.OFW_CACHE_DIR;
|
|
39118
|
+
if (override && override.trim().length > 0) return override.trim();
|
|
39119
|
+
return join4(homedir3(), ".cache", "ofw-mcp");
|
|
39120
|
+
}
|
|
39121
|
+
function getCacheDbPath() {
|
|
39122
|
+
const identity = readCacheIdentity();
|
|
39123
|
+
const hash2 = createHash("sha256").update(identity).digest("hex").slice(0, 16);
|
|
39124
|
+
return join4(getCacheDir(), `${hash2}.db`);
|
|
39125
|
+
}
|
|
39126
|
+
function getAttachmentsDir() {
|
|
39127
|
+
const override = process.env.OFW_ATTACHMENTS_DIR;
|
|
39128
|
+
if (override && override.trim().length > 0) return override.trim();
|
|
39129
|
+
return join4(homedir3(), "Downloads", "ofw-mcp");
|
|
39130
|
+
}
|
|
39131
|
+
function getWriteMode() {
|
|
39132
|
+
const raw = process.env.OFW_WRITE_MODE;
|
|
39133
|
+
if (typeof raw !== "string" || raw.trim().length === 0) return "all";
|
|
39134
|
+
const mode = raw.trim().toLowerCase();
|
|
39135
|
+
if (mode === "none" || mode === "drafts" || mode === "all") return mode;
|
|
39136
|
+
console.error(
|
|
39137
|
+
`[ofw-mcp] Unrecognized OFW_WRITE_MODE "${raw.trim()}" \u2014 failing closed to "none" (no write tools registered). Valid values: none, drafts, all.`
|
|
39138
|
+
);
|
|
39139
|
+
return "none";
|
|
39140
|
+
}
|
|
39141
|
+
function getCalendarWritesAllowed() {
|
|
39142
|
+
const mode = getWriteMode();
|
|
39143
|
+
if (mode === "all") return true;
|
|
39144
|
+
return mode === "drafts" && parseBoolEnv("OFW_CALENDAR_WRITES");
|
|
39145
|
+
}
|
|
39146
|
+
function getDefaultInlineAttachments() {
|
|
39147
|
+
return parseBoolEnv("OFW_INLINE_ATTACHMENTS");
|
|
39148
|
+
}
|
|
39149
|
+
function getSyncMaxRequests() {
|
|
39150
|
+
const raw = readEnvVar("OFW_SYNC_MAX_REQUESTS");
|
|
39151
|
+
if (raw === void 0) return Number.POSITIVE_INFINITY;
|
|
39152
|
+
const n = Number(raw);
|
|
39153
|
+
if (!Number.isInteger(n) || n <= 0) return Number.POSITIVE_INFINITY;
|
|
39154
|
+
return n;
|
|
39155
|
+
}
|
|
39156
|
+
|
|
39157
|
+
// src/tools/messages.ts
|
|
39158
|
+
import { basename, join as join5 } from "node:path";
|
|
39159
|
+
var DateSchema = external_exports.looseObject({ dateTime: external_exports.string() });
|
|
39160
|
+
var SentDetailSchema = external_exports.looseObject({
|
|
39161
|
+
subject: external_exports.string().optional(),
|
|
39162
|
+
body: external_exports.string().optional(),
|
|
39163
|
+
date: DateSchema.optional(),
|
|
39164
|
+
from: external_exports.looseObject({ name: external_exports.string().optional() }).optional(),
|
|
39165
|
+
recipients: external_exports.array(ApiRecipientSchema).optional()
|
|
39166
|
+
});
|
|
39167
|
+
var SavedDraftDetailSchema = external_exports.looseObject({
|
|
39168
|
+
subject: external_exports.string().optional(),
|
|
39169
|
+
body: external_exports.string().optional(),
|
|
39170
|
+
date: DateSchema.optional(),
|
|
39171
|
+
replyToId: external_exports.number().nullable().optional(),
|
|
39234
39172
|
recipients: external_exports.array(ApiRecipientSchema).optional()
|
|
39235
39173
|
});
|
|
39236
39174
|
var MessageDetailSchema = external_exports.looseObject({
|
|
@@ -39240,7 +39178,11 @@ var MessageDetailSchema = external_exports.looseObject({
|
|
|
39240
39178
|
date: DateSchema,
|
|
39241
39179
|
from: external_exports.looseObject({ name: external_exports.string().optional() }).optional(),
|
|
39242
39180
|
files: external_exports.array(external_exports.number()).optional(),
|
|
39243
|
-
recipients: external_exports.array(ApiRecipientSchema).optional()
|
|
39181
|
+
recipients: external_exports.array(ApiRecipientSchema).optional(),
|
|
39182
|
+
// The detail payload carries its own owning folder ({id, name}). We read the
|
|
39183
|
+
// id to label a live-fetched message sent-vs-inbox instead of blindly
|
|
39184
|
+
// defaulting to inbox — see the folder derivation in ofw_get_message.
|
|
39185
|
+
folder: external_exports.looseObject({ id: external_exports.number() }).optional()
|
|
39244
39186
|
});
|
|
39245
39187
|
var DetailFilesSchema = external_exports.looseObject({ files: external_exports.array(external_exports.number()).optional() });
|
|
39246
39188
|
var UploadedFileSchema = external_exports.looseObject({
|
|
@@ -39251,33 +39193,6 @@ var UploadedFileSchema = external_exports.looseObject({
|
|
|
39251
39193
|
sizeInBytes: external_exports.number().optional(),
|
|
39252
39194
|
shareClass: external_exports.string().optional()
|
|
39253
39195
|
});
|
|
39254
|
-
var MIME_BY_EXT = {
|
|
39255
|
-
".pdf": "application/pdf",
|
|
39256
|
-
".png": "image/png",
|
|
39257
|
-
".jpg": "image/jpeg",
|
|
39258
|
-
".jpeg": "image/jpeg",
|
|
39259
|
-
".gif": "image/gif",
|
|
39260
|
-
".webp": "image/webp",
|
|
39261
|
-
".heic": "image/heic",
|
|
39262
|
-
".txt": "text/plain",
|
|
39263
|
-
".md": "text/markdown",
|
|
39264
|
-
".csv": "text/csv",
|
|
39265
|
-
".html": "text/html",
|
|
39266
|
-
".htm": "text/html",
|
|
39267
|
-
".json": "application/json",
|
|
39268
|
-
".xml": "application/xml",
|
|
39269
|
-
".doc": "application/msword",
|
|
39270
|
-
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
39271
|
-
".xls": "application/vnd.ms-excel",
|
|
39272
|
-
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
39273
|
-
".ppt": "application/vnd.ms-powerpoint",
|
|
39274
|
-
".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
39275
|
-
".zip": "application/zip",
|
|
39276
|
-
".ics": "text/calendar"
|
|
39277
|
-
};
|
|
39278
|
-
function mimeFromName(name) {
|
|
39279
|
-
return MIME_BY_EXT[extname(name).toLowerCase()] ?? "application/octet-stream";
|
|
39280
|
-
}
|
|
39281
39196
|
function listDataHintsAtFiles(listData) {
|
|
39282
39197
|
if (typeof listData !== "object" || listData === null) return false;
|
|
39283
39198
|
const ld = listData;
|
|
@@ -39285,7 +39200,7 @@ function listDataHintsAtFiles(listData) {
|
|
|
39285
39200
|
if (Array.isArray(ld.files)) return ld.files.length > 0;
|
|
39286
39201
|
return false;
|
|
39287
39202
|
}
|
|
39288
|
-
function registerMessageTools(server, client2) {
|
|
39203
|
+
function registerMessageTools(server, client2, cacheProvider, attachmentIO) {
|
|
39289
39204
|
const writeMode = getWriteMode();
|
|
39290
39205
|
const allowSend = writeMode === "all";
|
|
39291
39206
|
const allowDrafts = writeMode !== "none";
|
|
@@ -39321,9 +39236,10 @@ function registerMessageTools(server, client2) {
|
|
|
39321
39236
|
note: 'folderId must be "inbox", "sent", or "both". Numeric OFW folder IDs are not supported by the cache.'
|
|
39322
39237
|
});
|
|
39323
39238
|
}
|
|
39239
|
+
const cache = cacheProvider();
|
|
39324
39240
|
const filter = { folder, since: args.since, until: args.until, q: args.q };
|
|
39325
|
-
const total = countMessages(filter);
|
|
39326
|
-
const messages = listMessages({ ...filter, page, size });
|
|
39241
|
+
const total = await cache.countMessages(filter);
|
|
39242
|
+
const messages = await cache.listMessages({ ...filter, page, size });
|
|
39327
39243
|
const payload = { messages, total, page, size };
|
|
39328
39244
|
if (total === 0) {
|
|
39329
39245
|
payload.note = "No messages match these filters. If you expected results, check ofw_sync_messages was run, or relax the filters.";
|
|
@@ -39340,7 +39256,8 @@ function registerMessageTools(server, client2) {
|
|
|
39340
39256
|
}
|
|
39341
39257
|
}, async (args) => {
|
|
39342
39258
|
const id = Number(args.messageId);
|
|
39343
|
-
const
|
|
39259
|
+
const cache = cacheProvider();
|
|
39260
|
+
const draftRow = await cache.getDraft(id);
|
|
39344
39261
|
if (draftRow !== null) {
|
|
39345
39262
|
return jsonResponse({
|
|
39346
39263
|
id: draftRow.id,
|
|
@@ -39360,10 +39277,28 @@ function registerMessageTools(server, client2) {
|
|
|
39360
39277
|
attachments: []
|
|
39361
39278
|
});
|
|
39362
39279
|
}
|
|
39363
|
-
const cached2 = getMessage(id);
|
|
39280
|
+
const cached2 = await cache.getMessage(id);
|
|
39364
39281
|
if (cached2 && cached2.body !== null) {
|
|
39365
|
-
let
|
|
39366
|
-
if (
|
|
39282
|
+
let row2 = cached2;
|
|
39283
|
+
if (cached2.folder === "sent" && !hasRealView(cached2.recipients)) {
|
|
39284
|
+
try {
|
|
39285
|
+
const detail2 = parseLenient(
|
|
39286
|
+
MessageDetailSchema,
|
|
39287
|
+
await client2.request("GET", `/pub/v3/messages/${id}`),
|
|
39288
|
+
{ label: "ofw-mcp", context: "GET /pub/v3/messages/{id} (view-status refresh)" }
|
|
39289
|
+
);
|
|
39290
|
+
const recipients = mapRecipients(detail2.recipients);
|
|
39291
|
+
row2 = {
|
|
39292
|
+
...cached2,
|
|
39293
|
+
recipients,
|
|
39294
|
+
listData: { ...cached2.listData, showNeverViewed: !hasRealView(recipients) }
|
|
39295
|
+
};
|
|
39296
|
+
await cache.upsertMessage(row2);
|
|
39297
|
+
} catch {
|
|
39298
|
+
}
|
|
39299
|
+
}
|
|
39300
|
+
let attachments2 = await cache.listAttachmentsForMessage(id);
|
|
39301
|
+
if (attachments2.length === 0 && listDataHintsAtFiles(row2.listData)) {
|
|
39367
39302
|
try {
|
|
39368
39303
|
const detail2 = parseLenient(
|
|
39369
39304
|
DetailFilesSchema,
|
|
@@ -39371,20 +39306,26 @@ function registerMessageTools(server, client2) {
|
|
|
39371
39306
|
{ label: "ofw-mcp", context: "GET /pub/v3/messages/{id} (attachment backfill)" }
|
|
39372
39307
|
);
|
|
39373
39308
|
if (Array.isArray(detail2.files) && detail2.files.length > 0) {
|
|
39374
|
-
await fetchAttachmentMetaForMessage(client2, id, detail2.files);
|
|
39375
|
-
attachments2 = listAttachmentsForMessage(id);
|
|
39309
|
+
await fetchAttachmentMetaForMessage(client2, id, detail2.files, cache);
|
|
39310
|
+
attachments2 = await cache.listAttachmentsForMessage(id);
|
|
39376
39311
|
}
|
|
39377
39312
|
} catch {
|
|
39378
39313
|
}
|
|
39379
39314
|
}
|
|
39380
|
-
return jsonResponse({ ...
|
|
39315
|
+
return jsonResponse({ ...row2, attachments: attachments2 });
|
|
39381
39316
|
}
|
|
39382
39317
|
const detail = parseLenient(
|
|
39383
39318
|
MessageDetailSchema,
|
|
39384
39319
|
await client2.request("GET", `/pub/v3/messages/${encodeURIComponent(args.messageId)}`),
|
|
39385
39320
|
{ label: "ofw-mcp", context: "GET /pub/v3/messages/{id} (ofw_get_message)" }
|
|
39386
39321
|
);
|
|
39387
|
-
|
|
39322
|
+
let folder = cached2?.folder ?? "inbox";
|
|
39323
|
+
if (!cached2) {
|
|
39324
|
+
const sentFolderId = await cache.getMeta("sent_folder_id");
|
|
39325
|
+
if (sentFolderId !== null && detail.folder?.id != null && String(detail.folder.id) === sentFolderId) {
|
|
39326
|
+
folder = "sent";
|
|
39327
|
+
}
|
|
39328
|
+
}
|
|
39388
39329
|
const row = {
|
|
39389
39330
|
id: detail.id,
|
|
39390
39331
|
folder,
|
|
@@ -39398,11 +39339,11 @@ function registerMessageTools(server, client2) {
|
|
|
39398
39339
|
chainRootId: cached2?.chainRootId ?? null,
|
|
39399
39340
|
listData: cached2?.listData ?? detail
|
|
39400
39341
|
};
|
|
39401
|
-
upsertMessage(row);
|
|
39342
|
+
await cache.upsertMessage(row);
|
|
39402
39343
|
if (Array.isArray(detail.files) && detail.files.length > 0) {
|
|
39403
|
-
await fetchAttachmentMetaForMessage(client2, detail.id, detail.files);
|
|
39344
|
+
await fetchAttachmentMetaForMessage(client2, detail.id, detail.files, cache);
|
|
39404
39345
|
}
|
|
39405
|
-
const attachments = listAttachmentsForMessage(detail.id);
|
|
39346
|
+
const attachments = await cache.listAttachmentsForMessage(detail.id);
|
|
39406
39347
|
return jsonResponse({ ...row, attachments });
|
|
39407
39348
|
});
|
|
39408
39349
|
if (allowSend) server.registerTool("ofw_send_message", {
|
|
@@ -39422,6 +39363,7 @@ function registerMessageTools(server, client2) {
|
|
|
39422
39363
|
throw new Error(`messageId (${args.messageId}) and draftId (${args.draftId}) refer to different drafts; pass only one.`);
|
|
39423
39364
|
}
|
|
39424
39365
|
const draftRef = args.messageId ?? args.draftId;
|
|
39366
|
+
const cache = cacheProvider();
|
|
39425
39367
|
let subject = args.subject;
|
|
39426
39368
|
let body = args.body;
|
|
39427
39369
|
let recipientIds = args.recipientIds;
|
|
@@ -39430,7 +39372,7 @@ function registerMessageTools(server, client2) {
|
|
|
39430
39372
|
let draftFound = false;
|
|
39431
39373
|
if (draftRef !== void 0) {
|
|
39432
39374
|
draftLookupAttempted = true;
|
|
39433
|
-
const draft = getDraft(draftRef);
|
|
39375
|
+
const draft = await cache.getDraft(draftRef);
|
|
39434
39376
|
if (draft !== null) {
|
|
39435
39377
|
draftFound = true;
|
|
39436
39378
|
subject = subject ?? draft.subject;
|
|
@@ -39459,11 +39401,11 @@ function registerMessageTools(server, client2) {
|
|
|
39459
39401
|
let chainRootId = null;
|
|
39460
39402
|
let rewriteNote = null;
|
|
39461
39403
|
if (requestedReplyTo !== null) {
|
|
39462
|
-
resolvedReplyTo = findLatestReplyTip(requestedReplyTo);
|
|
39404
|
+
resolvedReplyTo = await cache.findLatestReplyTip(requestedReplyTo);
|
|
39463
39405
|
if (resolvedReplyTo !== requestedReplyTo) {
|
|
39464
39406
|
rewriteNote = `replyToId rewritten from ${requestedReplyTo} to ${resolvedReplyTo} (later reply in same thread found in sent cache).`;
|
|
39465
39407
|
}
|
|
39466
|
-
const parent = getMessage(resolvedReplyTo);
|
|
39408
|
+
const parent = await cache.getMessage(resolvedReplyTo);
|
|
39467
39409
|
chainRootId = parent?.chainRootId ?? parent?.id ?? requestedReplyTo;
|
|
39468
39410
|
}
|
|
39469
39411
|
const myFileIDs = args.myFileIDs ?? [];
|
|
@@ -39493,10 +39435,10 @@ function registerMessageTools(server, client2) {
|
|
|
39493
39435
|
chainRootId,
|
|
39494
39436
|
listData: detail
|
|
39495
39437
|
};
|
|
39496
|
-
upsertMessage(persisted);
|
|
39438
|
+
await cache.upsertMessage(persisted);
|
|
39497
39439
|
for (const fileId of myFileIDs) {
|
|
39498
|
-
const existing = getAttachment(fileId);
|
|
39499
|
-
upsertAttachmentForMessage({
|
|
39440
|
+
const existing = await cache.getAttachment(fileId);
|
|
39441
|
+
await cache.upsertAttachmentForMessage({
|
|
39500
39442
|
fileId,
|
|
39501
39443
|
fileName: existing?.fileName ?? `file-${fileId}`,
|
|
39502
39444
|
label: existing?.label ?? existing?.fileName ?? `file-${fileId}`,
|
|
@@ -39513,7 +39455,7 @@ function registerMessageTools(server, client2) {
|
|
|
39513
39455
|
unconfirmedNote = `WARNING: OFW's send response did not include a message id, so the send could not be confirmed. ${draftClause} ourfamilywizard.com to see whether the message went out before retrying.`;
|
|
39514
39456
|
} else if (draftRef !== void 0) {
|
|
39515
39457
|
await deleteOFWMessages(client2, [draftRef]);
|
|
39516
|
-
deleteDraft(draftRef);
|
|
39458
|
+
await cache.deleteDraft(draftRef);
|
|
39517
39459
|
}
|
|
39518
39460
|
const responseObj = persisted ?? raw;
|
|
39519
39461
|
const text = responseObj ? JSON.stringify(responseObj, null, 2) : "Message sent successfully.";
|
|
@@ -39532,7 +39474,7 @@ ${text}` : text);
|
|
|
39532
39474
|
}, async (args) => {
|
|
39533
39475
|
const page = args.page ?? 1;
|
|
39534
39476
|
const size = args.size ?? 50;
|
|
39535
|
-
const drafts = listDrafts({ page, size });
|
|
39477
|
+
const drafts = await cacheProvider().listDrafts({ page, size });
|
|
39536
39478
|
const payload = drafts.length === 0 ? { drafts: [], note: "Cache empty. Call ofw_sync_messages to populate." } : { drafts };
|
|
39537
39479
|
return jsonResponse(payload);
|
|
39538
39480
|
});
|
|
@@ -39548,11 +39490,12 @@ ${text}` : text);
|
|
|
39548
39490
|
myFileIDs: external_exports.array(external_exports.number()).describe("Attachment file ids (from ofw_upload_attachment)").optional()
|
|
39549
39491
|
}
|
|
39550
39492
|
}, async (args) => {
|
|
39493
|
+
const cache = cacheProvider();
|
|
39551
39494
|
const requestedReplyTo = args.replyToId ?? null;
|
|
39552
39495
|
let resolvedReplyTo = requestedReplyTo;
|
|
39553
39496
|
let rewriteNote = null;
|
|
39554
39497
|
if (requestedReplyTo !== null) {
|
|
39555
|
-
resolvedReplyTo = findLatestReplyTip(requestedReplyTo);
|
|
39498
|
+
resolvedReplyTo = await cache.findLatestReplyTip(requestedReplyTo);
|
|
39556
39499
|
if (resolvedReplyTo !== requestedReplyTo) {
|
|
39557
39500
|
rewriteNote = `replyToId rewritten from ${requestedReplyTo} to ${resolvedReplyTo} (later reply in same thread found in sent cache).`;
|
|
39558
39501
|
}
|
|
@@ -39587,11 +39530,11 @@ ${text}` : text);
|
|
|
39587
39530
|
modifiedAt: detail.date?.dateTime ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
39588
39531
|
listData: detail
|
|
39589
39532
|
};
|
|
39590
|
-
upsertDraft(persisted);
|
|
39533
|
+
await cache.upsertDraft(persisted);
|
|
39591
39534
|
if (args.messageId !== void 0 && args.messageId !== newId) {
|
|
39592
39535
|
try {
|
|
39593
39536
|
await deleteOFWMessages(client2, [args.messageId]);
|
|
39594
|
-
deleteDraft(args.messageId);
|
|
39537
|
+
await cache.deleteDraft(args.messageId);
|
|
39595
39538
|
replaceNote = `NOTE: ofw_save_draft replaced draft ${args.messageId} via create-then-delete. The new draft id is ${newId}; the old draft has been deleted. (OFW's update-in-place endpoint silently no-ops on subsequent updates, so we never use it. If you cached the old id anywhere, replace it with the new one.)`;
|
|
39596
39539
|
} catch (e) {
|
|
39597
39540
|
replaceNote = `WARNING: New draft ${newId} created successfully, but failed to delete the old draft (${args.messageId}): ${e.message}. You may want to clean it up manually with ofw_delete_draft.`;
|
|
@@ -39613,7 +39556,7 @@ ${text}` : text);
|
|
|
39613
39556
|
}
|
|
39614
39557
|
}, async (args) => {
|
|
39615
39558
|
const data = await deleteOFWMessages(client2, [args.messageId]);
|
|
39616
|
-
deleteDraft(args.messageId);
|
|
39559
|
+
await cacheProvider().deleteDraft(args.messageId);
|
|
39617
39560
|
return data ? jsonResponse(data) : textResponse("Draft deleted.");
|
|
39618
39561
|
});
|
|
39619
39562
|
server.registerTool("ofw_get_unread_sent", {
|
|
@@ -39626,7 +39569,7 @@ ${text}` : text);
|
|
|
39626
39569
|
}, async (args) => {
|
|
39627
39570
|
const page = args.page ?? 1;
|
|
39628
39571
|
const size = args.size ?? 50;
|
|
39629
|
-
const sent = listMessages({ folder: "sent", page, size });
|
|
39572
|
+
const sent = await cacheProvider().listMessages({ folder: "sent", page, size });
|
|
39630
39573
|
if (sent.length === 0) {
|
|
39631
39574
|
return jsonResponse({ note: "Sent cache is empty. Call ofw_sync_messages to populate." });
|
|
39632
39575
|
}
|
|
@@ -39652,13 +39595,9 @@ ${text}` : text);
|
|
|
39652
39595
|
description: external_exports.string().describe("Description shown in OFW My Files (default: filename)").optional()
|
|
39653
39596
|
}
|
|
39654
39597
|
}, async (args) => {
|
|
39655
|
-
const
|
|
39656
|
-
const stat = statSync(abs);
|
|
39657
|
-
if (!stat.isFile()) throw new Error(`Not a file: ${abs}`);
|
|
39658
|
-
const fileName = basename(abs);
|
|
39659
|
-
const mime = mimeFromName(fileName);
|
|
39598
|
+
const { blob, fileName, mimeType: mime, sizeBytes } = await attachmentIO.resolveUpload(args.path);
|
|
39660
39599
|
const form = new FormData();
|
|
39661
|
-
form.append("file",
|
|
39600
|
+
form.append("file", blob, fileName);
|
|
39662
39601
|
form.append("source", "message");
|
|
39663
39602
|
form.append("description", args.description ?? fileName);
|
|
39664
39603
|
form.append("label", args.label ?? fileName);
|
|
@@ -39669,12 +39608,12 @@ ${text}` : text);
|
|
|
39669
39608
|
await client2.request("POST", "/pub/v3/myfiles/multipart", form),
|
|
39670
39609
|
{ label: "ofw-mcp", context: "POST /pub/v3/myfiles/multipart (ofw_upload_attachment)", mode: "strict" }
|
|
39671
39610
|
);
|
|
39672
|
-
upsertAttachmentForMessage({
|
|
39611
|
+
await cacheProvider().upsertAttachmentForMessage({
|
|
39673
39612
|
fileId: meta3.fileId,
|
|
39674
39613
|
fileName: meta3.fileName ?? fileName,
|
|
39675
39614
|
label: meta3.label ?? args.label ?? fileName,
|
|
39676
39615
|
mimeType: meta3.fileType ?? mime,
|
|
39677
|
-
sizeBytes: typeof meta3.sizeInBytes === "number" ? meta3.sizeInBytes :
|
|
39616
|
+
sizeBytes: typeof meta3.sizeInBytes === "number" ? meta3.sizeInBytes : sizeBytes,
|
|
39678
39617
|
metadata: meta3,
|
|
39679
39618
|
messageId: 0
|
|
39680
39619
|
});
|
|
@@ -39682,7 +39621,7 @@ ${text}` : text);
|
|
|
39682
39621
|
fileId: meta3.fileId,
|
|
39683
39622
|
fileName: meta3.fileName ?? fileName,
|
|
39684
39623
|
mimeType: meta3.fileType ?? mime,
|
|
39685
|
-
sizeBytes: meta3.sizeInBytes ??
|
|
39624
|
+
sizeBytes: meta3.sizeInBytes ?? sizeBytes,
|
|
39686
39625
|
shareClass: meta3.shareClass ?? args.shareClass ?? "PRIVATE",
|
|
39687
39626
|
note: "Pass this fileId to ofw_send_message or ofw_save_draft in myFileIDs to attach it."
|
|
39688
39627
|
});
|
|
@@ -39698,11 +39637,12 @@ ${text}` : text);
|
|
|
39698
39637
|
}
|
|
39699
39638
|
}, async (args) => {
|
|
39700
39639
|
const fileId = args.fileId;
|
|
39640
|
+
const cache = cacheProvider();
|
|
39701
39641
|
const inline = args.inline ?? getDefaultInlineAttachments();
|
|
39702
|
-
let cached2 = getAttachment(fileId);
|
|
39642
|
+
let cached2 = await cache.getAttachment(fileId);
|
|
39703
39643
|
if (!cached2) {
|
|
39704
|
-
await fetchAttachmentMeta(client2, fileId, 0);
|
|
39705
|
-
cached2 = getAttachment(fileId);
|
|
39644
|
+
await fetchAttachmentMeta(client2, fileId, 0, cache);
|
|
39645
|
+
cached2 = await cache.getAttachment(fileId);
|
|
39706
39646
|
if (!cached2) throw new Error(`failed to fetch metadata for fileId ${fileId}`);
|
|
39707
39647
|
}
|
|
39708
39648
|
if (inline) {
|
|
@@ -39710,10 +39650,7 @@ ${text}` : text);
|
|
|
39710
39650
|
let mimeType = cached2.mimeType;
|
|
39711
39651
|
let fileName = cached2.fileName;
|
|
39712
39652
|
if (cached2.downloadedPath) {
|
|
39713
|
-
|
|
39714
|
-
bytes = readFileSync(cached2.downloadedPath);
|
|
39715
|
-
} catch {
|
|
39716
|
-
}
|
|
39653
|
+
bytes = attachmentIO.readDownloaded(cached2.downloadedPath);
|
|
39717
39654
|
}
|
|
39718
39655
|
if (bytes === null) {
|
|
39719
39656
|
const response2 = await client2.requestBinary("GET", `/pub/v1/myfiles/${fileId}/data`);
|
|
@@ -39758,9 +39695,8 @@ ${text}` : text);
|
|
|
39758
39695
|
});
|
|
39759
39696
|
}
|
|
39760
39697
|
const response = await client2.requestBinary("GET", `/pub/v1/myfiles/${fileId}/data`);
|
|
39761
|
-
|
|
39762
|
-
|
|
39763
|
-
markAttachmentDownloaded(fileId, dest);
|
|
39698
|
+
attachmentIO.writeDownload(dest, response.body);
|
|
39699
|
+
await cache.markAttachmentDownloaded(fileId, dest);
|
|
39764
39700
|
return jsonResponse({
|
|
39765
39701
|
fileId,
|
|
39766
39702
|
path: dest,
|
|
@@ -39770,19 +39706,21 @@ ${text}` : text);
|
|
|
39770
39706
|
});
|
|
39771
39707
|
});
|
|
39772
39708
|
server.registerTool("ofw_sync_messages", {
|
|
39773
|
-
description: "Sync messages from OurFamilyWizard into the local cache. Returns counts per folder and a list of unread inbox messages whose bodies were NOT fetched (to avoid mark-as-read on OFW). Call ofw_get_message(id) on those to read them. Pass deep:true to walk all OFW pages instead of stopping at the first all-cached page (use to backfill suspected gaps).",
|
|
39709
|
+
description: "Sync messages from OurFamilyWizard into the local cache. Returns counts per folder and a list of unread inbox messages whose bodies were NOT fetched (to avoid mark-as-read on OFW). Call ofw_get_message(id) on those to read them. EVERY call re-checks the newest page first, so new messages are picked up promptly even while an old-history backfill is still running; only then does it spend what is left of its budget advancing that backfill. Pass deep:true to walk all OFW pages instead of stopping at the first all-cached page (use to backfill suspected gaps). Sync is BOUNDED and RESUMABLE: on hosted deployments a per-call OFW-request budget (env OFW_SYNC_MAX_REQUESTS, or the maxRequests argument) caps how far one call walks; when the budget is hit the response reports done:false with a note \u2014 call again with the SAME arguments to resume. done:false means older history is still being backfilled; it does NOT mean recent messages are missing. Local installs are unbounded by default (done is always true).",
|
|
39774
39710
|
annotations: { readOnlyHint: false },
|
|
39775
39711
|
inputSchema: {
|
|
39776
39712
|
folders: external_exports.array(external_exports.enum(["inbox", "sent", "drafts"])).describe("Folders to sync (default: all three)").optional(),
|
|
39777
39713
|
fetchUnreadBodies: external_exports.boolean().describe("If true, also fetch bodies for unread inbox messages (will mark them as read on OFW). Default false.").optional(),
|
|
39778
|
-
deep: external_exports.boolean().describe("If true, walk every OFW page until empty regardless of cache state. Use to backfill gaps. Default false.").optional()
|
|
39714
|
+
deep: external_exports.boolean().describe("If true, walk every OFW page until empty regardless of cache state. Use to backfill gaps. Default false.").optional(),
|
|
39715
|
+
maxRequests: external_exports.number().int().min(1).describe("Maximum OFW requests this single call may make before pausing. When hit, the response reports done:false \u2014 call again with the same arguments to continue. Omit to use the server default (OFW_SYNC_MAX_REQUESTS, or unbounded on local installs).").optional()
|
|
39779
39716
|
}
|
|
39780
39717
|
}, async (args) => {
|
|
39781
39718
|
const result = await syncAll(client2, {
|
|
39782
39719
|
folders: args.folders,
|
|
39783
39720
|
fetchUnreadBodies: args.fetchUnreadBodies,
|
|
39784
|
-
deep: args.deep
|
|
39785
|
-
|
|
39721
|
+
deep: args.deep,
|
|
39722
|
+
maxRequests: args.maxRequests ?? getSyncMaxRequests()
|
|
39723
|
+
}, cacheProvider());
|
|
39786
39724
|
return jsonResponse(result);
|
|
39787
39725
|
});
|
|
39788
39726
|
}
|
|
@@ -39793,8 +39731,87 @@ async function deleteOFWMessages(client2, ids) {
|
|
|
39793
39731
|
}
|
|
39794
39732
|
|
|
39795
39733
|
// src/tools/calendar.ts
|
|
39734
|
+
var ofwDate = external_exports.looseObject({ dateTime: external_exports.string() });
|
|
39735
|
+
var userRef = external_exports.looseObject({ userId: external_exports.number() });
|
|
39736
|
+
var eventDetailSchema = external_exports.looseObject({
|
|
39737
|
+
eventRecurrenceId: external_exports.number(),
|
|
39738
|
+
title: external_exports.string(),
|
|
39739
|
+
allDay: external_exports.boolean(),
|
|
39740
|
+
publicFlag: external_exports.boolean(),
|
|
39741
|
+
startDate: ofwDate,
|
|
39742
|
+
endDate: ofwDate,
|
|
39743
|
+
location: external_exports.string().nullish(),
|
|
39744
|
+
notes: external_exports.string().nullish(),
|
|
39745
|
+
reminderMinutes: external_exports.number().nullish(),
|
|
39746
|
+
children: external_exports.array(userRef).nullish(),
|
|
39747
|
+
eventParent: userRef.nullish(),
|
|
39748
|
+
dropOffParent: userRef.nullish(),
|
|
39749
|
+
pickUpParent: userRef.nullish()
|
|
39750
|
+
});
|
|
39751
|
+
var eventWriteFields = {
|
|
39752
|
+
startDate: external_exports.string().describe("Start date YYYY-MM-DD"),
|
|
39753
|
+
endDate: external_exports.string().describe("End date YYYY-MM-DD (default: startDate)").optional(),
|
|
39754
|
+
startTime: external_exports.string().describe("Start time HH:mm, 24-hour (required unless allDay)").optional(),
|
|
39755
|
+
endTime: external_exports.string().describe("End time HH:mm, 24-hour (required unless allDay)").optional(),
|
|
39756
|
+
allDay: external_exports.boolean().optional(),
|
|
39757
|
+
privateEvent: external_exports.boolean().describe("true = visible only to you; default false = shared with co-parent").optional(),
|
|
39758
|
+
location: external_exports.string().optional(),
|
|
39759
|
+
notes: external_exports.string().optional(),
|
|
39760
|
+
reminderMinutes: external_exports.number().int().min(0).optional(),
|
|
39761
|
+
children: external_exports.array(external_exports.number()).describe("Child userIds to tag (see ofw_get_profile)").optional(),
|
|
39762
|
+
eventParentId: external_exports.number().describe("userId of the parent the event is 'for'").optional(),
|
|
39763
|
+
dropOffParentId: external_exports.number().describe("userId of the drop-off parent").optional(),
|
|
39764
|
+
pickUpParentId: external_exports.number().describe("userId of the pick-up parent").optional()
|
|
39765
|
+
};
|
|
39766
|
+
function buildEventPayload(a) {
|
|
39767
|
+
const allDay = a.allDay ?? false;
|
|
39768
|
+
if (!allDay && (!a.startTime || !a.endTime)) {
|
|
39769
|
+
throw new Error("startTime and endTime (HH:mm) are required unless allDay is true");
|
|
39770
|
+
}
|
|
39771
|
+
const payload = {
|
|
39772
|
+
title: a.title,
|
|
39773
|
+
startDate: a.startDate,
|
|
39774
|
+
endDate: a.endDate ?? a.startDate,
|
|
39775
|
+
// The web form always sends times; for all-day events it uses 01:00/02:00
|
|
39776
|
+
// placeholders that OFW ignores.
|
|
39777
|
+
startTime: a.startTime ?? "01:00",
|
|
39778
|
+
endTime: a.endTime ?? "02:00",
|
|
39779
|
+
allDay,
|
|
39780
|
+
publicFlag: !(a.privateEvent ?? false)
|
|
39781
|
+
};
|
|
39782
|
+
if (a.location) payload.location = a.location;
|
|
39783
|
+
if (a.notes) payload.notes = a.notes;
|
|
39784
|
+
if (a.reminderMinutes !== void 0) payload.reminderMinutes = String(a.reminderMinutes);
|
|
39785
|
+
if (a.children !== void 0) payload.children = a.children;
|
|
39786
|
+
if (a.eventParentId !== void 0) payload.eventParentId = String(a.eventParentId);
|
|
39787
|
+
if (a.dropOffParentId !== void 0) payload.dropOffParentId = String(a.dropOffParentId);
|
|
39788
|
+
if (a.pickUpParentId !== void 0) payload.pickUpParentId = String(a.pickUpParentId);
|
|
39789
|
+
return payload;
|
|
39790
|
+
}
|
|
39791
|
+
function detailToWriteArgs(d) {
|
|
39792
|
+
const [startDate, startClock] = d.startDate.dateTime.split("T");
|
|
39793
|
+
const [endDate, endClock] = d.endDate.dateTime.split("T");
|
|
39794
|
+
return {
|
|
39795
|
+
title: d.title,
|
|
39796
|
+
startDate,
|
|
39797
|
+
endDate,
|
|
39798
|
+
startTime: (startClock ?? "01:00:00").slice(0, 5),
|
|
39799
|
+
endTime: (endClock ?? "02:00:00").slice(0, 5),
|
|
39800
|
+
allDay: d.allDay,
|
|
39801
|
+
privateEvent: !d.publicFlag,
|
|
39802
|
+
location: d.location ?? void 0,
|
|
39803
|
+
notes: d.notes ?? void 0,
|
|
39804
|
+
reminderMinutes: d.reminderMinutes ?? void 0,
|
|
39805
|
+
// Untagged (nullish or empty) → undefined, so the merged PUT omits the
|
|
39806
|
+
// field (omission preserves; only a CALLER-supplied [] should clear).
|
|
39807
|
+
children: d.children?.length ? d.children.map((c) => c.userId) : void 0,
|
|
39808
|
+
eventParentId: d.eventParent?.userId,
|
|
39809
|
+
dropOffParentId: d.dropOffParent?.userId,
|
|
39810
|
+
pickUpParentId: d.pickUpParent?.userId
|
|
39811
|
+
};
|
|
39812
|
+
}
|
|
39796
39813
|
function registerCalendarTools(server, client2) {
|
|
39797
|
-
const allowWrites =
|
|
39814
|
+
const allowWrites = getCalendarWritesAllowed();
|
|
39798
39815
|
server.registerTool("ofw_list_events", {
|
|
39799
39816
|
description: "List OurFamilyWizard calendar events in a date range",
|
|
39800
39817
|
annotations: { readOnlyHint: true },
|
|
@@ -39812,51 +39829,62 @@ function registerCalendarTools(server, client2) {
|
|
|
39812
39829
|
return jsonResponse(data);
|
|
39813
39830
|
});
|
|
39814
39831
|
if (allowWrites) server.registerTool("ofw_create_event", {
|
|
39815
|
-
description: "Create a calendar event in OurFamilyWizard",
|
|
39832
|
+
description: "Create a calendar event in OurFamilyWizard. Unless privateEvent is true, the event is immediately visible to the co-parent \u2014 there is no draft stage.",
|
|
39816
39833
|
annotations: { destructiveHint: false },
|
|
39817
39834
|
inputSchema: {
|
|
39818
39835
|
title: external_exports.string(),
|
|
39819
|
-
|
|
39820
|
-
endDate: external_exports.string().describe("ISO datetime string"),
|
|
39821
|
-
allDay: external_exports.boolean().optional(),
|
|
39822
|
-
location: external_exports.string().optional(),
|
|
39823
|
-
reminder: external_exports.string().describe('Reminder setting (e.g. "1 hour before")').optional(),
|
|
39824
|
-
privateEvent: external_exports.boolean().optional(),
|
|
39825
|
-
eventFor: external_exports.string().describe("neither | parent1 | parent2").optional(),
|
|
39826
|
-
dropOffParent: external_exports.string().optional(),
|
|
39827
|
-
pickUpParent: external_exports.string().optional(),
|
|
39828
|
-
children: external_exports.array(external_exports.number()).describe("Array of child IDs").optional()
|
|
39836
|
+
...eventWriteFields
|
|
39829
39837
|
}
|
|
39830
39838
|
}, async (args) => {
|
|
39831
|
-
const
|
|
39832
|
-
|
|
39839
|
+
const raw = await client2.request("POST", "/pub/v3/events", buildEventPayload(args));
|
|
39840
|
+
const event = parseLenient(eventDetailSchema, raw, { label: "ofw-mcp", context: "POST /pub/v3/events", mode: "strict" });
|
|
39841
|
+
return jsonResponse({
|
|
39842
|
+
note: `Event created. Use eventRecurrenceId ${event.eventRecurrenceId} as eventId for ofw_update_event/ofw_delete_event.`,
|
|
39843
|
+
event
|
|
39844
|
+
});
|
|
39833
39845
|
});
|
|
39834
39846
|
if (allowWrites) server.registerTool("ofw_update_event", {
|
|
39835
|
-
description: "Update an existing OurFamilyWizard calendar event",
|
|
39847
|
+
description: "Update an existing OurFamilyWizard calendar event. Fetches the event, applies the given changes, and writes the merged result back (OFW has no partial update).",
|
|
39836
39848
|
annotations: { destructiveHint: true },
|
|
39837
39849
|
inputSchema: {
|
|
39838
|
-
eventId: external_exports.string(),
|
|
39850
|
+
eventId: external_exports.string().describe("Event id \u2014 the `id` from ofw_list_events / eventRecurrenceId from ofw_create_event"),
|
|
39839
39851
|
title: external_exports.string().optional(),
|
|
39840
|
-
startDate:
|
|
39841
|
-
endDate:
|
|
39842
|
-
|
|
39843
|
-
|
|
39844
|
-
|
|
39845
|
-
privateEvent:
|
|
39852
|
+
startDate: eventWriteFields.startDate.optional(),
|
|
39853
|
+
endDate: eventWriteFields.endDate,
|
|
39854
|
+
startTime: eventWriteFields.startTime,
|
|
39855
|
+
endTime: eventWriteFields.endTime,
|
|
39856
|
+
allDay: eventWriteFields.allDay,
|
|
39857
|
+
privateEvent: eventWriteFields.privateEvent,
|
|
39858
|
+
location: eventWriteFields.location,
|
|
39859
|
+
notes: eventWriteFields.notes,
|
|
39860
|
+
reminderMinutes: eventWriteFields.reminderMinutes,
|
|
39861
|
+
children: external_exports.array(external_exports.number()).describe("Child userIds to tag; pass [] to remove all child tags (omit to keep current tags)").optional(),
|
|
39862
|
+
eventParentId: eventWriteFields.eventParentId,
|
|
39863
|
+
dropOffParentId: eventWriteFields.dropOffParentId,
|
|
39864
|
+
pickUpParentId: eventWriteFields.pickUpParentId
|
|
39846
39865
|
}
|
|
39847
39866
|
}, async (args) => {
|
|
39848
|
-
const { eventId, ...
|
|
39849
|
-
const
|
|
39850
|
-
|
|
39867
|
+
const { eventId, ...changes } = args;
|
|
39868
|
+
const id = encodeURIComponent(eventId);
|
|
39869
|
+
const rawDetail = await client2.request("GET", `/pub/v3/events/${id}`);
|
|
39870
|
+
const current = parseLenient(eventDetailSchema, rawDetail, { label: "ofw-mcp", context: `GET /pub/v3/events/${eventId}`, mode: "strict" });
|
|
39871
|
+
const defined = Object.fromEntries(Object.entries(changes).filter(([, v]) => v !== void 0));
|
|
39872
|
+
const merged = { ...detailToWriteArgs(current), ...defined };
|
|
39873
|
+
await client2.request("PUT", `/pub/v3/events/${id}`, buildEventPayload(merged));
|
|
39874
|
+
const rawAfter = await client2.request("GET", `/pub/v3/events/${id}`);
|
|
39875
|
+
const event = parseLenient(eventDetailSchema, rawAfter, { label: "ofw-mcp", context: `GET /pub/v3/events/${eventId} (post-update)`, mode: "strict" });
|
|
39876
|
+
return jsonResponse({ note: "Event updated; returning re-fetched event state.", event });
|
|
39851
39877
|
});
|
|
39852
39878
|
if (allowWrites) server.registerTool("ofw_delete_event", {
|
|
39853
39879
|
description: "Delete an OurFamilyWizard calendar event",
|
|
39854
39880
|
annotations: { destructiveHint: true },
|
|
39855
39881
|
inputSchema: {
|
|
39856
|
-
eventId: external_exports.string().describe("Event
|
|
39882
|
+
eventId: external_exports.string().describe("Event id \u2014 the `id` from ofw_list_events / eventRecurrenceId from ofw_create_event"),
|
|
39883
|
+
includeFuture: external_exports.boolean().describe("For repeating events: also delete future occurrences (default false)").optional()
|
|
39857
39884
|
}
|
|
39858
39885
|
}, async (args) => {
|
|
39859
|
-
|
|
39886
|
+
const includeFuture = args.includeFuture ?? false;
|
|
39887
|
+
await client2.request("DELETE", `/pub/v3/events/${encodeURIComponent(args.eventId)}?includeFuture=${includeFuture}`);
|
|
39860
39888
|
return textResponse(`Event ${args.eventId} deleted`);
|
|
39861
39889
|
});
|
|
39862
39890
|
}
|
|
@@ -39926,6 +39954,584 @@ function registerJournalTools(server, client2) {
|
|
|
39926
39954
|
});
|
|
39927
39955
|
}
|
|
39928
39956
|
|
|
39957
|
+
// src/cache/node.ts
|
|
39958
|
+
import { DatabaseSync } from "node:sqlite";
|
|
39959
|
+
import { mkdirSync, chmodSync, existsSync } from "node:fs";
|
|
39960
|
+
import { dirname as dirname2 } from "node:path";
|
|
39961
|
+
|
|
39962
|
+
// src/cache/store.ts
|
|
39963
|
+
function rowFromDb(r) {
|
|
39964
|
+
return {
|
|
39965
|
+
id: r.id,
|
|
39966
|
+
folder: r.folder,
|
|
39967
|
+
subject: r.subject,
|
|
39968
|
+
fromUser: r.from_user,
|
|
39969
|
+
sentAt: r.sent_at,
|
|
39970
|
+
recipients: JSON.parse(r.recipients_json),
|
|
39971
|
+
body: r.body,
|
|
39972
|
+
fetchedBodyAt: r.fetched_body_at,
|
|
39973
|
+
replyToId: r.reply_to_id,
|
|
39974
|
+
chainRootId: r.chain_root_id,
|
|
39975
|
+
listData: JSON.parse(r.list_data_json)
|
|
39976
|
+
};
|
|
39977
|
+
}
|
|
39978
|
+
function draftFromDb(r) {
|
|
39979
|
+
return {
|
|
39980
|
+
id: r.id,
|
|
39981
|
+
subject: r.subject,
|
|
39982
|
+
body: r.body,
|
|
39983
|
+
recipients: JSON.parse(r.recipients_json),
|
|
39984
|
+
replyToId: r.reply_to_id,
|
|
39985
|
+
modifiedAt: r.modified_at,
|
|
39986
|
+
listData: JSON.parse(r.list_data_json)
|
|
39987
|
+
};
|
|
39988
|
+
}
|
|
39989
|
+
function attachmentFromDb(r) {
|
|
39990
|
+
return {
|
|
39991
|
+
fileId: r.file_id,
|
|
39992
|
+
fileName: r.file_name,
|
|
39993
|
+
label: r.label,
|
|
39994
|
+
mimeType: r.mime_type,
|
|
39995
|
+
sizeBytes: r.size_bytes,
|
|
39996
|
+
metadata: JSON.parse(r.metadata_json),
|
|
39997
|
+
messageIds: JSON.parse(r.message_ids_json),
|
|
39998
|
+
downloadedPath: r.downloaded_path,
|
|
39999
|
+
downloadedAt: r.downloaded_at
|
|
40000
|
+
};
|
|
40001
|
+
}
|
|
40002
|
+
function nullish3(v) {
|
|
40003
|
+
return v === void 0 ? null : v;
|
|
40004
|
+
}
|
|
40005
|
+
function requireString(field, v) {
|
|
40006
|
+
if (typeof v === "string") return v;
|
|
40007
|
+
throw new Error(`cache: ${field} is required (got ${v === void 0 ? "undefined" : "null"})`);
|
|
40008
|
+
}
|
|
40009
|
+
var SCHEMA_STATEMENTS = [
|
|
40010
|
+
`CREATE TABLE IF NOT EXISTS messages (
|
|
40011
|
+
id INTEGER PRIMARY KEY,
|
|
40012
|
+
folder TEXT NOT NULL,
|
|
40013
|
+
subject TEXT NOT NULL,
|
|
40014
|
+
from_user TEXT NOT NULL,
|
|
40015
|
+
sent_at TEXT NOT NULL,
|
|
40016
|
+
recipients_json TEXT NOT NULL,
|
|
40017
|
+
body TEXT,
|
|
40018
|
+
fetched_body_at TEXT,
|
|
40019
|
+
reply_to_id INTEGER,
|
|
40020
|
+
chain_root_id INTEGER,
|
|
40021
|
+
list_data_json TEXT NOT NULL,
|
|
40022
|
+
last_seen_at TEXT NOT NULL
|
|
40023
|
+
)`,
|
|
40024
|
+
`CREATE INDEX IF NOT EXISTS idx_messages_folder_sent_at ON messages(folder, sent_at DESC)`,
|
|
40025
|
+
`CREATE INDEX IF NOT EXISTS idx_messages_chain_root ON messages(chain_root_id)`,
|
|
40026
|
+
`CREATE TABLE IF NOT EXISTS drafts (
|
|
40027
|
+
id INTEGER PRIMARY KEY,
|
|
40028
|
+
subject TEXT NOT NULL,
|
|
40029
|
+
body TEXT NOT NULL,
|
|
40030
|
+
recipients_json TEXT NOT NULL,
|
|
40031
|
+
reply_to_id INTEGER,
|
|
40032
|
+
modified_at TEXT NOT NULL,
|
|
40033
|
+
list_data_json TEXT NOT NULL
|
|
40034
|
+
)`,
|
|
40035
|
+
`CREATE TABLE IF NOT EXISTS sync_state (
|
|
40036
|
+
folder TEXT PRIMARY KEY,
|
|
40037
|
+
last_sync_at TEXT NOT NULL,
|
|
40038
|
+
newest_id INTEGER
|
|
40039
|
+
)`,
|
|
40040
|
+
`CREATE TABLE IF NOT EXISTS meta (
|
|
40041
|
+
key TEXT PRIMARY KEY,
|
|
40042
|
+
value TEXT NOT NULL
|
|
40043
|
+
)`,
|
|
40044
|
+
// v2: attachments table. Idempotent — IF NOT EXISTS.
|
|
40045
|
+
`CREATE TABLE IF NOT EXISTS attachments (
|
|
40046
|
+
file_id INTEGER PRIMARY KEY,
|
|
40047
|
+
file_name TEXT NOT NULL,
|
|
40048
|
+
label TEXT NOT NULL,
|
|
40049
|
+
mime_type TEXT NOT NULL,
|
|
40050
|
+
size_bytes INTEGER,
|
|
40051
|
+
metadata_json TEXT NOT NULL,
|
|
40052
|
+
message_ids_json TEXT NOT NULL,
|
|
40053
|
+
downloaded_path TEXT,
|
|
40054
|
+
downloaded_at TEXT,
|
|
40055
|
+
fetched_metadata_at TEXT NOT NULL
|
|
40056
|
+
)`
|
|
40057
|
+
];
|
|
40058
|
+
var MIGRATIONS = [
|
|
40059
|
+
// Resumable deep-sync cursor. Absent/NULL → SyncState.resumePage null.
|
|
40060
|
+
"ALTER TABLE sync_state ADD COLUMN resume_page INTEGER"
|
|
40061
|
+
];
|
|
40062
|
+
var SCHEMA_VERSION = "2";
|
|
40063
|
+
function buildMessageFilter(opts) {
|
|
40064
|
+
const wheres = [];
|
|
40065
|
+
const params = [];
|
|
40066
|
+
if (opts.folder !== void 0) {
|
|
40067
|
+
wheres.push("folder = ?");
|
|
40068
|
+
params.push(opts.folder);
|
|
40069
|
+
}
|
|
40070
|
+
if (opts.since !== void 0) {
|
|
40071
|
+
wheres.push("sent_at >= ?");
|
|
40072
|
+
params.push(opts.since);
|
|
40073
|
+
}
|
|
40074
|
+
if (opts.until !== void 0) {
|
|
40075
|
+
wheres.push("sent_at < ?");
|
|
40076
|
+
params.push(opts.until);
|
|
40077
|
+
}
|
|
40078
|
+
if (opts.q !== void 0 && opts.q.length > 0) {
|
|
40079
|
+
const pattern = `%${opts.q}%`;
|
|
40080
|
+
wheres.push("(subject LIKE ? OR body LIKE ?)");
|
|
40081
|
+
params.push(pattern, pattern);
|
|
40082
|
+
}
|
|
40083
|
+
return {
|
|
40084
|
+
where: wheres.length > 0 ? `WHERE ${wheres.join(" AND ")}` : "",
|
|
40085
|
+
params
|
|
40086
|
+
};
|
|
40087
|
+
}
|
|
40088
|
+
var OFWCacheCore = class {
|
|
40089
|
+
constructor(db) {
|
|
40090
|
+
this.db = db;
|
|
40091
|
+
for (const stmt of SCHEMA_STATEMENTS) this.db.execScript(stmt);
|
|
40092
|
+
for (const stmt of MIGRATIONS) {
|
|
40093
|
+
try {
|
|
40094
|
+
this.db.execScript(stmt);
|
|
40095
|
+
} catch {
|
|
40096
|
+
}
|
|
40097
|
+
}
|
|
40098
|
+
this.db.run(
|
|
40099
|
+
"INSERT INTO meta(key, value) VALUES(?, ?) ON CONFLICT(key) DO UPDATE SET value=excluded.value",
|
|
40100
|
+
["schema_version", SCHEMA_VERSION]
|
|
40101
|
+
);
|
|
40102
|
+
}
|
|
40103
|
+
db;
|
|
40104
|
+
upsertMessage(row) {
|
|
40105
|
+
this.db.run(
|
|
40106
|
+
`INSERT INTO messages (
|
|
40107
|
+
id, folder, subject, from_user, sent_at, recipients_json,
|
|
40108
|
+
body, fetched_body_at, reply_to_id, chain_root_id, list_data_json, last_seen_at
|
|
40109
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
40110
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
40111
|
+
folder=excluded.folder,
|
|
40112
|
+
subject=excluded.subject,
|
|
40113
|
+
from_user=excluded.from_user,
|
|
40114
|
+
sent_at=excluded.sent_at,
|
|
40115
|
+
recipients_json=excluded.recipients_json,
|
|
40116
|
+
body=excluded.body,
|
|
40117
|
+
fetched_body_at=excluded.fetched_body_at,
|
|
40118
|
+
reply_to_id=excluded.reply_to_id,
|
|
40119
|
+
chain_root_id=excluded.chain_root_id,
|
|
40120
|
+
list_data_json=excluded.list_data_json,
|
|
40121
|
+
last_seen_at=excluded.last_seen_at`,
|
|
40122
|
+
[
|
|
40123
|
+
row.id,
|
|
40124
|
+
requireString("messages.folder", row.folder),
|
|
40125
|
+
requireString("messages.subject", row.subject),
|
|
40126
|
+
requireString("messages.fromUser", row.fromUser),
|
|
40127
|
+
requireString("messages.sentAt", row.sentAt),
|
|
40128
|
+
JSON.stringify(row.recipients ?? []),
|
|
40129
|
+
nullish3(row.body),
|
|
40130
|
+
nullish3(row.fetchedBodyAt),
|
|
40131
|
+
nullish3(row.replyToId),
|
|
40132
|
+
nullish3(row.chainRootId),
|
|
40133
|
+
JSON.stringify(row.listData ?? null),
|
|
40134
|
+
(/* @__PURE__ */ new Date()).toISOString()
|
|
40135
|
+
]
|
|
40136
|
+
);
|
|
40137
|
+
}
|
|
40138
|
+
/**
|
|
40139
|
+
* Batch upsert every row in a single transaction — one round-trip's worth of
|
|
40140
|
+
* work (crucial on the Durable Object backend, where each RPC is a subrequest).
|
|
40141
|
+
* Empty array is a no-op (no transaction opened).
|
|
40142
|
+
*/
|
|
40143
|
+
upsertMessages(rows) {
|
|
40144
|
+
if (rows.length === 0) return;
|
|
40145
|
+
this.db.transaction(() => {
|
|
40146
|
+
for (const row of rows) this.upsertMessage(row);
|
|
40147
|
+
});
|
|
40148
|
+
}
|
|
40149
|
+
getMessage(id) {
|
|
40150
|
+
const r = this.db.get("SELECT * FROM messages WHERE id = ?", [id]);
|
|
40151
|
+
return r ? rowFromDb(r) : null;
|
|
40152
|
+
}
|
|
40153
|
+
/**
|
|
40154
|
+
* Batch read: one `SELECT ... WHERE id IN (...)` returning the present rows
|
|
40155
|
+
* (absent ids are simply omitted — order is not guaranteed). Empty ids returns
|
|
40156
|
+
* `[]` without querying.
|
|
40157
|
+
*/
|
|
40158
|
+
getMessages(ids) {
|
|
40159
|
+
if (ids.length === 0) return [];
|
|
40160
|
+
const placeholders = ids.map(() => "?").join(", ");
|
|
40161
|
+
const rows = this.db.all(
|
|
40162
|
+
`SELECT * FROM messages WHERE id IN (${placeholders})`,
|
|
40163
|
+
ids
|
|
40164
|
+
);
|
|
40165
|
+
return rows.map(rowFromDb);
|
|
40166
|
+
}
|
|
40167
|
+
/**
|
|
40168
|
+
* Remove a row from the `messages` table. Used by syncDrafts to evict
|
|
40169
|
+
* stale rows that were cached when a draft was previously read through
|
|
40170
|
+
* `ofw_get_message` (which would have wrongly classified it as `inbox`)
|
|
40171
|
+
* — the drafts table is the authoritative source for that id now.
|
|
40172
|
+
*/
|
|
40173
|
+
deleteMessage(id) {
|
|
40174
|
+
this.db.run("DELETE FROM messages WHERE id = ?", [id]);
|
|
40175
|
+
}
|
|
40176
|
+
listMessages(opts) {
|
|
40177
|
+
const { where, params } = buildMessageFilter(opts);
|
|
40178
|
+
const offset = (opts.page - 1) * opts.size;
|
|
40179
|
+
const rows = this.db.all(
|
|
40180
|
+
`SELECT * FROM messages ${where}
|
|
40181
|
+
ORDER BY sent_at DESC, id DESC
|
|
40182
|
+
LIMIT ? OFFSET ?`,
|
|
40183
|
+
[...params, opts.size, offset]
|
|
40184
|
+
);
|
|
40185
|
+
return rows.map(rowFromDb);
|
|
40186
|
+
}
|
|
40187
|
+
countMessages(opts) {
|
|
40188
|
+
const { where, params } = buildMessageFilter(opts);
|
|
40189
|
+
const r = this.db.get(`SELECT COUNT(*) as n FROM messages ${where}`, params);
|
|
40190
|
+
return r?.n ?? 0;
|
|
40191
|
+
}
|
|
40192
|
+
upsertDraft(row) {
|
|
40193
|
+
this.db.run(
|
|
40194
|
+
`INSERT INTO drafts (id, subject, body, recipients_json, reply_to_id, modified_at, list_data_json)
|
|
40195
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
40196
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
40197
|
+
subject=excluded.subject,
|
|
40198
|
+
body=excluded.body,
|
|
40199
|
+
recipients_json=excluded.recipients_json,
|
|
40200
|
+
reply_to_id=excluded.reply_to_id,
|
|
40201
|
+
modified_at=excluded.modified_at,
|
|
40202
|
+
list_data_json=excluded.list_data_json`,
|
|
40203
|
+
[
|
|
40204
|
+
row.id,
|
|
40205
|
+
requireString("drafts.subject", row.subject),
|
|
40206
|
+
requireString("drafts.body", row.body),
|
|
40207
|
+
JSON.stringify(row.recipients ?? []),
|
|
40208
|
+
nullish3(row.replyToId),
|
|
40209
|
+
requireString("drafts.modifiedAt", row.modifiedAt),
|
|
40210
|
+
JSON.stringify(row.listData ?? null)
|
|
40211
|
+
]
|
|
40212
|
+
);
|
|
40213
|
+
}
|
|
40214
|
+
/** Batch upsert every draft in a single transaction. Empty array is a no-op. */
|
|
40215
|
+
upsertDrafts(rows) {
|
|
40216
|
+
if (rows.length === 0) return;
|
|
40217
|
+
this.db.transaction(() => {
|
|
40218
|
+
for (const row of rows) this.upsertDraft(row);
|
|
40219
|
+
});
|
|
40220
|
+
}
|
|
40221
|
+
getDraft(id) {
|
|
40222
|
+
const r = this.db.get("SELECT * FROM drafts WHERE id = ?", [id]);
|
|
40223
|
+
return r ? draftFromDb(r) : null;
|
|
40224
|
+
}
|
|
40225
|
+
/**
|
|
40226
|
+
* Batch read: one `SELECT ... WHERE id IN (...)` returning the present drafts
|
|
40227
|
+
* (absent ids omitted — order not guaranteed). Empty ids returns `[]` without
|
|
40228
|
+
* querying.
|
|
40229
|
+
*/
|
|
40230
|
+
getDrafts(ids) {
|
|
40231
|
+
if (ids.length === 0) return [];
|
|
40232
|
+
const placeholders = ids.map(() => "?").join(", ");
|
|
40233
|
+
const rows = this.db.all(
|
|
40234
|
+
`SELECT * FROM drafts WHERE id IN (${placeholders})`,
|
|
40235
|
+
ids
|
|
40236
|
+
);
|
|
40237
|
+
return rows.map(draftFromDb);
|
|
40238
|
+
}
|
|
40239
|
+
listDrafts(opts) {
|
|
40240
|
+
const offset = (opts.page - 1) * opts.size;
|
|
40241
|
+
const rows = this.db.all(
|
|
40242
|
+
"SELECT * FROM drafts ORDER BY modified_at DESC, id DESC LIMIT ? OFFSET ?",
|
|
40243
|
+
[opts.size, offset]
|
|
40244
|
+
);
|
|
40245
|
+
return rows.map(draftFromDb);
|
|
40246
|
+
}
|
|
40247
|
+
deleteDraft(id) {
|
|
40248
|
+
this.db.run("DELETE FROM drafts WHERE id = ?", [id]);
|
|
40249
|
+
}
|
|
40250
|
+
listDraftIds() {
|
|
40251
|
+
const rows = this.db.all("SELECT id FROM drafts", []);
|
|
40252
|
+
return rows.map((r) => r.id);
|
|
40253
|
+
}
|
|
40254
|
+
getSyncState(folder) {
|
|
40255
|
+
const r = this.db.get("SELECT last_sync_at, newest_id, resume_page FROM sync_state WHERE folder = ?", [folder]);
|
|
40256
|
+
if (!r) return null;
|
|
40257
|
+
return { lastSyncAt: r.last_sync_at, newestId: r.newest_id, resumePage: r.resume_page ?? null };
|
|
40258
|
+
}
|
|
40259
|
+
setSyncState(folder, state) {
|
|
40260
|
+
this.db.run(
|
|
40261
|
+
`INSERT INTO sync_state (folder, last_sync_at, newest_id, resume_page) VALUES (?, ?, ?, ?)
|
|
40262
|
+
ON CONFLICT(folder) DO UPDATE SET
|
|
40263
|
+
last_sync_at = excluded.last_sync_at,
|
|
40264
|
+
newest_id = excluded.newest_id,
|
|
40265
|
+
resume_page = excluded.resume_page`,
|
|
40266
|
+
[folder, state.lastSyncAt, nullish3(state.newestId), nullish3(state.resumePage)]
|
|
40267
|
+
);
|
|
40268
|
+
}
|
|
40269
|
+
getMeta(key) {
|
|
40270
|
+
const r = this.db.get("SELECT value FROM meta WHERE key = ?", [key]);
|
|
40271
|
+
return r ? r.value : null;
|
|
40272
|
+
}
|
|
40273
|
+
setMeta(key, value) {
|
|
40274
|
+
this.db.run(
|
|
40275
|
+
"INSERT INTO meta (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value=excluded.value",
|
|
40276
|
+
[key, value]
|
|
40277
|
+
);
|
|
40278
|
+
}
|
|
40279
|
+
findLatestReplyTip(replyToId) {
|
|
40280
|
+
const parent = this.db.get("SELECT id, folder, chain_root_id FROM messages WHERE id = ?", [replyToId]);
|
|
40281
|
+
if (!parent) return replyToId;
|
|
40282
|
+
const chainRoot = parent.chain_root_id ?? parent.id;
|
|
40283
|
+
const tip = this.db.get(
|
|
40284
|
+
`SELECT id FROM messages
|
|
40285
|
+
WHERE folder = 'sent' AND chain_root_id = ?
|
|
40286
|
+
ORDER BY id DESC LIMIT 1`,
|
|
40287
|
+
[chainRoot]
|
|
40288
|
+
);
|
|
40289
|
+
return tip ? tip.id : replyToId;
|
|
40290
|
+
}
|
|
40291
|
+
getAttachment(fileId) {
|
|
40292
|
+
const r = this.db.get("SELECT * FROM attachments WHERE file_id = ?", [fileId]);
|
|
40293
|
+
return r ? attachmentFromDb(r) : null;
|
|
40294
|
+
}
|
|
40295
|
+
listAttachmentsForMessage(messageId) {
|
|
40296
|
+
const rows = this.db.all(
|
|
40297
|
+
`SELECT * FROM attachments
|
|
40298
|
+
WHERE EXISTS (SELECT 1 FROM json_each(message_ids_json) WHERE value = ?)
|
|
40299
|
+
ORDER BY file_id`,
|
|
40300
|
+
[messageId]
|
|
40301
|
+
);
|
|
40302
|
+
return rows.map(attachmentFromDb);
|
|
40303
|
+
}
|
|
40304
|
+
upsertAttachmentForMessage(input) {
|
|
40305
|
+
const existing = this.db.get("SELECT message_ids_json FROM attachments WHERE file_id = ?", [input.fileId]);
|
|
40306
|
+
const prior = existing ? JSON.parse(existing.message_ids_json) : [];
|
|
40307
|
+
let messageIds;
|
|
40308
|
+
if (input.messageId === 0) {
|
|
40309
|
+
messageIds = prior;
|
|
40310
|
+
} else if (prior.includes(input.messageId)) {
|
|
40311
|
+
messageIds = prior;
|
|
40312
|
+
} else {
|
|
40313
|
+
messageIds = [...prior, input.messageId];
|
|
40314
|
+
}
|
|
40315
|
+
this.db.run(
|
|
40316
|
+
`INSERT INTO attachments (
|
|
40317
|
+
file_id, file_name, label, mime_type, size_bytes,
|
|
40318
|
+
metadata_json, message_ids_json, fetched_metadata_at
|
|
40319
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
40320
|
+
ON CONFLICT(file_id) DO UPDATE SET
|
|
40321
|
+
file_name=excluded.file_name,
|
|
40322
|
+
label=excluded.label,
|
|
40323
|
+
mime_type=excluded.mime_type,
|
|
40324
|
+
size_bytes=excluded.size_bytes,
|
|
40325
|
+
metadata_json=excluded.metadata_json,
|
|
40326
|
+
message_ids_json=excluded.message_ids_json,
|
|
40327
|
+
fetched_metadata_at=excluded.fetched_metadata_at`,
|
|
40328
|
+
[
|
|
40329
|
+
input.fileId,
|
|
40330
|
+
requireString("attachments.fileName", input.fileName),
|
|
40331
|
+
requireString("attachments.label", input.label),
|
|
40332
|
+
requireString("attachments.mimeType", input.mimeType),
|
|
40333
|
+
nullish3(input.sizeBytes),
|
|
40334
|
+
JSON.stringify(input.metadata ?? null),
|
|
40335
|
+
JSON.stringify(messageIds),
|
|
40336
|
+
(/* @__PURE__ */ new Date()).toISOString()
|
|
40337
|
+
]
|
|
40338
|
+
);
|
|
40339
|
+
}
|
|
40340
|
+
markAttachmentDownloaded(fileId, path) {
|
|
40341
|
+
this.db.run("UPDATE attachments SET downloaded_path = ?, downloaded_at = ? WHERE file_id = ?", [
|
|
40342
|
+
path,
|
|
40343
|
+
(/* @__PURE__ */ new Date()).toISOString(),
|
|
40344
|
+
fileId
|
|
40345
|
+
]);
|
|
40346
|
+
}
|
|
40347
|
+
};
|
|
40348
|
+
var LocalCacheStore = class {
|
|
40349
|
+
constructor(core) {
|
|
40350
|
+
this.core = core;
|
|
40351
|
+
}
|
|
40352
|
+
core;
|
|
40353
|
+
async upsertMessage(row) {
|
|
40354
|
+
this.core.upsertMessage(row);
|
|
40355
|
+
}
|
|
40356
|
+
async upsertMessages(rows) {
|
|
40357
|
+
this.core.upsertMessages(rows);
|
|
40358
|
+
}
|
|
40359
|
+
async getMessage(id) {
|
|
40360
|
+
return this.core.getMessage(id);
|
|
40361
|
+
}
|
|
40362
|
+
async getMessages(ids) {
|
|
40363
|
+
return this.core.getMessages(ids);
|
|
40364
|
+
}
|
|
40365
|
+
async deleteMessage(id) {
|
|
40366
|
+
this.core.deleteMessage(id);
|
|
40367
|
+
}
|
|
40368
|
+
async listMessages(opts) {
|
|
40369
|
+
return this.core.listMessages(opts);
|
|
40370
|
+
}
|
|
40371
|
+
async countMessages(opts) {
|
|
40372
|
+
return this.core.countMessages(opts);
|
|
40373
|
+
}
|
|
40374
|
+
async upsertDraft(row) {
|
|
40375
|
+
this.core.upsertDraft(row);
|
|
40376
|
+
}
|
|
40377
|
+
async upsertDrafts(rows) {
|
|
40378
|
+
this.core.upsertDrafts(rows);
|
|
40379
|
+
}
|
|
40380
|
+
async getDraft(id) {
|
|
40381
|
+
return this.core.getDraft(id);
|
|
40382
|
+
}
|
|
40383
|
+
async getDrafts(ids) {
|
|
40384
|
+
return this.core.getDrafts(ids);
|
|
40385
|
+
}
|
|
40386
|
+
async listDrafts(opts) {
|
|
40387
|
+
return this.core.listDrafts(opts);
|
|
40388
|
+
}
|
|
40389
|
+
async deleteDraft(id) {
|
|
40390
|
+
this.core.deleteDraft(id);
|
|
40391
|
+
}
|
|
40392
|
+
async listDraftIds() {
|
|
40393
|
+
return this.core.listDraftIds();
|
|
40394
|
+
}
|
|
40395
|
+
async getSyncState(folder) {
|
|
40396
|
+
return this.core.getSyncState(folder);
|
|
40397
|
+
}
|
|
40398
|
+
async setSyncState(folder, state) {
|
|
40399
|
+
this.core.setSyncState(folder, state);
|
|
40400
|
+
}
|
|
40401
|
+
async getMeta(key) {
|
|
40402
|
+
return this.core.getMeta(key);
|
|
40403
|
+
}
|
|
40404
|
+
async setMeta(key, value) {
|
|
40405
|
+
this.core.setMeta(key, value);
|
|
40406
|
+
}
|
|
40407
|
+
async findLatestReplyTip(replyToId) {
|
|
40408
|
+
return this.core.findLatestReplyTip(replyToId);
|
|
40409
|
+
}
|
|
40410
|
+
async getAttachment(fileId) {
|
|
40411
|
+
return this.core.getAttachment(fileId);
|
|
40412
|
+
}
|
|
40413
|
+
async listAttachmentsForMessage(messageId) {
|
|
40414
|
+
return this.core.listAttachmentsForMessage(messageId);
|
|
40415
|
+
}
|
|
40416
|
+
async upsertAttachmentForMessage(input) {
|
|
40417
|
+
this.core.upsertAttachmentForMessage(input);
|
|
40418
|
+
}
|
|
40419
|
+
async markAttachmentDownloaded(fileId, path) {
|
|
40420
|
+
this.core.markAttachmentDownloaded(fileId, path);
|
|
40421
|
+
}
|
|
40422
|
+
};
|
|
40423
|
+
|
|
40424
|
+
// src/cache/node.ts
|
|
40425
|
+
var NodeSqlDriver = class {
|
|
40426
|
+
constructor(db) {
|
|
40427
|
+
this.db = db;
|
|
40428
|
+
}
|
|
40429
|
+
db;
|
|
40430
|
+
execScript(sql) {
|
|
40431
|
+
this.db.exec(sql);
|
|
40432
|
+
}
|
|
40433
|
+
run(sql, params) {
|
|
40434
|
+
this.db.prepare(sql).run(...params);
|
|
40435
|
+
}
|
|
40436
|
+
get(sql, params) {
|
|
40437
|
+
return this.db.prepare(sql).get(...params);
|
|
40438
|
+
}
|
|
40439
|
+
all(sql, params) {
|
|
40440
|
+
return this.db.prepare(sql).all(...params);
|
|
40441
|
+
}
|
|
40442
|
+
transaction(fn) {
|
|
40443
|
+
this.db.exec("BEGIN");
|
|
40444
|
+
try {
|
|
40445
|
+
fn();
|
|
40446
|
+
this.db.exec("COMMIT");
|
|
40447
|
+
} catch (e) {
|
|
40448
|
+
this.db.exec("ROLLBACK");
|
|
40449
|
+
throw e;
|
|
40450
|
+
}
|
|
40451
|
+
}
|
|
40452
|
+
};
|
|
40453
|
+
function enforceCachePermissions(dbPath) {
|
|
40454
|
+
chmodSync(dirname2(dbPath), 448);
|
|
40455
|
+
chmodSync(dbPath, 384);
|
|
40456
|
+
for (const sibling of [`${dbPath}-wal`, `${dbPath}-shm`]) {
|
|
40457
|
+
if (existsSync(sibling)) chmodSync(sibling, 384);
|
|
40458
|
+
}
|
|
40459
|
+
}
|
|
40460
|
+
var OFWCache = class _OFWCache extends LocalCacheStore {
|
|
40461
|
+
constructor(db, core) {
|
|
40462
|
+
super(core);
|
|
40463
|
+
this.db = db;
|
|
40464
|
+
}
|
|
40465
|
+
db;
|
|
40466
|
+
static open(path) {
|
|
40467
|
+
const memory = path === ":memory:";
|
|
40468
|
+
if (!memory) mkdirSync(dirname2(path), { recursive: true });
|
|
40469
|
+
const db = new DatabaseSync(path);
|
|
40470
|
+
if (!memory) enforceCachePermissions(path);
|
|
40471
|
+
db.exec("PRAGMA journal_mode = WAL");
|
|
40472
|
+
db.exec("PRAGMA foreign_keys = ON");
|
|
40473
|
+
const core = new OFWCacheCore(new NodeSqlDriver(db));
|
|
40474
|
+
if (!memory) enforceCachePermissions(path);
|
|
40475
|
+
return new _OFWCache(db, core);
|
|
40476
|
+
}
|
|
40477
|
+
close() {
|
|
40478
|
+
this.db.close();
|
|
40479
|
+
}
|
|
40480
|
+
};
|
|
40481
|
+
|
|
40482
|
+
// src/tools/attachments.ts
|
|
40483
|
+
import { readFileSync, statSync, mkdirSync as mkdirSync2, writeFileSync } from "node:fs";
|
|
40484
|
+
import { basename as basename2, dirname as dirname3, extname } from "node:path";
|
|
40485
|
+
var MIME_BY_EXT = {
|
|
40486
|
+
".pdf": "application/pdf",
|
|
40487
|
+
".png": "image/png",
|
|
40488
|
+
".jpg": "image/jpeg",
|
|
40489
|
+
".jpeg": "image/jpeg",
|
|
40490
|
+
".gif": "image/gif",
|
|
40491
|
+
".webp": "image/webp",
|
|
40492
|
+
".heic": "image/heic",
|
|
40493
|
+
".txt": "text/plain",
|
|
40494
|
+
".md": "text/markdown",
|
|
40495
|
+
".csv": "text/csv",
|
|
40496
|
+
".html": "text/html",
|
|
40497
|
+
".htm": "text/html",
|
|
40498
|
+
".json": "application/json",
|
|
40499
|
+
".xml": "application/xml",
|
|
40500
|
+
".doc": "application/msword",
|
|
40501
|
+
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
40502
|
+
".xls": "application/vnd.ms-excel",
|
|
40503
|
+
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
40504
|
+
".ppt": "application/vnd.ms-powerpoint",
|
|
40505
|
+
".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
40506
|
+
".zip": "application/zip",
|
|
40507
|
+
".ics": "text/calendar"
|
|
40508
|
+
};
|
|
40509
|
+
function mimeFromName(name) {
|
|
40510
|
+
return MIME_BY_EXT[extname(name).toLowerCase()] ?? "application/octet-stream";
|
|
40511
|
+
}
|
|
40512
|
+
var NodeAttachmentIO = class {
|
|
40513
|
+
async resolveUpload(path) {
|
|
40514
|
+
const abs = expandPath(path);
|
|
40515
|
+
const stat = statSync(abs);
|
|
40516
|
+
if (!stat.isFile()) throw new Error(`Not a file: ${abs}`);
|
|
40517
|
+
const fileName = basename2(abs);
|
|
40518
|
+
const mimeType = mimeFromName(fileName);
|
|
40519
|
+
const blob = await fileBlob(abs, { type: mimeType });
|
|
40520
|
+
return { blob, fileName, mimeType, sizeBytes: stat.size };
|
|
40521
|
+
}
|
|
40522
|
+
readDownloaded(path) {
|
|
40523
|
+
try {
|
|
40524
|
+
return readFileSync(path);
|
|
40525
|
+
} catch {
|
|
40526
|
+
return null;
|
|
40527
|
+
}
|
|
40528
|
+
}
|
|
40529
|
+
writeDownload(dest, bytes) {
|
|
40530
|
+
mkdirSync2(dirname3(dest), { recursive: true });
|
|
40531
|
+
writeFileSync(dest, bytes);
|
|
40532
|
+
}
|
|
40533
|
+
};
|
|
40534
|
+
|
|
39929
40535
|
// src/index.ts
|
|
39930
40536
|
var originalEmit = process.emit.bind(process);
|
|
39931
40537
|
process.emit = function(event, ...args) {
|
|
@@ -39937,14 +40543,17 @@ process.emit = function(event, ...args) {
|
|
|
39937
40543
|
}
|
|
39938
40544
|
return originalEmit(event, ...args);
|
|
39939
40545
|
};
|
|
40546
|
+
var nodeCache;
|
|
40547
|
+
var nodeCacheProvider = () => nodeCache ??= OFWCache.open(getCacheDbPath());
|
|
40548
|
+
var nodeAttachmentIO = new NodeAttachmentIO();
|
|
39940
40549
|
await runMcp({
|
|
39941
40550
|
name: "ofw",
|
|
39942
|
-
version: "2.
|
|
40551
|
+
version: "2.6.3",
|
|
39943
40552
|
// x-release-please-version
|
|
39944
40553
|
deps: client,
|
|
39945
40554
|
tools: [
|
|
39946
40555
|
registerUserTools,
|
|
39947
|
-
registerMessageTools,
|
|
40556
|
+
(server, deps) => registerMessageTools(server, deps, nodeCacheProvider, nodeAttachmentIO),
|
|
39948
40557
|
registerCalendarTools,
|
|
39949
40558
|
registerExpenseTools,
|
|
39950
40559
|
registerJournalTools
|