ofw-mcp 2.9.2 → 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 +223 -71
- 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 +1 -1
- package/server.json +2 -2
- package/skills/ofw/SKILL.md +5 -5
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
},
|
|
7
7
|
"metadata": {
|
|
8
8
|
"description": "OurFamilyWizard tools for Claude Code",
|
|
9
|
-
"version": "2.
|
|
9
|
+
"version": "2.10.0"
|
|
10
10
|
},
|
|
11
11
|
"plugins": [
|
|
12
12
|
{
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"displayName": "OurFamilyWizard",
|
|
15
15
|
"source": "./",
|
|
16
16
|
"description": "OurFamilyWizard co-parenting tools for Claude — messages, calendar, expenses, and journal via MCP",
|
|
17
|
-
"version": "2.
|
|
17
|
+
"version": "2.10.0",
|
|
18
18
|
"author": {
|
|
19
19
|
"name": "Chris Chall"
|
|
20
20
|
},
|
package/dist/bundle.js
CHANGED
|
@@ -38590,7 +38590,7 @@ async function loginWithPassword(username, password) {
|
|
|
38590
38590
|
// package.json
|
|
38591
38591
|
var package_default = {
|
|
38592
38592
|
name: "ofw-mcp",
|
|
38593
|
-
version: "2.
|
|
38593
|
+
version: "2.10.0",
|
|
38594
38594
|
license: "MIT",
|
|
38595
38595
|
mcpName: "io.github.chrischall/ofw-mcp",
|
|
38596
38596
|
description: "OurFamilyWizard MCP server for Claude \u2014 developed and maintained by AI (Claude Code)",
|
|
@@ -39099,6 +39099,16 @@ function mapRecipients(items) {
|
|
|
39099
39099
|
function hasRealView(recipients) {
|
|
39100
39100
|
return recipients.some((r) => r.viewedAt !== null && !r.viewedAt.startsWith("1970-01-01"));
|
|
39101
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
|
+
}
|
|
39102
39112
|
function scrapeSaysRead(listData) {
|
|
39103
39113
|
if (typeof listData !== "object" || listData === null) return false;
|
|
39104
39114
|
const ld = listData;
|
|
@@ -39413,7 +39423,12 @@ var DraftListItemSchema = external_exports.looseObject({
|
|
|
39413
39423
|
id: external_exports.number(),
|
|
39414
39424
|
subject: external_exports.string(),
|
|
39415
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.
|
|
39416
39430
|
replyToId: external_exports.number().nullable().optional(),
|
|
39431
|
+
inReplyTo: external_exports.number().nullable().optional(),
|
|
39417
39432
|
recipients: external_exports.array(ApiRecipientSchema).optional()
|
|
39418
39433
|
});
|
|
39419
39434
|
var DraftListResponseSchema = external_exports.looseObject({ data: external_exports.array(DraftListItemSchema).optional() });
|
|
@@ -39476,7 +39491,7 @@ async function syncDrafts(client2, draftsFolderId, store, budget) {
|
|
|
39476
39491
|
subject: detail.subject ?? item.subject ?? "(no subject)",
|
|
39477
39492
|
body: detail.body ?? "",
|
|
39478
39493
|
recipients: mapRecipients(item.recipients),
|
|
39479
|
-
replyToId: item
|
|
39494
|
+
replyToId: threadedReplyTo(item),
|
|
39480
39495
|
modifiedAt: item.date?.dateTime ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
39481
39496
|
listData: item
|
|
39482
39497
|
});
|
|
@@ -39777,8 +39792,18 @@ function draftRevision(d) {
|
|
|
39777
39792
|
var ServerDraftSchema = external_exports.looseObject({
|
|
39778
39793
|
subject: external_exports.string().optional(),
|
|
39779
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.
|
|
39780
39801
|
replyToId: external_exports.number().nullable().optional(),
|
|
39802
|
+
inReplyTo: external_exports.number().nullable().optional(),
|
|
39781
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(),
|
|
39782
39807
|
// Read for the LIFECYCLE answer (see tools/lifecycle.ts): which folder OFW
|
|
39783
39808
|
// itself says this id lives in right now. `existsOnServer` alone cannot
|
|
39784
39809
|
// distinguish "still a draft" from "was sent" — a sent draft still exists.
|
|
@@ -39821,8 +39846,9 @@ async function fetchMessageSnapshot(client2, id) {
|
|
|
39821
39846
|
content: {
|
|
39822
39847
|
subject: detail.subject ?? "",
|
|
39823
39848
|
body: detail.body ?? "",
|
|
39824
|
-
replyToId: detail
|
|
39825
|
-
recipients: mapRecipients(detail.recipients)
|
|
39849
|
+
replyToId: threadedReplyTo(detail),
|
|
39850
|
+
recipients: mapRecipients(detail.recipients),
|
|
39851
|
+
...detail.files !== void 0 ? { files: detail.files } : {}
|
|
39826
39852
|
},
|
|
39827
39853
|
folderId: detail.folder?.id === void 0 ? null : String(detail.folder.id),
|
|
39828
39854
|
folderName: detail.folder?.name ?? null,
|
|
@@ -39952,13 +39978,24 @@ async function ensureFolderIdMap(client2, store) {
|
|
|
39952
39978
|
return { map: cached2, requests: 1 };
|
|
39953
39979
|
}
|
|
39954
39980
|
}
|
|
39981
|
+
var STATE_BY_FOLDER_NAME = /* @__PURE__ */ new Map([
|
|
39982
|
+
["drafts", "draft"],
|
|
39983
|
+
["sent", "sent"],
|
|
39984
|
+
["sent messages", "sent"],
|
|
39985
|
+
["inbox", "received"]
|
|
39986
|
+
]);
|
|
39955
39987
|
function classifyState(snapshot, map2) {
|
|
39956
39988
|
if (snapshot === null) return "deleted";
|
|
39957
|
-
const { folderId } = snapshot;
|
|
39958
|
-
if (folderId
|
|
39959
|
-
|
|
39960
|
-
|
|
39961
|
-
|
|
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
|
+
}
|
|
39962
39999
|
return "unknown";
|
|
39963
40000
|
}
|
|
39964
40001
|
function probeWouldStamp(cachedDraft, cachedMessage) {
|
|
@@ -41100,13 +41137,25 @@ var SentDetailSchema = external_exports.looseObject({
|
|
|
41100
41137
|
body: external_exports.string().optional(),
|
|
41101
41138
|
date: DateSchema.optional(),
|
|
41102
41139
|
from: external_exports.looseObject({ name: external_exports.string().optional() }).optional(),
|
|
41103
|
-
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()
|
|
41104
41147
|
});
|
|
41105
41148
|
var SavedDraftDetailSchema = external_exports.looseObject({
|
|
41106
41149
|
subject: external_exports.string().optional(),
|
|
41107
41150
|
body: external_exports.string().optional(),
|
|
41108
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.
|
|
41109
41156
|
replyToId: external_exports.number().nullable().optional(),
|
|
41157
|
+
inReplyTo: external_exports.number().nullable().optional(),
|
|
41158
|
+
showContext: external_exports.boolean().optional(),
|
|
41110
41159
|
recipients: external_exports.array(ApiRecipientSchema).optional(),
|
|
41111
41160
|
// Read to audit whether requested myFileIDs actually attached (Defect 3).
|
|
41112
41161
|
files: external_exports.array(external_exports.number()).optional()
|
|
@@ -41305,6 +41354,11 @@ function registerMessageTools(server, client2, cacheProvider, attachmentIO) {
|
|
|
41305
41354
|
if (draftRow !== null) {
|
|
41306
41355
|
const { freshness: freshness2, serverConfirmed, cacheStatus } = await draftsFreshness(cache);
|
|
41307
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,
|
|
41308
41362
|
id: draftRow.id,
|
|
41309
41363
|
folder: "drafts",
|
|
41310
41364
|
subject: draftRow.subject,
|
|
@@ -41321,13 +41375,9 @@ function registerMessageTools(server, client2, cacheProvider, attachmentIO) {
|
|
|
41321
41375
|
listData: draftRow.listData,
|
|
41322
41376
|
attachments: [],
|
|
41323
41377
|
// Concurrency token — pass as expectedRevision to ofw_save_draft /
|
|
41324
|
-
// ofw_delete_draft to assert you are
|
|
41378
|
+
// ofw_delete_draft / ofw_send_message to assert you are acting on
|
|
41379
|
+
// THIS version.
|
|
41325
41380
|
revision: draftRevision(draftRow),
|
|
41326
|
-
// Stable logical identity. Survives the create-then-delete id churn of
|
|
41327
|
-
// editing AND the transition to sent — pass it to ofw_status to ask
|
|
41328
|
-
// "what happened to the thing I was working on?". Null when this draft
|
|
41329
|
-
// was never written through this tool (e.g. authored in the web app).
|
|
41330
|
-
draftKey: (await cache.getDraftLineageById(draftRow.id))?.draftKey ?? null,
|
|
41331
41381
|
cacheStatus,
|
|
41332
41382
|
// False = this draft's existence and unsent status are remembered from
|
|
41333
41383
|
// a cache, not confirmed on OFW. Call ofw_check_freshness before
|
|
@@ -41410,16 +41460,19 @@ function registerMessageTools(server, client2, cacheProvider, attachmentIO) {
|
|
|
41410
41460
|
return jsonResponse({ ...withReadState(row), attachments, freshness });
|
|
41411
41461
|
});
|
|
41412
41462
|
if (allowSend) server.registerTool("ofw_send_message", {
|
|
41413
|
-
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.",
|
|
41414
41464
|
annotations: { destructiveHint: true },
|
|
41415
41465
|
inputSchema: {
|
|
41416
|
-
subject: external_exports.string().describe("Message subject. Required unless messageId
|
|
41417
|
-
body: external_exports.string().describe("Message body text. Required unless messageId
|
|
41418
|
-
recipientIds: external_exports.array(external_exports.number()).describe("Array of recipient user IDs (get from ofw_get_profile).
|
|
41419
|
-
replyToId: external_exports.number().describe("ID of the message being replied to").optional(),
|
|
41420
|
-
|
|
41421
|
-
|
|
41422
|
-
|
|
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()
|
|
41423
41476
|
}
|
|
41424
41477
|
}, async (args) => {
|
|
41425
41478
|
if (args.messageId !== void 0 && args.draftId !== void 0 && args.messageId !== args.draftId) {
|
|
@@ -41427,37 +41480,51 @@ function registerMessageTools(server, client2, cacheProvider, attachmentIO) {
|
|
|
41427
41480
|
}
|
|
41428
41481
|
const draftRef = args.messageId ?? args.draftId;
|
|
41429
41482
|
const cache = cacheProvider();
|
|
41483
|
+
const deleteOnSuccess = args.deleteDraftOnSuccess ?? true;
|
|
41430
41484
|
let subject = args.subject;
|
|
41431
41485
|
let body = args.body;
|
|
41432
41486
|
let recipientIds = args.recipientIds;
|
|
41433
41487
|
let draftReplyToId = null;
|
|
41434
|
-
let
|
|
41435
|
-
let
|
|
41488
|
+
let guardNote = null;
|
|
41489
|
+
let serverDraft;
|
|
41436
41490
|
if (draftRef !== void 0) {
|
|
41437
|
-
|
|
41438
|
-
const
|
|
41439
|
-
if (
|
|
41440
|
-
|
|
41441
|
-
|
|
41442
|
-
|
|
41443
|
-
|
|
41444
|
-
|
|
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
|
+
}
|
|
41445
41518
|
}
|
|
41446
41519
|
}
|
|
41447
41520
|
if (subject === void 0 || body === void 0 || recipientIds === void 0) {
|
|
41448
|
-
if (draftLookupAttempted && !draftFound) {
|
|
41449
|
-
throw new Error(
|
|
41450
|
-
`draft ${draftRef} not found in local cache. Call ofw_sync_messages first, or supply subject/body/recipientIds explicitly.`
|
|
41451
|
-
);
|
|
41452
|
-
}
|
|
41453
41521
|
const missing = [
|
|
41454
41522
|
subject === void 0 ? "subject" : null,
|
|
41455
41523
|
body === void 0 ? "body" : null,
|
|
41456
41524
|
recipientIds === void 0 ? "recipientIds" : null
|
|
41457
41525
|
].filter((n) => n !== null).join(", ");
|
|
41458
|
-
|
|
41459
|
-
|
|
41460
|
-
);
|
|
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}`);
|
|
41461
41528
|
}
|
|
41462
41529
|
const requestedReplyTo = args.replyToId ?? draftReplyToId ?? null;
|
|
41463
41530
|
let resolvedReplyTo = requestedReplyTo;
|
|
@@ -41471,7 +41538,7 @@ function registerMessageTools(server, client2, cacheProvider, attachmentIO) {
|
|
|
41471
41538
|
const parent = await cache.getMessage(resolvedReplyTo);
|
|
41472
41539
|
chainRootId = parent?.chainRootId ?? parent?.id ?? requestedReplyTo;
|
|
41473
41540
|
}
|
|
41474
|
-
const myFileIDs = args.myFileIDs ?? [];
|
|
41541
|
+
const myFileIDs = args.myFileIDs ?? serverDraft?.files ?? [];
|
|
41475
41542
|
const { id: newId, detail, raw } = await postMessageAndRefetch(client2, {
|
|
41476
41543
|
subject,
|
|
41477
41544
|
body,
|
|
@@ -41484,19 +41551,51 @@ function registerMessageTools(server, client2, cacheProvider, attachmentIO) {
|
|
|
41484
41551
|
let persisted = null;
|
|
41485
41552
|
let verifyNote = null;
|
|
41486
41553
|
let sentDraftKey = null;
|
|
41554
|
+
let threaded = false;
|
|
41555
|
+
let threadNote = null;
|
|
41487
41556
|
if (newId !== null) {
|
|
41488
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
|
+
}
|
|
41489
41583
|
persisted = {
|
|
41490
41584
|
id: newId,
|
|
41491
41585
|
folder: "sent",
|
|
41492
41586
|
subject: detail.subject ?? subject,
|
|
41493
41587
|
fromUser: detail.from?.name ?? "",
|
|
41494
41588
|
sentAt: detail.date?.dateTime ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
41495
|
-
recipients:
|
|
41589
|
+
recipients: storedRecipients,
|
|
41496
41590
|
body: detail.body ?? body,
|
|
41497
41591
|
fetchedBodyAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
41498
|
-
|
|
41499
|
-
|
|
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,
|
|
41500
41599
|
listData: detail
|
|
41501
41600
|
};
|
|
41502
41601
|
await cache.upsertMessage(persisted);
|
|
@@ -41524,16 +41623,43 @@ function registerMessageTools(server, client2, cacheProvider, attachmentIO) {
|
|
|
41524
41623
|
}
|
|
41525
41624
|
}
|
|
41526
41625
|
let unconfirmedNote = null;
|
|
41626
|
+
let draftDeleted = false;
|
|
41627
|
+
let draftRetainedReason = null;
|
|
41527
41628
|
if (newId === null) {
|
|
41528
41629
|
const draftClause = draftRef !== void 0 ? `Draft ${draftRef} was NOT deleted \u2014 check` : "Check";
|
|
41529
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
|
+
}
|
|
41530
41634
|
} else if (draftRef !== void 0) {
|
|
41531
|
-
|
|
41532
|
-
|
|
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
|
+
}
|
|
41533
41648
|
}
|
|
41534
|
-
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
|
+
};
|
|
41535
41661
|
const text = responseObj ? JSON.stringify(responseObj, null, 2) : "Message sent successfully.";
|
|
41536
|
-
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");
|
|
41537
41663
|
return textResponse(notes ? `${notes}
|
|
41538
41664
|
|
|
41539
41665
|
${text}` : text);
|
|
@@ -41553,7 +41679,7 @@ ${text}` : text);
|
|
|
41553
41679
|
} catch (e) {
|
|
41554
41680
|
const reason = e.message;
|
|
41555
41681
|
if (force) {
|
|
41556
|
-
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 };
|
|
41557
41683
|
}
|
|
41558
41684
|
return {
|
|
41559
41685
|
ok: false,
|
|
@@ -41568,7 +41694,7 @@ ${text}` : text);
|
|
|
41568
41694
|
const verdict = checkDraftFreshness({ server: server2, cached: cached2, expectedRevision });
|
|
41569
41695
|
if (verdict.verdict === "FRESH") {
|
|
41570
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;
|
|
41571
|
-
return { ok: true, note };
|
|
41697
|
+
return { ok: true, note, server: server2 };
|
|
41572
41698
|
}
|
|
41573
41699
|
if (force) {
|
|
41574
41700
|
console.error(`[ofw-mcp] WARNING: force:true overrode a ${verdict.verdict} verdict on draft ${draftId} (${action}). ${verdict.reason}`);
|
|
@@ -41581,7 +41707,8 @@ ${JSON.stringify(
|
|
|
41581
41707
|
{ overwrittenServerDraft: server2 === null ? null : { ...server2, revision: draftRevision(server2) } },
|
|
41582
41708
|
null,
|
|
41583
41709
|
2
|
|
41584
|
-
)}
|
|
41710
|
+
)}`,
|
|
41711
|
+
server: server2
|
|
41585
41712
|
};
|
|
41586
41713
|
}
|
|
41587
41714
|
return {
|
|
@@ -41596,17 +41723,31 @@ ${JSON.stringify(
|
|
|
41596
41723
|
};
|
|
41597
41724
|
}
|
|
41598
41725
|
server.registerTool("ofw_list_drafts", {
|
|
41599
|
-
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.',
|
|
41600
41727
|
annotations: { readOnlyHint: false },
|
|
41601
41728
|
inputSchema: {
|
|
41602
41729
|
page: external_exports.number().int().min(1).describe("Page number (default 1)").optional(),
|
|
41603
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(),
|
|
41604
41732
|
autoRefresh: external_exports.boolean().describe(AUTO_REFRESH_DESC).optional()
|
|
41605
41733
|
}
|
|
41606
41734
|
}, async (args) => {
|
|
41607
41735
|
const page = args.page ?? 1;
|
|
41608
41736
|
const size = args.size ?? 50;
|
|
41609
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
|
+
}
|
|
41610
41751
|
const { value, refreshed, unverifiedEmpty } = await guardedCacheRead({
|
|
41611
41752
|
client: client2,
|
|
41612
41753
|
cache,
|
|
@@ -41637,7 +41778,7 @@ ${JSON.stringify(
|
|
|
41637
41778
|
freshness: value.freshness,
|
|
41638
41779
|
refreshed,
|
|
41639
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.',
|
|
41640
|
-
extra: { page, size }
|
|
41781
|
+
extra: { page, size, ...verifyNote !== null ? { verifyNote } : {} }
|
|
41641
41782
|
});
|
|
41642
41783
|
}
|
|
41643
41784
|
const { drafts, total, freshness, serverConfirmed } = value;
|
|
@@ -41656,10 +41797,16 @@ ${JSON.stringify(
|
|
|
41656
41797
|
if (refreshed) {
|
|
41657
41798
|
payload.autoRefreshed = true;
|
|
41658
41799
|
}
|
|
41800
|
+
if (autoVerified) {
|
|
41801
|
+
payload.autoVerified = true;
|
|
41802
|
+
}
|
|
41803
|
+
if (verifyNote !== null) {
|
|
41804
|
+
payload.verifyNote = verifyNote;
|
|
41805
|
+
}
|
|
41659
41806
|
return jsonResponse(payload);
|
|
41660
41807
|
});
|
|
41661
41808
|
if (allowDrafts) server.registerTool("ofw_save_draft", {
|
|
41662
|
-
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.",
|
|
41663
41810
|
annotations: { readOnlyHint: false },
|
|
41664
41811
|
inputSchema: {
|
|
41665
41812
|
subject: external_exports.string().describe("Message subject"),
|
|
@@ -41713,12 +41860,13 @@ ${JSON.stringify(
|
|
|
41713
41860
|
let persisted = null;
|
|
41714
41861
|
let replaceNote = null;
|
|
41715
41862
|
let verifyNote = null;
|
|
41863
|
+
let recipientsNote = null;
|
|
41716
41864
|
let newRevision = null;
|
|
41717
41865
|
let draftKey = null;
|
|
41718
41866
|
const warnings = [];
|
|
41719
41867
|
if (newId !== null) {
|
|
41720
41868
|
verifyNote = verifyWriteLanded("draft", { subject: args.subject, body: args.body }, detail);
|
|
41721
|
-
const effectiveReplyTo = detail
|
|
41869
|
+
const effectiveReplyTo = threadedReplyTo(detail);
|
|
41722
41870
|
const storedRecipients = mapRecipients(detail.recipients);
|
|
41723
41871
|
persisted = {
|
|
41724
41872
|
id: newId,
|
|
@@ -41754,17 +41902,23 @@ ${JSON.stringify(
|
|
|
41754
41902
|
previousId: args.messageId ?? null,
|
|
41755
41903
|
recordedAt: now
|
|
41756
41904
|
});
|
|
41757
|
-
if (resolvedReplyTo !== null && effectiveReplyTo !== resolvedReplyTo) {
|
|
41905
|
+
if (resolvedReplyTo !== null && effectiveReplyTo !== resolvedReplyTo && reportsUnthreaded(detail)) {
|
|
41758
41906
|
const rewrittenFrom = requestedReplyTo !== resolvedReplyTo ? ` (rewritten from ${requestedReplyTo})` : "";
|
|
41759
|
-
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.`;
|
|
41760
41907
|
warnings.push(
|
|
41761
|
-
`replyToId was requested as ${resolvedReplyTo}${rewrittenFrom} but the saved draft came back with replyToId
|
|
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) {
|
|
41911
|
+
const rewrittenFrom = requestedReplyTo !== resolvedReplyTo ? ` (rewritten from ${requestedReplyTo})` : "";
|
|
41912
|
+
warnings.push(
|
|
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.`
|
|
41762
41914
|
);
|
|
41763
41915
|
}
|
|
41764
|
-
if (args.recipientIds !== void 0 && Array.isArray(detail.recipients)) {
|
|
41916
|
+
if (args.recipientIds !== void 0 && args.recipientIds.length > 0 && Array.isArray(detail.recipients)) {
|
|
41765
41917
|
const requested = [...new Set(args.recipientIds)].sort((a, b) => a - b);
|
|
41766
41918
|
const stored = [...new Set(storedRecipients.map((r) => r.userId))].sort((a, b) => a - b);
|
|
41767
|
-
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(",")) {
|
|
41768
41922
|
warnings.push(
|
|
41769
41923
|
`recipientIds were requested as [${requested.join(", ")}] but the saved draft has [${stored.join(", ")}]. Verify the recipients on ourfamilywizard.com.`
|
|
41770
41924
|
);
|
|
@@ -41783,28 +41937,26 @@ ${JSON.stringify(
|
|
|
41783
41937
|
try {
|
|
41784
41938
|
await deleteOFWMessages(client2, [args.messageId]);
|
|
41785
41939
|
await cache.deleteDraft(args.messageId);
|
|
41786
|
-
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." : ""}`;
|
|
41787
41941
|
} catch (e) {
|
|
41788
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.`;
|
|
41789
41943
|
}
|
|
41790
41944
|
}
|
|
41791
41945
|
}
|
|
41792
41946
|
const responseObj = persisted !== null ? {
|
|
41947
|
+
draftKey,
|
|
41948
|
+
revision: newRevision,
|
|
41793
41949
|
...persisted,
|
|
41794
41950
|
inReplyTo: persisted.replyToId,
|
|
41795
|
-
revision: newRevision,
|
|
41796
|
-
// The id above is volatile — it changes on every edit. `draftKey` is
|
|
41797
|
-
// not: pass it to ofw_status to resolve the chain's CURRENT id, or to
|
|
41798
|
-
// find out that the draft was sent and when.
|
|
41799
|
-
draftKey,
|
|
41800
41951
|
previousId: args.messageId ?? null,
|
|
41801
41952
|
cacheStatus: "fresh",
|
|
41802
41953
|
serverConfirmed: true,
|
|
41803
|
-
...warnings.length > 0 ? { warnings } : {}
|
|
41954
|
+
...warnings.length > 0 ? { warnings } : {},
|
|
41955
|
+
...recipientsNote !== null ? { recipientsNote } : {}
|
|
41804
41956
|
} : raw;
|
|
41805
41957
|
const text = responseObj ? JSON.stringify(responseObj, null, 2) : "Draft saved.";
|
|
41806
41958
|
const warnNote = warnings.length > 0 ? `WARNING: ${warnings.join("\n\n")}` : null;
|
|
41807
|
-
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");
|
|
41808
41960
|
return textResponse(notes ? `${notes}
|
|
41809
41961
|
|
|
41810
41962
|
${text}` : text);
|
|
@@ -43094,7 +43246,7 @@ var nodeCacheProvider = () => nodeCache ??= OFWCache.open(getCacheDbPath());
|
|
|
43094
43246
|
var nodeAttachmentIO = new NodeAttachmentIO();
|
|
43095
43247
|
await runMcp({
|
|
43096
43248
|
name: "ofw",
|
|
43097
|
-
version: "2.
|
|
43249
|
+
version: "2.10.0",
|
|
43098
43250
|
// x-release-please-version
|
|
43099
43251
|
deps: client,
|
|
43100
43252
|
tools: [
|
package/dist/index.js
CHANGED
|
@@ -35,7 +35,7 @@ const nodeAttachmentIO = new NodeAttachmentIO();
|
|
|
35
35
|
// always succeeds before any credential check runs.
|
|
36
36
|
await runMcp({
|
|
37
37
|
name: 'ofw',
|
|
38
|
-
version: '2.
|
|
38
|
+
version: '2.10.0', // x-release-please-version
|
|
39
39
|
deps: client,
|
|
40
40
|
tools: [
|
|
41
41
|
registerUserTools,
|
package/dist/sync.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { ApiRecipientSchema, hasRealView, mapRecipients } from './tools/_shared.js';
|
|
2
|
+
import { ApiRecipientSchema, hasRealView, mapRecipients, threadedReplyTo } from './tools/_shared.js';
|
|
3
3
|
import { parseLenient } from '@chrischall/mcp-utils';
|
|
4
4
|
// Each OFW message detail returns `files: [fileId, ...]`. We fetch the metadata
|
|
5
5
|
// for each file id (cheap JSON call) so the model can see filenames/mime types
|
|
@@ -347,7 +347,12 @@ const DraftListItemSchema = z.looseObject({
|
|
|
347
347
|
id: z.number(),
|
|
348
348
|
subject: z.string(),
|
|
349
349
|
date: z.looseObject({ dateTime: z.string() }),
|
|
350
|
+
// Both spellings of the threading echo — OFW reports the reply target as
|
|
351
|
+
// `inReplyTo` (with showContext) on list payloads where `replyToId` is null.
|
|
352
|
+
// The cached row must derive the SAME value ofw_save_draft derived from the
|
|
353
|
+
// detail, or the content revision drifts between a save and the next sync.
|
|
350
354
|
replyToId: z.number().nullable().optional(),
|
|
355
|
+
inReplyTo: z.number().nullable().optional(),
|
|
351
356
|
recipients: z.array(ApiRecipientSchema).optional(),
|
|
352
357
|
});
|
|
353
358
|
const DraftListResponseSchema = z.looseObject({ data: z.array(DraftListItemSchema).optional() });
|
|
@@ -451,7 +456,7 @@ export async function syncDrafts(client, draftsFolderId, store, budget) {
|
|
|
451
456
|
subject: detail.subject ?? item.subject ?? '(no subject)',
|
|
452
457
|
body: detail.body ?? '',
|
|
453
458
|
recipients: mapRecipients(item.recipients),
|
|
454
|
-
replyToId: item
|
|
459
|
+
replyToId: threadedReplyTo(item),
|
|
455
460
|
modifiedAt: item.date?.dateTime ?? new Date().toISOString(),
|
|
456
461
|
listData: item,
|
|
457
462
|
});
|