ofw-mcp 2.9.1 → 2.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/dist/bundle.js +423 -88
- package/dist/index.js +1 -1
- package/dist/sync.js +7 -2
- package/dist/tools/_shared.js +25 -0
- package/dist/tools/draft-freshness.js +13 -2
- package/dist/tools/lifecycle.js +38 -12
- package/dist/tools/messages.js +264 -79
- package/package.json +3 -3
- package/server.json +2 -2
- package/skills/ofw/SKILL.md +5 -5
package/dist/bundle.js
CHANGED
|
@@ -34929,7 +34929,8 @@ var KNOWN_CAPABILITIES = /* @__PURE__ */ new Set([
|
|
|
34929
34929
|
"capture_redirect",
|
|
34930
34930
|
"read_indexed_db",
|
|
34931
34931
|
"read_dom",
|
|
34932
|
-
"download"
|
|
34932
|
+
"download",
|
|
34933
|
+
"graphql"
|
|
34933
34934
|
]);
|
|
34934
34935
|
|
|
34935
34936
|
// node_modules/@fetchproxy/protocol/dist/mcp-id.js
|
|
@@ -35232,6 +35233,7 @@ function assertIndexedDbScopesArray(value, label) {
|
|
|
35232
35233
|
}
|
|
35233
35234
|
var DOM_SELECTOR_RE = /^[^-]{1,512}$/;
|
|
35234
35235
|
var DOM_ATTRIBUTE_RE = /^[A-Za-z_:][A-Za-z0-9_:.\-]{0,127}$/;
|
|
35236
|
+
var GRAPHQL_OP_NAME_RE = /^[_A-Za-z][_0-9A-Za-z]{0,127}$/;
|
|
35235
35237
|
function assertDomSelectorsArray(value, label) {
|
|
35236
35238
|
if (!Array.isArray(value)) {
|
|
35237
35239
|
throw new ProtocolError(`${label}: expected array, got ${typeof value}`);
|
|
@@ -35268,6 +35270,37 @@ function assertDomSelectorsArray(value, label) {
|
|
|
35268
35270
|
}
|
|
35269
35271
|
}
|
|
35270
35272
|
}
|
|
35273
|
+
function assertGraphqlOpsArray(value, label) {
|
|
35274
|
+
if (!Array.isArray(value)) {
|
|
35275
|
+
throw new ProtocolError(`${label}: expected array, got ${typeof value}`);
|
|
35276
|
+
}
|
|
35277
|
+
const seen = /* @__PURE__ */ new Set();
|
|
35278
|
+
for (let i = 0; i < value.length; i++) {
|
|
35279
|
+
const entry = value[i];
|
|
35280
|
+
assertObject(entry, `${label}[${i}]`);
|
|
35281
|
+
if (entry.name === void 0) {
|
|
35282
|
+
throw new ProtocolError(`${label}[${i}].name: missing`);
|
|
35283
|
+
}
|
|
35284
|
+
if (entry.operationName === void 0) {
|
|
35285
|
+
throw new ProtocolError(`${label}[${i}].operationName: missing`);
|
|
35286
|
+
}
|
|
35287
|
+
if (typeof entry.name !== "string" || !SCOPE_KEY_RE.test(entry.name)) {
|
|
35288
|
+
throw new ProtocolError(`${label}[${i}].name: invalid ${JSON.stringify(entry.name)}`);
|
|
35289
|
+
}
|
|
35290
|
+
if (typeof entry.operationName !== "string" || !GRAPHQL_OP_NAME_RE.test(entry.operationName)) {
|
|
35291
|
+
throw new ProtocolError(`${label}[${i}].operationName: invalid ${JSON.stringify(entry.operationName)}`);
|
|
35292
|
+
}
|
|
35293
|
+
if (seen.has(entry.name)) {
|
|
35294
|
+
throw new ProtocolError(`${label}: duplicate name ${JSON.stringify(entry.name)}`);
|
|
35295
|
+
}
|
|
35296
|
+
seen.add(entry.name);
|
|
35297
|
+
for (const k of Object.keys(entry)) {
|
|
35298
|
+
if (k !== "name" && k !== "operationName") {
|
|
35299
|
+
throw new ProtocolError(`${label}[${i}]: unexpected field ${JSON.stringify(k)}`);
|
|
35300
|
+
}
|
|
35301
|
+
}
|
|
35302
|
+
}
|
|
35303
|
+
}
|
|
35271
35304
|
function validateFrame(raw) {
|
|
35272
35305
|
assertObject(raw, "frame");
|
|
35273
35306
|
const t = raw.type;
|
|
@@ -35346,6 +35379,9 @@ function validateHello(raw) {
|
|
|
35346
35379
|
if (raw.domSelectors !== void 0) {
|
|
35347
35380
|
assertDomSelectorsArray(raw.domSelectors, "hello.domSelectors");
|
|
35348
35381
|
}
|
|
35382
|
+
if (raw.graphqlOps !== void 0) {
|
|
35383
|
+
assertGraphqlOpsArray(raw.graphqlOps, "hello.graphqlOps");
|
|
35384
|
+
}
|
|
35349
35385
|
assertBase64(raw.identityX25519Pub, "hello.identityX25519Pub");
|
|
35350
35386
|
assertBase64(raw.identityEd25519Pub, "hello.identityEd25519Pub");
|
|
35351
35387
|
assertBase64(raw.sessionNonce, "hello.sessionNonce");
|
|
@@ -35595,6 +35631,28 @@ function validateInnerRequest(raw) {
|
|
|
35595
35631
|
}
|
|
35596
35632
|
return raw;
|
|
35597
35633
|
}
|
|
35634
|
+
if (raw.op === "graphql_query") {
|
|
35635
|
+
assertObject(raw.init, "inner.init");
|
|
35636
|
+
if (raw.init.name === void 0)
|
|
35637
|
+
throw new ProtocolError("inner.init.name: missing");
|
|
35638
|
+
if (raw.init.variables === void 0) {
|
|
35639
|
+
throw new ProtocolError("inner.init.variables: missing");
|
|
35640
|
+
}
|
|
35641
|
+
assertString(raw.init.name, "inner.init.name");
|
|
35642
|
+
if (raw.init.name.length === 0) {
|
|
35643
|
+
throw new ProtocolError("inner.init.name: must be non-empty");
|
|
35644
|
+
}
|
|
35645
|
+
assertObject(raw.init.variables, "inner.init.variables");
|
|
35646
|
+
if (raw.init.tabUrl !== void 0) {
|
|
35647
|
+
assertString(raw.init.tabUrl, "inner.init.tabUrl");
|
|
35648
|
+
}
|
|
35649
|
+
for (const k of Object.keys(raw.init)) {
|
|
35650
|
+
if (k !== "name" && k !== "variables" && k !== "tabUrl") {
|
|
35651
|
+
throw new ProtocolError(`inner.init: unexpected field ${JSON.stringify(k)} on graphql_query`);
|
|
35652
|
+
}
|
|
35653
|
+
}
|
|
35654
|
+
return raw;
|
|
35655
|
+
}
|
|
35598
35656
|
if (raw.op === "download") {
|
|
35599
35657
|
assertObject(raw.init, "inner.init");
|
|
35600
35658
|
if (raw.init.url === void 0) {
|
|
@@ -35620,7 +35678,7 @@ function validateInnerRequest(raw) {
|
|
|
35620
35678
|
}
|
|
35621
35679
|
return raw;
|
|
35622
35680
|
}
|
|
35623
|
-
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)}`);
|
|
35681
|
+
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", "graphql_query"; got ${JSON.stringify(raw.op)}`);
|
|
35624
35682
|
}
|
|
35625
35683
|
function assertNonEmptyKeyArray(value, label) {
|
|
35626
35684
|
if (!Array.isArray(value)) {
|
|
@@ -35651,6 +35709,10 @@ function assertStringMap(value, label) {
|
|
|
35651
35709
|
}
|
|
35652
35710
|
}
|
|
35653
35711
|
}
|
|
35712
|
+
var KNOWN_RESPONSE_OPS = /* @__PURE__ */ new Set([
|
|
35713
|
+
...KNOWN_CAPABILITIES,
|
|
35714
|
+
"graphql_query"
|
|
35715
|
+
]);
|
|
35654
35716
|
function validateInnerResponse(raw) {
|
|
35655
35717
|
assertPositiveInt(raw.id, "inner.id");
|
|
35656
35718
|
if (raw.ok === true) {
|
|
@@ -35712,6 +35774,13 @@ function validateInnerResponse(raw) {
|
|
|
35712
35774
|
assertStringMap(raw.values, "inner.values");
|
|
35713
35775
|
return raw;
|
|
35714
35776
|
}
|
|
35777
|
+
if (op === "graphql_query") {
|
|
35778
|
+
if (raw.data === void 0) {
|
|
35779
|
+
throw new ProtocolError("inner.data: missing on graphql_query response");
|
|
35780
|
+
}
|
|
35781
|
+
assertObject(raw.data, "inner.data");
|
|
35782
|
+
return raw;
|
|
35783
|
+
}
|
|
35715
35784
|
if (op === "download") {
|
|
35716
35785
|
assertObject(raw.value, "inner.value");
|
|
35717
35786
|
assertString(raw.value.path, "inner.value.path");
|
|
@@ -35736,7 +35805,7 @@ function validateInnerResponse(raw) {
|
|
|
35736
35805
|
if (raw.ok === false) {
|
|
35737
35806
|
assertString(raw.error, "inner.error");
|
|
35738
35807
|
if (raw.op !== void 0) {
|
|
35739
|
-
if (typeof raw.op !== "string" || !
|
|
35808
|
+
if (typeof raw.op !== "string" || !KNOWN_RESPONSE_OPS.has(raw.op)) {
|
|
35740
35809
|
throw new ProtocolError(`inner.op: unknown response op ${JSON.stringify(raw.op)}`);
|
|
35741
35810
|
}
|
|
35742
35811
|
}
|
|
@@ -35928,11 +35997,33 @@ async function sealInnerFrame(sessionKey, mcpId, seq, inner) {
|
|
|
35928
35997
|
};
|
|
35929
35998
|
}
|
|
35930
35999
|
async function openEncryptedFrame(sessionKey, frame) {
|
|
35931
|
-
const
|
|
35932
|
-
|
|
35933
|
-
|
|
35934
|
-
|
|
35935
|
-
|
|
36000
|
+
const result = await openEncryptedFrameDetailed(sessionKey, frame);
|
|
36001
|
+
if (result.stage === "ok")
|
|
36002
|
+
return result.inner;
|
|
36003
|
+
throw result.error instanceof Error ? result.error : new Error(String(result.error));
|
|
36004
|
+
}
|
|
36005
|
+
async function openEncryptedFrameDetailed(sessionKey, frame) {
|
|
36006
|
+
let pt;
|
|
36007
|
+
try {
|
|
36008
|
+
const iv = fromB64(frame.iv);
|
|
36009
|
+
const ct = fromB64(frame.ciphertext);
|
|
36010
|
+
pt = await aesGcmOpen(sessionKey, iv, ct);
|
|
36011
|
+
} catch (error51) {
|
|
36012
|
+
return { stage: "decrypt-failed", error: error51 };
|
|
36013
|
+
}
|
|
36014
|
+
let parsed;
|
|
36015
|
+
try {
|
|
36016
|
+
parsed = JSON.parse(dec.decode(pt));
|
|
36017
|
+
} catch (error51) {
|
|
36018
|
+
return { stage: "validation-failed", error: error51, recoveredId: void 0 };
|
|
36019
|
+
}
|
|
36020
|
+
try {
|
|
36021
|
+
const inner = validateInnerFrame(parsed);
|
|
36022
|
+
return { stage: "ok", inner };
|
|
36023
|
+
} catch (error51) {
|
|
36024
|
+
const recoveredId = parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) && typeof parsed.id === "number" && Number.isInteger(parsed.id) && parsed.id > 0 ? parsed.id : void 0;
|
|
36025
|
+
return { stage: "validation-failed", error: error51, recoveredId };
|
|
36026
|
+
}
|
|
35936
36027
|
}
|
|
35937
36028
|
|
|
35938
36029
|
// node_modules/@fetchproxy/server/dist/election.js
|
|
@@ -36058,6 +36149,12 @@ async function buildServerHello(opts) {
|
|
|
36058
36149
|
...d.attribute !== void 0 ? { attribute: d.attribute } : {}
|
|
36059
36150
|
}));
|
|
36060
36151
|
}
|
|
36152
|
+
if (opts.graphqlOps && opts.graphqlOps.length > 0) {
|
|
36153
|
+
hello.graphqlOps = opts.graphqlOps.map((d) => ({
|
|
36154
|
+
name: d.name,
|
|
36155
|
+
operationName: d.operationName
|
|
36156
|
+
}));
|
|
36157
|
+
}
|
|
36061
36158
|
return hello;
|
|
36062
36159
|
}
|
|
36063
36160
|
|
|
@@ -36148,7 +36245,8 @@ async function startHost(opts) {
|
|
|
36148
36245
|
indexedDbScopes: opts.ownIndexedDbScopes,
|
|
36149
36246
|
localStoragePointers: opts.ownLocalStoragePointers,
|
|
36150
36247
|
sessionStoragePointers: opts.ownSessionStoragePointers,
|
|
36151
|
-
domSelectors: opts.ownDomSelectors
|
|
36248
|
+
domSelectors: opts.ownDomSelectors,
|
|
36249
|
+
graphqlOps: opts.ownGraphqlOps
|
|
36152
36250
|
});
|
|
36153
36251
|
const ownSessionNonce = fromB64(ownHello.sessionNonce);
|
|
36154
36252
|
let extensionWs = null;
|
|
@@ -36385,7 +36483,8 @@ async function startPeer(opts) {
|
|
|
36385
36483
|
indexedDbScopes: opts.indexedDbScopes,
|
|
36386
36484
|
domSelectors: opts.domSelectors,
|
|
36387
36485
|
localStoragePointers: opts.localStoragePointers,
|
|
36388
|
-
sessionStoragePointers: opts.sessionStoragePointers
|
|
36486
|
+
sessionStoragePointers: opts.sessionStoragePointers,
|
|
36487
|
+
graphqlOps: opts.graphqlOps
|
|
36389
36488
|
});
|
|
36390
36489
|
const sessionNonce = fromB64(hello.sessionNonce);
|
|
36391
36490
|
ws.send(JSON.stringify(hello));
|
|
@@ -36429,10 +36528,20 @@ async function startPeer(opts) {
|
|
|
36429
36528
|
return;
|
|
36430
36529
|
if (!session.acceptInboundSeq(frame.seq))
|
|
36431
36530
|
return;
|
|
36432
|
-
|
|
36433
|
-
|
|
36434
|
-
innerListeners.forEach((cb) => cb(inner));
|
|
36435
|
-
}
|
|
36531
|
+
const result = await openEncryptedFrameDetailed(session.sessionKey, frame);
|
|
36532
|
+
if (result.stage === "ok") {
|
|
36533
|
+
innerListeners.forEach((cb) => cb(result.inner));
|
|
36534
|
+
} else if (result.stage === "decrypt-failed") {
|
|
36535
|
+
} else {
|
|
36536
|
+
console.error("[fetchproxy] peer: received a frame that decrypted OK but failed validation:", result.error);
|
|
36537
|
+
if (result.recoveredId !== void 0) {
|
|
36538
|
+
innerListeners.forEach((cb) => cb({
|
|
36539
|
+
type: "response",
|
|
36540
|
+
id: result.recoveredId,
|
|
36541
|
+
ok: false,
|
|
36542
|
+
error: `malformed response failed protocol validation: ${String(result.error)}`
|
|
36543
|
+
}));
|
|
36544
|
+
}
|
|
36436
36545
|
}
|
|
36437
36546
|
}
|
|
36438
36547
|
} catch (e) {
|
|
@@ -36709,6 +36818,10 @@ var FetchproxyServer = class {
|
|
|
36709
36818
|
pendingIdb = /* @__PURE__ */ new Map();
|
|
36710
36819
|
// download awaiters resolve the saved-file metadata (path + size + mime).
|
|
36711
36820
|
pendingDownload = /* @__PURE__ */ new Map();
|
|
36821
|
+
// 1.x+: graphql_query awaiters resolve the GraphQL `data` object. Its
|
|
36822
|
+
// shape is operation-specific, so the awaiter resolves `unknown` and the
|
|
36823
|
+
// caller narrows.
|
|
36824
|
+
pendingGraphql = /* @__PURE__ */ new Map();
|
|
36712
36825
|
mcpId = null;
|
|
36713
36826
|
identity = null;
|
|
36714
36827
|
// 0.5.3+: in-flight role-election / handle-start promise. Set the
|
|
@@ -36796,6 +36909,10 @@ var FetchproxyServer = class {
|
|
|
36796
36909
|
selector: d.selector,
|
|
36797
36910
|
...d.attribute !== void 0 ? { attribute: d.attribute } : {}
|
|
36798
36911
|
})),
|
|
36912
|
+
graphqlOps: (opts.graphqlOps ?? []).map((d) => ({
|
|
36913
|
+
name: d.name,
|
|
36914
|
+
operationName: d.operationName
|
|
36915
|
+
})),
|
|
36799
36916
|
// 0.8.0+: timer + lazy-revive default to ON. Every realty MCP
|
|
36800
36917
|
// adapter was about to set these to the same numbers anyway; the
|
|
36801
36918
|
// back-door is `0` (explicit opt-out) if a caller genuinely wants
|
|
@@ -36917,6 +37034,7 @@ var FetchproxyServer = class {
|
|
|
36917
37034
|
ownLocalStoragePointers: this.opts.localStoragePointers,
|
|
36918
37035
|
ownSessionStoragePointers: this.opts.sessionStoragePointers,
|
|
36919
37036
|
ownDomSelectors: this.opts.domSelectors,
|
|
37037
|
+
ownGraphqlOps: this.opts.graphqlOps,
|
|
36920
37038
|
onPairCode: this.opts.onPairCode
|
|
36921
37039
|
});
|
|
36922
37040
|
this.hostHandle.onOwnInner((inner) => this.onInner(inner));
|
|
@@ -36945,7 +37063,8 @@ var FetchproxyServer = class {
|
|
|
36945
37063
|
indexedDbScopes: this.opts.indexedDbScopes,
|
|
36946
37064
|
localStoragePointers: this.opts.localStoragePointers,
|
|
36947
37065
|
sessionStoragePointers: this.opts.sessionStoragePointers,
|
|
36948
|
-
domSelectors: this.opts.domSelectors
|
|
37066
|
+
domSelectors: this.opts.domSelectors,
|
|
37067
|
+
graphqlOps: this.opts.graphqlOps
|
|
36949
37068
|
});
|
|
36950
37069
|
this.peerHandle.onInner((inner) => this.onInner(inner));
|
|
36951
37070
|
this.peerHandle.onRenegotiate(() => {
|
|
@@ -37151,6 +37270,7 @@ var FetchproxyServer = class {
|
|
|
37151
37270
|
this.pendingRedirect.delete(id);
|
|
37152
37271
|
this.pendingDownload.delete(id);
|
|
37153
37272
|
this.pendingIdb.delete(id);
|
|
37273
|
+
this.pendingGraphql.delete(id);
|
|
37154
37274
|
}
|
|
37155
37275
|
throw err;
|
|
37156
37276
|
}
|
|
@@ -37960,6 +38080,52 @@ var FetchproxyServer = class {
|
|
|
37960
38080
|
await this.sendInnerFrame(inner);
|
|
37961
38081
|
return this._withVerbTimeout(pending, this.pendingStorage, id, origin);
|
|
37962
38082
|
}
|
|
38083
|
+
/**
|
|
38084
|
+
* 1.x+: run a declared GraphQL operation through the page's own Apollo
|
|
38085
|
+
* client (`window.__APOLLO_CLIENT__`) in the signed-in tab's MAIN world.
|
|
38086
|
+
* Requires `'graphql'` in capabilities AND `name` to match a declared
|
|
38087
|
+
* `graphqlOps` entry. The extension resolves `name` → `operationName` →
|
|
38088
|
+
* the live DocumentNode the page already observed, then invokes
|
|
38089
|
+
* `client.query({ query, variables })` — the site's own request path, so
|
|
38090
|
+
* per-request bot telemetry (Akamai etc.) runs automatically.
|
|
38091
|
+
*
|
|
38092
|
+
* Returns the GraphQL `data` object on success (shape is
|
|
38093
|
+
* operation-specific; the caller narrows). Throws a plain `Error` on
|
|
38094
|
+
* developer mistakes (undeclared capability, undeclared name) and a
|
|
38095
|
+
* descriptive `Error` on the `ok:false` bridge path — which includes the
|
|
38096
|
+
* typed "operation not yet observed on this tab" case (open the site's
|
|
38097
|
+
* page and retry).
|
|
38098
|
+
*/
|
|
38099
|
+
async graphqlQuery(opts) {
|
|
38100
|
+
if (!this.opts.capabilities.includes("graphql")) {
|
|
38101
|
+
throw new Error('FetchproxyServer.graphqlQuery(): MCP did not declare "graphql" in capabilities');
|
|
38102
|
+
}
|
|
38103
|
+
if (typeof opts.name !== "string" || opts.name.length === 0) {
|
|
38104
|
+
throw new Error("FetchproxyServer.graphqlQuery: opts.name must be a non-empty string");
|
|
38105
|
+
}
|
|
38106
|
+
const declaredNames = this.opts.graphqlOps.map((d) => d.name);
|
|
38107
|
+
if (!declaredNames.includes(opts.name)) {
|
|
38108
|
+
throw new Error(`FetchproxyServer.graphqlQuery: operation ${JSON.stringify(opts.name)} not in declared graphqlOps [${declaredNames.map((n) => JSON.stringify(n)).join(", ")}]`);
|
|
38109
|
+
}
|
|
38110
|
+
await this.ensureConnected();
|
|
38111
|
+
this.throwIfPendingPair();
|
|
38112
|
+
const id = this.nextRequestId++;
|
|
38113
|
+
const inner = {
|
|
38114
|
+
type: "request",
|
|
38115
|
+
id,
|
|
38116
|
+
op: "graphql_query",
|
|
38117
|
+
init: {
|
|
38118
|
+
name: opts.name,
|
|
38119
|
+
variables: opts.variables,
|
|
38120
|
+
...opts.tabUrl !== void 0 ? { tabUrl: opts.tabUrl } : {}
|
|
38121
|
+
}
|
|
38122
|
+
};
|
|
38123
|
+
const pending = new Promise((resolve2, reject) => {
|
|
38124
|
+
this.pendingGraphql.set(id, { resolve: resolve2, reject });
|
|
38125
|
+
});
|
|
38126
|
+
await this.sendInnerFrame(inner);
|
|
38127
|
+
return this._withVerbTimeout(pending, this.pendingGraphql, id, opts.name);
|
|
38128
|
+
}
|
|
37963
38129
|
assertScopeSubset(requested, declared, label) {
|
|
37964
38130
|
const undeclared = undeclaredKeys(requested, declared);
|
|
37965
38131
|
if (undeclared.length > 0) {
|
|
@@ -38097,6 +38263,20 @@ var FetchproxyServer = class {
|
|
|
38097
38263
|
}
|
|
38098
38264
|
return;
|
|
38099
38265
|
}
|
|
38266
|
+
const graphqlCb = this.pendingGraphql.get(inner.id);
|
|
38267
|
+
if (graphqlCb) {
|
|
38268
|
+
this.pendingGraphql.delete(inner.id);
|
|
38269
|
+
if (inner.ok) {
|
|
38270
|
+
if (inner.op === "graphql_query") {
|
|
38271
|
+
graphqlCb.resolve(inner.data);
|
|
38272
|
+
} else {
|
|
38273
|
+
graphqlCb.reject(new FetchproxyProtocolError(`unexpected ${String(inner.op)} response on graphql_query awaiter`));
|
|
38274
|
+
}
|
|
38275
|
+
} else {
|
|
38276
|
+
graphqlCb.reject(new FetchproxyProtocolError(inner.error));
|
|
38277
|
+
}
|
|
38278
|
+
return;
|
|
38279
|
+
}
|
|
38100
38280
|
const cookiesCb = this.pendingReadCookies.get(inner.id);
|
|
38101
38281
|
if (cookiesCb) {
|
|
38102
38282
|
this.pendingReadCookies.delete(inner.id);
|
|
@@ -38148,6 +38328,9 @@ var FetchproxyServer = class {
|
|
|
38148
38328
|
for (const { reject } of this.pendingDownload.values())
|
|
38149
38329
|
reject(err);
|
|
38150
38330
|
this.pendingDownload.clear();
|
|
38331
|
+
for (const { reject } of this.pendingGraphql.values())
|
|
38332
|
+
reject(err);
|
|
38333
|
+
this.pendingGraphql.clear();
|
|
38151
38334
|
}
|
|
38152
38335
|
/**
|
|
38153
38336
|
* 0.5.2+: read the current pair-pending pair code from whichever handle
|
|
@@ -38407,7 +38590,7 @@ async function loginWithPassword(username, password) {
|
|
|
38407
38590
|
// package.json
|
|
38408
38591
|
var package_default = {
|
|
38409
38592
|
name: "ofw-mcp",
|
|
38410
|
-
version: "2.
|
|
38593
|
+
version: "2.10.0",
|
|
38411
38594
|
license: "MIT",
|
|
38412
38595
|
mcpName: "io.github.chrischall/ofw-mcp",
|
|
38413
38596
|
description: "OurFamilyWizard MCP server for Claude \u2014 developed and maintained by AI (Claude Code)",
|
|
@@ -38442,8 +38625,8 @@ var package_default = {
|
|
|
38442
38625
|
"worker:test": "vitest run --config vitest.workers.config.ts"
|
|
38443
38626
|
},
|
|
38444
38627
|
dependencies: {
|
|
38445
|
-
"@chrischall/mcp-utils": "^0.
|
|
38446
|
-
"@fetchproxy/bootstrap": "^1.
|
|
38628
|
+
"@chrischall/mcp-utils": "^0.14.0",
|
|
38629
|
+
"@fetchproxy/bootstrap": "^1.7.0",
|
|
38447
38630
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
38448
38631
|
dotenv: "^17.4.2",
|
|
38449
38632
|
zod: "^4.4.3"
|
|
@@ -38916,6 +39099,16 @@ function mapRecipients(items) {
|
|
|
38916
39099
|
function hasRealView(recipients) {
|
|
38917
39100
|
return recipients.some((r) => r.viewedAt !== null && !r.viewedAt.startsWith("1970-01-01"));
|
|
38918
39101
|
}
|
|
39102
|
+
function threadedReplyTo(detail) {
|
|
39103
|
+
return detail.replyToId ?? detail.inReplyTo ?? null;
|
|
39104
|
+
}
|
|
39105
|
+
function reportsThreaded(detail) {
|
|
39106
|
+
return threadedReplyTo(detail) !== null || detail.showContext === true;
|
|
39107
|
+
}
|
|
39108
|
+
function reportsUnthreaded(detail) {
|
|
39109
|
+
if (reportsThreaded(detail)) return false;
|
|
39110
|
+
return detail.inReplyTo !== void 0 || detail.showContext !== void 0;
|
|
39111
|
+
}
|
|
38919
39112
|
function scrapeSaysRead(listData) {
|
|
38920
39113
|
if (typeof listData !== "object" || listData === null) return false;
|
|
38921
39114
|
const ld = listData;
|
|
@@ -39230,7 +39423,12 @@ var DraftListItemSchema = external_exports.looseObject({
|
|
|
39230
39423
|
id: external_exports.number(),
|
|
39231
39424
|
subject: external_exports.string(),
|
|
39232
39425
|
date: external_exports.looseObject({ dateTime: external_exports.string() }),
|
|
39426
|
+
// Both spellings of the threading echo — OFW reports the reply target as
|
|
39427
|
+
// `inReplyTo` (with showContext) on list payloads where `replyToId` is null.
|
|
39428
|
+
// The cached row must derive the SAME value ofw_save_draft derived from the
|
|
39429
|
+
// detail, or the content revision drifts between a save and the next sync.
|
|
39233
39430
|
replyToId: external_exports.number().nullable().optional(),
|
|
39431
|
+
inReplyTo: external_exports.number().nullable().optional(),
|
|
39234
39432
|
recipients: external_exports.array(ApiRecipientSchema).optional()
|
|
39235
39433
|
});
|
|
39236
39434
|
var DraftListResponseSchema = external_exports.looseObject({ data: external_exports.array(DraftListItemSchema).optional() });
|
|
@@ -39293,7 +39491,7 @@ async function syncDrafts(client2, draftsFolderId, store, budget) {
|
|
|
39293
39491
|
subject: detail.subject ?? item.subject ?? "(no subject)",
|
|
39294
39492
|
body: detail.body ?? "",
|
|
39295
39493
|
recipients: mapRecipients(item.recipients),
|
|
39296
|
-
replyToId: item
|
|
39494
|
+
replyToId: threadedReplyTo(item),
|
|
39297
39495
|
modifiedAt: item.date?.dateTime ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
39298
39496
|
listData: item
|
|
39299
39497
|
});
|
|
@@ -39594,8 +39792,18 @@ function draftRevision(d) {
|
|
|
39594
39792
|
var ServerDraftSchema = external_exports.looseObject({
|
|
39595
39793
|
subject: external_exports.string().optional(),
|
|
39596
39794
|
body: external_exports.string().optional(),
|
|
39795
|
+
// BOTH spellings of the threading echo (see ThreadingEcho in _shared.ts):
|
|
39796
|
+
// OFW reports the reply target as `replyToId` on some payloads and as
|
|
39797
|
+
// `inReplyTo` on others. The snapshot derives one value from whichever is
|
|
39798
|
+
// present, so the revision hashed here matches the one ofw_save_draft
|
|
39799
|
+
// computed from the same server state — a one-sided read produced revisions
|
|
39800
|
+
// that disagreed about the same draft.
|
|
39597
39801
|
replyToId: external_exports.number().nullable().optional(),
|
|
39802
|
+
inReplyTo: external_exports.number().nullable().optional(),
|
|
39598
39803
|
recipients: external_exports.array(ApiRecipientSchema).optional(),
|
|
39804
|
+
// Attachment fileIds — read so send-by-draft carries the draft's
|
|
39805
|
+
// attachments onto the sent message (see DraftContent.files).
|
|
39806
|
+
files: external_exports.array(external_exports.number()).optional(),
|
|
39599
39807
|
// Read for the LIFECYCLE answer (see tools/lifecycle.ts): which folder OFW
|
|
39600
39808
|
// itself says this id lives in right now. `existsOnServer` alone cannot
|
|
39601
39809
|
// distinguish "still a draft" from "was sent" — a sent draft still exists.
|
|
@@ -39638,8 +39846,9 @@ async function fetchMessageSnapshot(client2, id) {
|
|
|
39638
39846
|
content: {
|
|
39639
39847
|
subject: detail.subject ?? "",
|
|
39640
39848
|
body: detail.body ?? "",
|
|
39641
|
-
replyToId: detail
|
|
39642
|
-
recipients: mapRecipients(detail.recipients)
|
|
39849
|
+
replyToId: threadedReplyTo(detail),
|
|
39850
|
+
recipients: mapRecipients(detail.recipients),
|
|
39851
|
+
...detail.files !== void 0 ? { files: detail.files } : {}
|
|
39643
39852
|
},
|
|
39644
39853
|
folderId: detail.folder?.id === void 0 ? null : String(detail.folder.id),
|
|
39645
39854
|
folderName: detail.folder?.name ?? null,
|
|
@@ -39769,13 +39978,24 @@ async function ensureFolderIdMap(client2, store) {
|
|
|
39769
39978
|
return { map: cached2, requests: 1 };
|
|
39770
39979
|
}
|
|
39771
39980
|
}
|
|
39981
|
+
var STATE_BY_FOLDER_NAME = /* @__PURE__ */ new Map([
|
|
39982
|
+
["drafts", "draft"],
|
|
39983
|
+
["sent", "sent"],
|
|
39984
|
+
["sent messages", "sent"],
|
|
39985
|
+
["inbox", "received"]
|
|
39986
|
+
]);
|
|
39772
39987
|
function classifyState(snapshot, map2) {
|
|
39773
39988
|
if (snapshot === null) return "deleted";
|
|
39774
|
-
const { folderId } = snapshot;
|
|
39775
|
-
if (folderId
|
|
39776
|
-
|
|
39777
|
-
|
|
39778
|
-
|
|
39989
|
+
const { folderId, folderName } = snapshot;
|
|
39990
|
+
if (folderId !== null) {
|
|
39991
|
+
if (map2.drafts !== null && folderId === map2.drafts) return "draft";
|
|
39992
|
+
if (map2.sent !== null && folderId === map2.sent) return "sent";
|
|
39993
|
+
if (map2.inbox !== null && folderId === map2.inbox) return "received";
|
|
39994
|
+
}
|
|
39995
|
+
if (folderName !== null) {
|
|
39996
|
+
const byName = STATE_BY_FOLDER_NAME.get(folderName.trim().toLowerCase());
|
|
39997
|
+
if (byName !== void 0) return byName;
|
|
39998
|
+
}
|
|
39779
39999
|
return "unknown";
|
|
39780
40000
|
}
|
|
39781
40001
|
function probeWouldStamp(cachedDraft, cachedMessage) {
|
|
@@ -40917,13 +41137,25 @@ var SentDetailSchema = external_exports.looseObject({
|
|
|
40917
41137
|
body: external_exports.string().optional(),
|
|
40918
41138
|
date: DateSchema.optional(),
|
|
40919
41139
|
from: external_exports.looseObject({ name: external_exports.string().optional() }).optional(),
|
|
40920
|
-
recipients: external_exports.array(ApiRecipientSchema).optional()
|
|
41140
|
+
recipients: external_exports.array(ApiRecipientSchema).optional(),
|
|
41141
|
+
// The threading echo, in BOTH spellings plus showContext — OFW reports the
|
|
41142
|
+
// reply target inconsistently across payloads (see ThreadingEcho in
|
|
41143
|
+
// _shared.ts). Backs the `threaded` verdict on ofw_send_message.
|
|
41144
|
+
replyToId: external_exports.number().nullable().optional(),
|
|
41145
|
+
inReplyTo: external_exports.number().nullable().optional(),
|
|
41146
|
+
showContext: external_exports.boolean().optional()
|
|
40921
41147
|
});
|
|
40922
41148
|
var SavedDraftDetailSchema = external_exports.looseObject({
|
|
40923
41149
|
subject: external_exports.string().optional(),
|
|
40924
41150
|
body: external_exports.string().optional(),
|
|
40925
41151
|
date: DateSchema.optional(),
|
|
41152
|
+
// All three threading-echo fields. Reading ONLY `replyToId` here fired a
|
|
41153
|
+
// false "OurFamilyWizard did not thread this draft" warning on nearly every
|
|
41154
|
+
// threaded save, while the same payload's `inReplyTo`/`showContext` showed
|
|
41155
|
+
// the draft WAS threaded — see threadedReplyTo in _shared.ts.
|
|
40926
41156
|
replyToId: external_exports.number().nullable().optional(),
|
|
41157
|
+
inReplyTo: external_exports.number().nullable().optional(),
|
|
41158
|
+
showContext: external_exports.boolean().optional(),
|
|
40927
41159
|
recipients: external_exports.array(ApiRecipientSchema).optional(),
|
|
40928
41160
|
// Read to audit whether requested myFileIDs actually attached (Defect 3).
|
|
40929
41161
|
files: external_exports.array(external_exports.number()).optional()
|
|
@@ -41122,6 +41354,11 @@ function registerMessageTools(server, client2, cacheProvider, attachmentIO) {
|
|
|
41122
41354
|
if (draftRow !== null) {
|
|
41123
41355
|
const { freshness: freshness2, serverConfirmed, cacheStatus } = await draftsFreshness(cache);
|
|
41124
41356
|
return jsonResponse({
|
|
41357
|
+
// Stable identity FIRST — the id below changes on every edit
|
|
41358
|
+
// (create-then-delete), so callers should key off draftKey. Null when
|
|
41359
|
+
// this draft was never written through this tool (e.g. authored in
|
|
41360
|
+
// the web app).
|
|
41361
|
+
draftKey: (await cache.getDraftLineageById(draftRow.id))?.draftKey ?? null,
|
|
41125
41362
|
id: draftRow.id,
|
|
41126
41363
|
folder: "drafts",
|
|
41127
41364
|
subject: draftRow.subject,
|
|
@@ -41138,13 +41375,9 @@ function registerMessageTools(server, client2, cacheProvider, attachmentIO) {
|
|
|
41138
41375
|
listData: draftRow.listData,
|
|
41139
41376
|
attachments: [],
|
|
41140
41377
|
// Concurrency token — pass as expectedRevision to ofw_save_draft /
|
|
41141
|
-
// ofw_delete_draft to assert you are
|
|
41378
|
+
// ofw_delete_draft / ofw_send_message to assert you are acting on
|
|
41379
|
+
// THIS version.
|
|
41142
41380
|
revision: draftRevision(draftRow),
|
|
41143
|
-
// Stable logical identity. Survives the create-then-delete id churn of
|
|
41144
|
-
// editing AND the transition to sent — pass it to ofw_status to ask
|
|
41145
|
-
// "what happened to the thing I was working on?". Null when this draft
|
|
41146
|
-
// was never written through this tool (e.g. authored in the web app).
|
|
41147
|
-
draftKey: (await cache.getDraftLineageById(draftRow.id))?.draftKey ?? null,
|
|
41148
41381
|
cacheStatus,
|
|
41149
41382
|
// False = this draft's existence and unsent status are remembered from
|
|
41150
41383
|
// a cache, not confirmed on OFW. Call ofw_check_freshness before
|
|
@@ -41227,16 +41460,19 @@ function registerMessageTools(server, client2, cacheProvider, attachmentIO) {
|
|
|
41227
41460
|
return jsonResponse({ ...withReadState(row), attachments, freshness });
|
|
41228
41461
|
});
|
|
41229
41462
|
if (allowSend) server.registerTool("ofw_send_message", {
|
|
41230
|
-
description: "Send a message via OurFamilyWizard
|
|
41463
|
+
description: "Send a message via OurFamilyWizard \u2014 the ONE irreversible operation here, so it carries the strongest guard. TO SEND AN EXISTING DRAFT (the safe default): pass draftId (or messageId \u2014 same thing). The tool re-reads the draft from OFW and sends the SERVER'S version, so what goes out is what is on OurFamilyWizard, not what this session remembers \u2014 subject/body act only as explicit overrides. It is guarded exactly like ofw_save_draft: pass expectedRevision to assert which version you are sending; if the draft changed on OFW since you read it \u2014 or no longer exists (it may already have been SENT) \u2014 the send is REFUSED with the current server content echoed back, and nothing goes out. RECIPIENTS: OurFamilyWizard does not persist recipients on drafts, so recipientIds is usually still required at send time (ids from ofw_get_profile). After the send is CONFIRMED (OFW returned the new message id and the re-fetched sent record matches what was posted), the source draft is deleted automatically; pass deleteDraftOnSuccess:false to keep it. On ANY failure or ambiguity the draft is never deleted \u2014 the response carries draftRetained:true with the reason. TO COMPOSE FROM SCRATCH: supply subject/body/recipientIds with no draftId. If replyToId is provided (or inherited from the draft), the cache may rewrite it to the latest reply in the same thread (a note is included when this happens). ATTACHMENTS: when sending by draftId, the server draft's own attachments carry over automatically; myFileIDs (from ofw_upload_attachment) overrides or attaches files on a fresh compose. The response leads with sentMessageId and the stable draftKey, and reports threaded (whether OFW actually linked the reply) and draftDeleted.",
|
|
41231
41464
|
annotations: { destructiveHint: true },
|
|
41232
41465
|
inputSchema: {
|
|
41233
|
-
subject: external_exports.string().describe("Message subject. Required unless messageId
|
|
41234
|
-
body: external_exports.string().describe("Message body text. Required unless messageId
|
|
41235
|
-
recipientIds: external_exports.array(external_exports.number()).describe("Array of recipient user IDs (get from ofw_get_profile).
|
|
41236
|
-
replyToId: external_exports.number().describe("ID of the message being replied to").optional(),
|
|
41237
|
-
|
|
41238
|
-
|
|
41239
|
-
|
|
41466
|
+
subject: external_exports.string().describe("Message subject. Required unless draftId/messageId is given (then it overrides the server draft's subject).").optional(),
|
|
41467
|
+
body: external_exports.string().describe("Message body text. Required unless draftId/messageId is given (then it overrides the server draft's body \u2014 omit it to send exactly what is on OurFamilyWizard).").optional(),
|
|
41468
|
+
recipientIds: external_exports.array(external_exports.number()).describe("Array of recipient user IDs (get from ofw_get_profile). Usually required even when sending a draft: OurFamilyWizard does not persist recipients on drafts.").optional(),
|
|
41469
|
+
replyToId: external_exports.number().describe("ID of the message being replied to. Defaults to the draft's stored reply target when sending by draftId.").optional(),
|
|
41470
|
+
draftId: external_exports.number().describe("ID of an existing draft to send. The draft is re-read from OurFamilyWizard and its SERVER content is sent; missing subject/body default from it. Guarded: a draft that changed since you read it, or that was already sent/deleted, refuses rather than sending blind.").optional(),
|
|
41471
|
+
messageId: external_exports.number().describe("Synonym for draftId (if both are passed they must be equal).").optional(),
|
|
41472
|
+
expectedRevision: external_exports.string().describe('With draftId: the `revision` from ofw_list_drafts / ofw_get_message / ofw_check_freshness for that draft. Asserts you are sending THAT version; if the draft changed on OFW since, the send is refused and the current server content returned. Omit and the tool compares the server against the local cache instead \u2014 omitting never means "send whatever is there now".').optional(),
|
|
41473
|
+
deleteDraftOnSuccess: external_exports.boolean().describe("Default true. Delete the source draft after \u2014 and ONLY after \u2014 the send is confirmed (new message id returned and the re-fetched sent record checks out). Set false to keep the draft. On a failed or unverifiable send the draft is ALWAYS kept, regardless of this flag.").optional(),
|
|
41474
|
+
force: external_exports.boolean().describe("Default false. Send even when the draft changed on OurFamilyWizard since you read it, or its current state could not be read. Only use after showing the user the conflict.").optional(),
|
|
41475
|
+
myFileIDs: external_exports.array(external_exports.number()).describe("Attachment file ids (from ofw_upload_attachment) to attach to the message. When sending by draftId, omit it to carry the server draft's own attachments over; passing it overrides them.").optional()
|
|
41240
41476
|
}
|
|
41241
41477
|
}, async (args) => {
|
|
41242
41478
|
if (args.messageId !== void 0 && args.draftId !== void 0 && args.messageId !== args.draftId) {
|
|
@@ -41244,37 +41480,51 @@ function registerMessageTools(server, client2, cacheProvider, attachmentIO) {
|
|
|
41244
41480
|
}
|
|
41245
41481
|
const draftRef = args.messageId ?? args.draftId;
|
|
41246
41482
|
const cache = cacheProvider();
|
|
41483
|
+
const deleteOnSuccess = args.deleteDraftOnSuccess ?? true;
|
|
41247
41484
|
let subject = args.subject;
|
|
41248
41485
|
let body = args.body;
|
|
41249
41486
|
let recipientIds = args.recipientIds;
|
|
41250
41487
|
let draftReplyToId = null;
|
|
41251
|
-
let
|
|
41252
|
-
let
|
|
41488
|
+
let guardNote = null;
|
|
41489
|
+
let serverDraft;
|
|
41253
41490
|
if (draftRef !== void 0) {
|
|
41254
|
-
|
|
41255
|
-
const
|
|
41256
|
-
if (
|
|
41257
|
-
|
|
41258
|
-
|
|
41259
|
-
|
|
41260
|
-
|
|
41261
|
-
|
|
41491
|
+
const cachedDraft = await cache.getDraft(draftRef);
|
|
41492
|
+
const needsContent = subject === void 0 || body === void 0 || recipientIds === void 0;
|
|
41493
|
+
if (needsContent || deleteOnSuccess) {
|
|
41494
|
+
const guard = await guardDestructiveDraftOp({
|
|
41495
|
+
cache,
|
|
41496
|
+
draftId: draftRef,
|
|
41497
|
+
expectedRevision: args.expectedRevision,
|
|
41498
|
+
force: args.force ?? false,
|
|
41499
|
+
action: "send"
|
|
41500
|
+
});
|
|
41501
|
+
if (!guard.ok) return guard.response;
|
|
41502
|
+
guardNote = guard.note;
|
|
41503
|
+
serverDraft = guard.server;
|
|
41504
|
+
}
|
|
41505
|
+
const base = serverDraft ?? cachedDraft;
|
|
41506
|
+
if (base != null) {
|
|
41507
|
+
subject = subject ?? base.subject;
|
|
41508
|
+
body = body ?? base.body;
|
|
41509
|
+
draftReplyToId = base.replyToId;
|
|
41510
|
+
}
|
|
41511
|
+
if (recipientIds === void 0) {
|
|
41512
|
+
const source = [serverDraft ?? null, cachedDraft].find(
|
|
41513
|
+
(s) => s !== null && s !== void 0 && s.recipients.some((r) => r.userId !== 0)
|
|
41514
|
+
);
|
|
41515
|
+
if (source != null) {
|
|
41516
|
+
recipientIds = [...new Set(source.recipients.map((r) => r.userId).filter((id) => id !== 0))];
|
|
41517
|
+
}
|
|
41262
41518
|
}
|
|
41263
41519
|
}
|
|
41264
41520
|
if (subject === void 0 || body === void 0 || recipientIds === void 0) {
|
|
41265
|
-
if (draftLookupAttempted && !draftFound) {
|
|
41266
|
-
throw new Error(
|
|
41267
|
-
`draft ${draftRef} not found in local cache. Call ofw_sync_messages first, or supply subject/body/recipientIds explicitly.`
|
|
41268
|
-
);
|
|
41269
|
-
}
|
|
41270
41521
|
const missing = [
|
|
41271
41522
|
subject === void 0 ? "subject" : null,
|
|
41272
41523
|
body === void 0 ? "body" : null,
|
|
41273
41524
|
recipientIds === void 0 ? "recipientIds" : null
|
|
41274
41525
|
].filter((n) => n !== null).join(", ");
|
|
41275
|
-
|
|
41276
|
-
|
|
41277
|
-
);
|
|
41526
|
+
const hint = draftRef === void 0 ? "Pass them directly, or pass draftId to send an existing draft." : missing === "recipientIds" ? `Draft ${draftRef} carries no stored recipients \u2014 OurFamilyWizard does not persist recipients on drafts, so they must be supplied at send time. Get the co-parent's user id from ofw_get_profile and pass recipientIds.` : `Draft ${draftRef}'s content was not readable from OurFamilyWizard or the local cache, so it cannot supply the missing fields. Pass them explicitly.`;
|
|
41527
|
+
throw new Error(`ofw_send_message requires ${missing}. ${hint}`);
|
|
41278
41528
|
}
|
|
41279
41529
|
const requestedReplyTo = args.replyToId ?? draftReplyToId ?? null;
|
|
41280
41530
|
let resolvedReplyTo = requestedReplyTo;
|
|
@@ -41288,7 +41538,7 @@ function registerMessageTools(server, client2, cacheProvider, attachmentIO) {
|
|
|
41288
41538
|
const parent = await cache.getMessage(resolvedReplyTo);
|
|
41289
41539
|
chainRootId = parent?.chainRootId ?? parent?.id ?? requestedReplyTo;
|
|
41290
41540
|
}
|
|
41291
|
-
const myFileIDs = args.myFileIDs ?? [];
|
|
41541
|
+
const myFileIDs = args.myFileIDs ?? serverDraft?.files ?? [];
|
|
41292
41542
|
const { id: newId, detail, raw } = await postMessageAndRefetch(client2, {
|
|
41293
41543
|
subject,
|
|
41294
41544
|
body,
|
|
@@ -41301,19 +41551,51 @@ function registerMessageTools(server, client2, cacheProvider, attachmentIO) {
|
|
|
41301
41551
|
let persisted = null;
|
|
41302
41552
|
let verifyNote = null;
|
|
41303
41553
|
let sentDraftKey = null;
|
|
41554
|
+
let threaded = false;
|
|
41555
|
+
let threadNote = null;
|
|
41304
41556
|
if (newId !== null) {
|
|
41305
41557
|
verifyNote = verifyWriteLanded("message", { subject, body }, detail);
|
|
41558
|
+
const echoed = threadedReplyTo(detail);
|
|
41559
|
+
if (resolvedReplyTo === null) {
|
|
41560
|
+
threaded = reportsThreaded(detail);
|
|
41561
|
+
} else if (reportsThreaded(detail)) {
|
|
41562
|
+
threaded = true;
|
|
41563
|
+
if (echoed !== null && echoed !== resolvedReplyTo) {
|
|
41564
|
+
threadNote = `NOTE: the sent message threads to ${echoed}, not the requested ${resolvedReplyTo} \u2014 OurFamilyWizard re-targeted the reply within the thread.`;
|
|
41565
|
+
}
|
|
41566
|
+
} else if (reportsUnthreaded(detail)) {
|
|
41567
|
+
threaded = false;
|
|
41568
|
+
threadNote = `WARNING: the sent message came back UNTHREADED \u2014 replyToId ${resolvedReplyTo} was posted but OurFamilyWizard reports no reply linkage on the sent record, so it went out as a new top-level conversation. Verify on ourfamilywizard.com.`;
|
|
41569
|
+
} else {
|
|
41570
|
+
threaded = true;
|
|
41571
|
+
}
|
|
41572
|
+
const storedRecipients = mapRecipients(detail.recipients);
|
|
41573
|
+
if (Array.isArray(detail.recipients) && detail.recipients.length > 0) {
|
|
41574
|
+
const landed = new Set(storedRecipients.map((r) => r.userId));
|
|
41575
|
+
const missingRecipients = recipientIds.filter((rid) => !landed.has(rid));
|
|
41576
|
+
if (missingRecipients.length > 0) {
|
|
41577
|
+
verifyNote = [
|
|
41578
|
+
verifyNote,
|
|
41579
|
+
`WARNING: the sent record does not list requested recipient id(s) ${missingRecipients.join(", ")}, so the send could not be fully confirmed. Verify on ourfamilywizard.com.`
|
|
41580
|
+
].filter((n) => n !== null).join("\n\n");
|
|
41581
|
+
}
|
|
41582
|
+
}
|
|
41306
41583
|
persisted = {
|
|
41307
41584
|
id: newId,
|
|
41308
41585
|
folder: "sent",
|
|
41309
41586
|
subject: detail.subject ?? subject,
|
|
41310
41587
|
fromUser: detail.from?.name ?? "",
|
|
41311
41588
|
sentAt: detail.date?.dateTime ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
41312
|
-
recipients:
|
|
41589
|
+
recipients: storedRecipients,
|
|
41313
41590
|
body: detail.body ?? body,
|
|
41314
41591
|
fetchedBodyAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
41315
|
-
|
|
41316
|
-
|
|
41592
|
+
// Prefer OFW's own echo of where the reply landed; keep what was
|
|
41593
|
+
// posted when OFW echoed nothing (sent rows feed findLatestReplyTip,
|
|
41594
|
+
// and a null would break the chain for a message that IS threaded).
|
|
41595
|
+
// A positively UNTHREADED send stores null — the chain link OFW says
|
|
41596
|
+
// does not exist must not be invented.
|
|
41597
|
+
replyToId: threaded ? echoed ?? resolvedReplyTo : null,
|
|
41598
|
+
chainRootId: threaded ? chainRootId : null,
|
|
41317
41599
|
listData: detail
|
|
41318
41600
|
};
|
|
41319
41601
|
await cache.upsertMessage(persisted);
|
|
@@ -41341,16 +41623,43 @@ function registerMessageTools(server, client2, cacheProvider, attachmentIO) {
|
|
|
41341
41623
|
}
|
|
41342
41624
|
}
|
|
41343
41625
|
let unconfirmedNote = null;
|
|
41626
|
+
let draftDeleted = false;
|
|
41627
|
+
let draftRetainedReason = null;
|
|
41344
41628
|
if (newId === null) {
|
|
41345
41629
|
const draftClause = draftRef !== void 0 ? `Draft ${draftRef} was NOT deleted \u2014 check` : "Check";
|
|
41346
41630
|
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.`;
|
|
41631
|
+
if (draftRef !== void 0) {
|
|
41632
|
+
draftRetainedReason = "the send could not be confirmed (OFW returned no message id), so the draft is your only reliable copy of the message";
|
|
41633
|
+
}
|
|
41347
41634
|
} else if (draftRef !== void 0) {
|
|
41348
|
-
|
|
41349
|
-
|
|
41635
|
+
if (verifyNote !== null) {
|
|
41636
|
+
draftRetainedReason = "the sent record could not be fully verified against what was posted (see WARNING above) \u2014 the draft is kept until you confirm the send on ourfamilywizard.com";
|
|
41637
|
+
} else if (!deleteOnSuccess) {
|
|
41638
|
+
draftRetainedReason = "deleteDraftOnSuccess:false \u2014 kept by request";
|
|
41639
|
+
} else {
|
|
41640
|
+
try {
|
|
41641
|
+
await deleteOFWMessages(client2, [draftRef]);
|
|
41642
|
+
await cache.deleteDraft(draftRef);
|
|
41643
|
+
draftDeleted = true;
|
|
41644
|
+
} catch (e) {
|
|
41645
|
+
draftRetainedReason = `the send succeeded but the draft delete failed (${e.message}) \u2014 remove it with ofw_delete_draft once you have verified the sent message`;
|
|
41646
|
+
}
|
|
41647
|
+
}
|
|
41350
41648
|
}
|
|
41351
|
-
const
|
|
41649
|
+
const retainNote = draftRef !== void 0 && newId !== null && !draftDeleted ? `NOTE: draft ${draftRef} was retained: ${draftRetainedReason}.` : null;
|
|
41650
|
+
const responseObj = persisted === null ? draftRef !== void 0 ? { sendConfirmed: false, draftDeleted: false, draftRetained: true, draftRetainedReason, raw } : raw : {
|
|
41651
|
+
sentMessageId: newId,
|
|
41652
|
+
draftKey: sentDraftKey,
|
|
41653
|
+
threaded,
|
|
41654
|
+
...draftRef !== void 0 ? {
|
|
41655
|
+
draftDeleted,
|
|
41656
|
+
...draftDeleted ? {} : { draftRetained: true, draftRetainedReason },
|
|
41657
|
+
previousId: draftRef
|
|
41658
|
+
} : {},
|
|
41659
|
+
...persisted
|
|
41660
|
+
};
|
|
41352
41661
|
const text = responseObj ? JSON.stringify(responseObj, null, 2) : "Message sent successfully.";
|
|
41353
|
-
const notes = [rewriteNote, verifyNote, unconfirmedNote].filter((n) => n !== null).join("\n\n");
|
|
41662
|
+
const notes = [guardNote, rewriteNote, verifyNote, threadNote, unconfirmedNote, retainNote].filter((n) => n !== null).join("\n\n");
|
|
41354
41663
|
return textResponse(notes ? `${notes}
|
|
41355
41664
|
|
|
41356
41665
|
${text}` : text);
|
|
@@ -41370,7 +41679,7 @@ ${text}` : text);
|
|
|
41370
41679
|
} catch (e) {
|
|
41371
41680
|
const reason = e.message;
|
|
41372
41681
|
if (force) {
|
|
41373
|
-
return { ok: true, note: `WARNING: force:true \u2014 proceeded with ${action} on draft ${draftId} even though its current state could not be read from OurFamilyWizard (${reason}). Any newer server-side version was destroyed and is NOT recoverable from this response
|
|
41682
|
+
return { ok: true, note: `WARNING: force:true \u2014 proceeded with ${action} on draft ${draftId} even though its current state could not be read from OurFamilyWizard (${reason}). Any newer server-side version was destroyed and is NOT recoverable from this response.`, server: void 0 };
|
|
41374
41683
|
}
|
|
41375
41684
|
return {
|
|
41376
41685
|
ok: false,
|
|
@@ -41385,7 +41694,7 @@ ${text}` : text);
|
|
|
41385
41694
|
const verdict = checkDraftFreshness({ server: server2, cached: cached2, expectedRevision });
|
|
41386
41695
|
if (verdict.verdict === "FRESH") {
|
|
41387
41696
|
const note = verdict.metadataOnly ? `NOTE: draft ${draftId} was treated as current for this ${action}. Since you read it, OurFamilyWizard normalized connector-authored metadata (${verdict.changedFields.join(", ")}); the subject, body and recipients are unchanged, so this is not a conflict.` : null;
|
|
41388
|
-
return { ok: true, note };
|
|
41697
|
+
return { ok: true, note, server: server2 };
|
|
41389
41698
|
}
|
|
41390
41699
|
if (force) {
|
|
41391
41700
|
console.error(`[ofw-mcp] WARNING: force:true overrode a ${verdict.verdict} verdict on draft ${draftId} (${action}). ${verdict.reason}`);
|
|
@@ -41398,7 +41707,8 @@ ${JSON.stringify(
|
|
|
41398
41707
|
{ overwrittenServerDraft: server2 === null ? null : { ...server2, revision: draftRevision(server2) } },
|
|
41399
41708
|
null,
|
|
41400
41709
|
2
|
|
41401
|
-
)}
|
|
41710
|
+
)}`,
|
|
41711
|
+
server: server2
|
|
41402
41712
|
};
|
|
41403
41713
|
}
|
|
41404
41714
|
return {
|
|
@@ -41413,17 +41723,31 @@ ${JSON.stringify(
|
|
|
41413
41723
|
};
|
|
41414
41724
|
}
|
|
41415
41725
|
server.registerTool("ofw_list_drafts", {
|
|
41416
|
-
description: 'List draft messages
|
|
41726
|
+
description: 'List draft messages, verified against OurFamilyWizard in ONE call: when the local drafts cache is not verified-fresh, a cheap drafts sync runs first by default (verify:true), so the answer is server-confirmed without a second call. Pass verify:false to answer purely from the cache (no OFW requests). Returns an explicit `complete` boolean describing the RESULT SET: true means "these are ALL the drafts on OurFamilyWizard as of freshness.asOf" \u2014 check it before saying "you have N drafts". Each draft carries its `draftKey` (stable across the create-then-delete churn of editing) when one is known. An empty result from a cache that is not verified-fresh is REFUSED (result:"UNVERIFIED_EMPTY"); pass autoRefresh:true to sync and answer instead.',
|
|
41417
41727
|
annotations: { readOnlyHint: false },
|
|
41418
41728
|
inputSchema: {
|
|
41419
41729
|
page: external_exports.number().int().min(1).describe("Page number (default 1)").optional(),
|
|
41420
41730
|
size: external_exports.number().int().min(1).describe("Drafts per page (default 50)").optional(),
|
|
41731
|
+
verify: external_exports.boolean().describe("Default true: when the drafts cache is not verified-fresh, run a drafts sync first (cheap \u2014 one list page plus one detail per draft) so the response is server-confirmed in one call. Set false to serve straight from the local cache with no OFW requests.").optional(),
|
|
41421
41732
|
autoRefresh: external_exports.boolean().describe(AUTO_REFRESH_DESC).optional()
|
|
41422
41733
|
}
|
|
41423
41734
|
}, async (args) => {
|
|
41424
41735
|
const page = args.page ?? 1;
|
|
41425
41736
|
const size = args.size ?? 50;
|
|
41426
41737
|
const cache = cacheProvider();
|
|
41738
|
+
let autoVerified = false;
|
|
41739
|
+
let verifyNote = null;
|
|
41740
|
+
if (args.verify ?? true) {
|
|
41741
|
+
const { cacheStatus } = await draftsFreshness(cache);
|
|
41742
|
+
if (cacheStatus !== "fresh") {
|
|
41743
|
+
try {
|
|
41744
|
+
await syncAll(client2, { folders: ["drafts"], maxRequests: getSyncMaxRequests() }, cache);
|
|
41745
|
+
autoVerified = await getDraftsCacheStatus(cache) === "fresh";
|
|
41746
|
+
} catch (e) {
|
|
41747
|
+
verifyNote = `The automatic drafts verification could not reach OurFamilyWizard (${e.message}). Answering from the local cache \u2014 the freshness block below labels its age, and an empty result will still be refused rather than reported as an absence.`;
|
|
41748
|
+
}
|
|
41749
|
+
}
|
|
41750
|
+
}
|
|
41427
41751
|
const { value, refreshed, unverifiedEmpty } = await guardedCacheRead({
|
|
41428
41752
|
client: client2,
|
|
41429
41753
|
cache,
|
|
@@ -41454,7 +41778,7 @@ ${JSON.stringify(
|
|
|
41454
41778
|
freshness: value.freshness,
|
|
41455
41779
|
refreshed,
|
|
41456
41780
|
remedy: 'Call ofw_sync_messages(folders:["drafts"]) and retry, re-call with autoRefresh:true, or use ofw_status(includeDraftInventory:true) for a single live answer.',
|
|
41457
|
-
extra: { page, size }
|
|
41781
|
+
extra: { page, size, ...verifyNote !== null ? { verifyNote } : {} }
|
|
41458
41782
|
});
|
|
41459
41783
|
}
|
|
41460
41784
|
const { drafts, total, freshness, serverConfirmed } = value;
|
|
@@ -41473,10 +41797,16 @@ ${JSON.stringify(
|
|
|
41473
41797
|
if (refreshed) {
|
|
41474
41798
|
payload.autoRefreshed = true;
|
|
41475
41799
|
}
|
|
41800
|
+
if (autoVerified) {
|
|
41801
|
+
payload.autoVerified = true;
|
|
41802
|
+
}
|
|
41803
|
+
if (verifyNote !== null) {
|
|
41804
|
+
payload.verifyNote = verifyNote;
|
|
41805
|
+
}
|
|
41476
41806
|
return jsonResponse(payload);
|
|
41477
41807
|
});
|
|
41478
41808
|
if (allowDrafts) server.registerTool("ofw_save_draft", {
|
|
41479
|
-
description: "Save a message as a draft in OurFamilyWizard.
|
|
41809
|
+
description: "Save a message as a draft in OurFamilyWizard. RECIPIENTS: OurFamilyWizard does NOT persist recipients on drafts \u2014 recipientIds are accepted but the saved draft comes back with none (documented OFW behavior, noted once in the response, not warned about; supply recipientIds at send time instead). IDENTITY: the response leads with `draftKey`, the stable identity that survives editing \u2014 key off it, because the `id` changes on EVERY edit (replacing a draft creates a NEW draft and deletes the old one; OFW's update-in-place endpoint silently no-ops, so we never use it). Pass messageId to replace an existing draft; the response.id will be the NEW id, and a transparency NOTE documents the swap and which fields were carried over. THREADING: if replyToId is provided, the cache may rewrite it to the latest reply in the thread (note included). The threading verdict is read from OFW's full echo (replyToId/inReplyTo/showContext) \u2014 a warning appears ONLY when the reply linkage was genuinely dropped or re-targeted, and the response's top-level replyToId/inReplyTo always agree with its listData. Attach files via myFileIDs (from ofw_upload_attachment). After saving, the tool re-fetches the draft from OFW, and the returned `revision` reflects that authoritative state (so it will match on your next edit). SAFETY: because replacing DESTROYS the old draft rather than merging, passing messageId first re-reads that draft from OFW and REFUSES the write if its subject/body/recipients changed since you read it (drafts edited in the OFW web app do not bump any timestamp, so the local cache can be silently behind). A pure replyToId normalization by OFW is NOT treated as a conflict. The refusal returns the current server body under serverBody \u2014 merge your edit into it and retry with expectedRevision.",
|
|
41480
41810
|
annotations: { readOnlyHint: false },
|
|
41481
41811
|
inputSchema: {
|
|
41482
41812
|
subject: external_exports.string().describe("Message subject"),
|
|
@@ -41530,12 +41860,13 @@ ${JSON.stringify(
|
|
|
41530
41860
|
let persisted = null;
|
|
41531
41861
|
let replaceNote = null;
|
|
41532
41862
|
let verifyNote = null;
|
|
41863
|
+
let recipientsNote = null;
|
|
41533
41864
|
let newRevision = null;
|
|
41534
41865
|
let draftKey = null;
|
|
41535
41866
|
const warnings = [];
|
|
41536
41867
|
if (newId !== null) {
|
|
41537
41868
|
verifyNote = verifyWriteLanded("draft", { subject: args.subject, body: args.body }, detail);
|
|
41538
|
-
const effectiveReplyTo = detail
|
|
41869
|
+
const effectiveReplyTo = threadedReplyTo(detail);
|
|
41539
41870
|
const storedRecipients = mapRecipients(detail.recipients);
|
|
41540
41871
|
persisted = {
|
|
41541
41872
|
id: newId,
|
|
@@ -41571,17 +41902,23 @@ ${JSON.stringify(
|
|
|
41571
41902
|
previousId: args.messageId ?? null,
|
|
41572
41903
|
recordedAt: now
|
|
41573
41904
|
});
|
|
41574
|
-
if (resolvedReplyTo !== null && effectiveReplyTo !== resolvedReplyTo) {
|
|
41905
|
+
if (resolvedReplyTo !== null && effectiveReplyTo !== resolvedReplyTo && reportsUnthreaded(detail)) {
|
|
41906
|
+
const rewrittenFrom = requestedReplyTo !== resolvedReplyTo ? ` (rewritten from ${requestedReplyTo})` : "";
|
|
41907
|
+
warnings.push(
|
|
41908
|
+
`replyToId was requested as ${resolvedReplyTo}${rewrittenFrom} but the saved draft came back with replyToId null \u2014 OurFamilyWizard did not thread this draft (its inReplyTo/showContext are empty). The subject and body were saved; only the reply linkage was dropped. If threading matters, verify on ourfamilywizard.com.`
|
|
41909
|
+
);
|
|
41910
|
+
} else if (resolvedReplyTo !== null && effectiveReplyTo !== null && effectiveReplyTo !== resolvedReplyTo) {
|
|
41575
41911
|
const rewrittenFrom = requestedReplyTo !== resolvedReplyTo ? ` (rewritten from ${requestedReplyTo})` : "";
|
|
41576
|
-
const outcome = effectiveReplyTo === null ? "OurFamilyWizard did not thread this draft (its inReplyTo/showContext will be empty). The subject and body were saved; only the reply linkage was dropped." : `OurFamilyWizard re-targeted the reply to message ${effectiveReplyTo} instead. The draft IS threaded \u2014 to that message, not the one requested \u2014 and the inReplyTo in this response reflects where it actually landed.`;
|
|
41577
41912
|
warnings.push(
|
|
41578
|
-
`replyToId was requested as ${resolvedReplyTo}${rewrittenFrom} but the saved draft came back with replyToId ${effectiveReplyTo
|
|
41913
|
+
`replyToId was requested as ${resolvedReplyTo}${rewrittenFrom} but the saved draft came back with replyToId ${effectiveReplyTo} \u2014 OurFamilyWizard re-targeted the reply to message ${effectiveReplyTo} instead. The draft IS threaded \u2014 to that message, not the one requested. If threading matters, verify on ourfamilywizard.com.`
|
|
41579
41914
|
);
|
|
41580
41915
|
}
|
|
41581
|
-
if (args.recipientIds !== void 0 && Array.isArray(detail.recipients)) {
|
|
41916
|
+
if (args.recipientIds !== void 0 && args.recipientIds.length > 0 && Array.isArray(detail.recipients)) {
|
|
41582
41917
|
const requested = [...new Set(args.recipientIds)].sort((a, b) => a - b);
|
|
41583
41918
|
const stored = [...new Set(storedRecipients.map((r) => r.userId))].sort((a, b) => a - b);
|
|
41584
|
-
if (
|
|
41919
|
+
if (stored.length === 0) {
|
|
41920
|
+
recipientsNote = "NOTE: OurFamilyWizard does not persist recipients on drafts \u2014 the recipientIds you passed were accepted but are not stored on the draft (documented OFW behavior, not an error; it also means a draft cannot be sent by accident). Supply recipientIds when you send: ofw_send_message requires them when the draft carries none.";
|
|
41921
|
+
} else if (requested.join(",") !== stored.join(",")) {
|
|
41585
41922
|
warnings.push(
|
|
41586
41923
|
`recipientIds were requested as [${requested.join(", ")}] but the saved draft has [${stored.join(", ")}]. Verify the recipients on ourfamilywizard.com.`
|
|
41587
41924
|
);
|
|
@@ -41600,28 +41937,26 @@ ${JSON.stringify(
|
|
|
41600
41937
|
try {
|
|
41601
41938
|
await deleteOFWMessages(client2, [args.messageId]);
|
|
41602
41939
|
await cache.deleteDraft(args.messageId);
|
|
41603
|
-
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.
|
|
41940
|
+
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. The draftKey is UNCHANGED \u2014 key off it rather than the volatile id, which changes on every edit. (OFW's update-in-place endpoint silently no-ops on subsequent updates, so we never use it.) Fields carried over to the new draft: subject, body, recipients (${persisted.recipients.length}), replyToId (${persisted.replyToId === null ? "none" : persisted.replyToId}), attachments (${myFileIDs.length}).${warnings.length > 0 ? " See warnings above for any field OurFamilyWizard did not carry over." : ""}`;
|
|
41604
41941
|
} catch (e) {
|
|
41605
41942
|
replaceNote = `WARNING: New draft ${newId} was created successfully, but the old draft ${args.messageId} could NOT be deleted: ${e.message}. BOTH drafts now exist on OurFamilyWizard and nothing was lost. Verify ${newId} reads correctly, then remove ${args.messageId} with ofw_delete_draft.`;
|
|
41606
41943
|
}
|
|
41607
41944
|
}
|
|
41608
41945
|
}
|
|
41609
41946
|
const responseObj = persisted !== null ? {
|
|
41947
|
+
draftKey,
|
|
41948
|
+
revision: newRevision,
|
|
41610
41949
|
...persisted,
|
|
41611
41950
|
inReplyTo: persisted.replyToId,
|
|
41612
|
-
revision: newRevision,
|
|
41613
|
-
// The id above is volatile — it changes on every edit. `draftKey` is
|
|
41614
|
-
// not: pass it to ofw_status to resolve the chain's CURRENT id, or to
|
|
41615
|
-
// find out that the draft was sent and when.
|
|
41616
|
-
draftKey,
|
|
41617
41951
|
previousId: args.messageId ?? null,
|
|
41618
41952
|
cacheStatus: "fresh",
|
|
41619
41953
|
serverConfirmed: true,
|
|
41620
|
-
...warnings.length > 0 ? { warnings } : {}
|
|
41954
|
+
...warnings.length > 0 ? { warnings } : {},
|
|
41955
|
+
...recipientsNote !== null ? { recipientsNote } : {}
|
|
41621
41956
|
} : raw;
|
|
41622
41957
|
const text = responseObj ? JSON.stringify(responseObj, null, 2) : "Draft saved.";
|
|
41623
41958
|
const warnNote = warnings.length > 0 ? `WARNING: ${warnings.join("\n\n")}` : null;
|
|
41624
|
-
const notes = [forceNote, rewriteNote, verifyNote, warnNote, replaceNote].filter((n) => n !== null).join("\n\n");
|
|
41959
|
+
const notes = [forceNote, rewriteNote, verifyNote, warnNote, recipientsNote, replaceNote].filter((n) => n !== null).join("\n\n");
|
|
41625
41960
|
return textResponse(notes ? `${notes}
|
|
41626
41961
|
|
|
41627
41962
|
${text}` : text);
|
|
@@ -42911,7 +43246,7 @@ var nodeCacheProvider = () => nodeCache ??= OFWCache.open(getCacheDbPath());
|
|
|
42911
43246
|
var nodeAttachmentIO = new NodeAttachmentIO();
|
|
42912
43247
|
await runMcp({
|
|
42913
43248
|
name: "ofw",
|
|
42914
|
-
version: "2.
|
|
43249
|
+
version: "2.10.0",
|
|
42915
43250
|
// x-release-please-version
|
|
42916
43251
|
deps: client,
|
|
42917
43252
|
tools: [
|