ofw-mcp 2.6.3 → 2.6.4
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 +34 -7
- package/dist/index.js +1 -1
- package/dist/tools/_shared.js +68 -2
- package/dist/tools/messages.js +8 -4
- package/package.json +1 -1
- package/server.json +2 -2
- package/skills/ofw-fpx/references/requests.md +1 -1
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
},
|
|
7
7
|
"metadata": {
|
|
8
8
|
"description": "OurFamilyWizard tools for Claude Code",
|
|
9
|
-
"version": "2.6.
|
|
9
|
+
"version": "2.6.4"
|
|
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.6.
|
|
17
|
+
"version": "2.6.4",
|
|
18
18
|
"author": {
|
|
19
19
|
"name": "Chris Chall"
|
|
20
20
|
},
|
package/dist/bundle.js
CHANGED
|
@@ -38410,7 +38410,7 @@ async function loginWithPassword(username, password) {
|
|
|
38410
38410
|
// package.json
|
|
38411
38411
|
var package_default = {
|
|
38412
38412
|
name: "ofw-mcp",
|
|
38413
|
-
version: "2.6.
|
|
38413
|
+
version: "2.6.4",
|
|
38414
38414
|
license: "MIT",
|
|
38415
38415
|
mcpName: "io.github.chrischall/ofw-mcp",
|
|
38416
38416
|
description: "OurFamilyWizard MCP server for Claude \u2014 developed and maintained by AI (Claude Code)",
|
|
@@ -38696,19 +38696,46 @@ var client = new OFWClient();
|
|
|
38696
38696
|
var jsonResponse = textResult;
|
|
38697
38697
|
var textResponse = rawTextResult;
|
|
38698
38698
|
var ApiRecipientSchema = external_exports.looseObject({
|
|
38699
|
-
|
|
38699
|
+
// Live OFW payloads key the recipient's id as `userId` (verified against a
|
|
38700
|
+
// real /pub/v3/messages record: `recipients[].user.userId === 3039201`). An
|
|
38701
|
+
// earlier guess read `id`, which is absent — so every normalized recipient
|
|
38702
|
+
// came out with `userId: 0`, breaking any "find my own recipient" match. Both
|
|
38703
|
+
// are accepted (userId first, id fallback) so a backend that ever returns `id`
|
|
38704
|
+
// still resolves.
|
|
38705
|
+
user: external_exports.looseObject({
|
|
38706
|
+
userId: external_exports.number().optional(),
|
|
38707
|
+
id: external_exports.number().optional(),
|
|
38708
|
+
name: external_exports.string().optional()
|
|
38709
|
+
}).optional(),
|
|
38700
38710
|
viewed: external_exports.looseObject({ dateTime: external_exports.string() }).nullable().optional()
|
|
38701
38711
|
});
|
|
38702
38712
|
function mapRecipients(items) {
|
|
38703
38713
|
return (items ?? []).map((r) => {
|
|
38704
38714
|
const dt = r.viewed?.dateTime;
|
|
38705
38715
|
const viewedAt = typeof dt === "string" && !dt.startsWith("1970-01-01") ? dt : null;
|
|
38706
|
-
return { userId: r.user?.id ?? 0, name: r.user?.name ?? "", viewedAt };
|
|
38716
|
+
return { userId: r.user?.userId ?? r.user?.id ?? 0, name: r.user?.name ?? "", viewedAt };
|
|
38707
38717
|
});
|
|
38708
38718
|
}
|
|
38709
38719
|
function hasRealView(recipients) {
|
|
38710
38720
|
return recipients.some((r) => r.viewedAt !== null && !r.viewedAt.startsWith("1970-01-01"));
|
|
38711
38721
|
}
|
|
38722
|
+
function scrapeSaysRead(listData) {
|
|
38723
|
+
if (typeof listData !== "object" || listData === null) return false;
|
|
38724
|
+
const ld = listData;
|
|
38725
|
+
return ld.read === true || ld.showNeverViewed === false;
|
|
38726
|
+
}
|
|
38727
|
+
function deriveRead(row, selfUserId) {
|
|
38728
|
+
if (row.folder === "inbox") {
|
|
38729
|
+
const viewed = selfUserId !== void 0 ? row.recipients.some((r) => r.userId === selfUserId && r.viewedAt !== null) : row.recipients.some((r) => r.viewedAt !== null);
|
|
38730
|
+
return viewed || row.fetchedBodyAt !== null || scrapeSaysRead(row.listData);
|
|
38731
|
+
}
|
|
38732
|
+
return row.recipients.some((r) => r.viewedAt !== null) || scrapeSaysRead(row.listData);
|
|
38733
|
+
}
|
|
38734
|
+
function withReadState(row, selfUserId) {
|
|
38735
|
+
const read = deriveRead(row, selfUserId);
|
|
38736
|
+
const listData = typeof row.listData === "object" && row.listData !== null ? { ...row.listData, read, showNeverViewed: !read } : row.listData;
|
|
38737
|
+
return { ...row, read, listData };
|
|
38738
|
+
}
|
|
38712
38739
|
var expandPath2 = expandPath;
|
|
38713
38740
|
function verifyWriteLanded(kind, sent, persisted) {
|
|
38714
38741
|
const mismatches = [];
|
|
@@ -39239,7 +39266,7 @@ function registerMessageTools(server, client2, cacheProvider, attachmentIO) {
|
|
|
39239
39266
|
const cache = cacheProvider();
|
|
39240
39267
|
const filter = { folder, since: args.since, until: args.until, q: args.q };
|
|
39241
39268
|
const total = await cache.countMessages(filter);
|
|
39242
|
-
const messages = await cache.listMessages({ ...filter, page, size });
|
|
39269
|
+
const messages = (await cache.listMessages({ ...filter, page, size })).map((m) => withReadState(m));
|
|
39243
39270
|
const payload = { messages, total, page, size };
|
|
39244
39271
|
if (total === 0) {
|
|
39245
39272
|
payload.note = "No messages match these filters. If you expected results, check ofw_sync_messages was run, or relax the filters.";
|
|
@@ -39312,7 +39339,7 @@ function registerMessageTools(server, client2, cacheProvider, attachmentIO) {
|
|
|
39312
39339
|
} catch {
|
|
39313
39340
|
}
|
|
39314
39341
|
}
|
|
39315
|
-
return jsonResponse({ ...row2, attachments: attachments2 });
|
|
39342
|
+
return jsonResponse({ ...withReadState(row2), attachments: attachments2 });
|
|
39316
39343
|
}
|
|
39317
39344
|
const detail = parseLenient(
|
|
39318
39345
|
MessageDetailSchema,
|
|
@@ -39344,7 +39371,7 @@ function registerMessageTools(server, client2, cacheProvider, attachmentIO) {
|
|
|
39344
39371
|
await fetchAttachmentMetaForMessage(client2, detail.id, detail.files, cache);
|
|
39345
39372
|
}
|
|
39346
39373
|
const attachments = await cache.listAttachmentsForMessage(detail.id);
|
|
39347
|
-
return jsonResponse({ ...row, attachments });
|
|
39374
|
+
return jsonResponse({ ...withReadState(row), attachments });
|
|
39348
39375
|
});
|
|
39349
39376
|
if (allowSend) server.registerTool("ofw_send_message", {
|
|
39350
39377
|
description: "Send a message via OurFamilyWizard. To send an existing draft, pass messageId \u2014 subject/body/recipientIds become optional overrides (missing fields default to the draft's cached values) and the draft is deleted after sending. To send a fresh message, supply subject/body/recipientIds directly. draftId is the legacy spelling of messageId and works the same way. If replyToId is provided, the cache may rewrite it to the latest reply in the same thread (a note is included in the response when this happens). Attach files by passing their fileIds (from ofw_upload_attachment) in myFileIDs. After sending, the tool re-fetches the message from OFW to populate the local cache and link attachments to the new message id.",
|
|
@@ -40548,7 +40575,7 @@ var nodeCacheProvider = () => nodeCache ??= OFWCache.open(getCacheDbPath());
|
|
|
40548
40575
|
var nodeAttachmentIO = new NodeAttachmentIO();
|
|
40549
40576
|
await runMcp({
|
|
40550
40577
|
name: "ofw",
|
|
40551
|
-
version: "2.6.
|
|
40578
|
+
version: "2.6.4",
|
|
40552
40579
|
// x-release-please-version
|
|
40553
40580
|
deps: client,
|
|
40554
40581
|
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.6.
|
|
38
|
+
version: '2.6.4', // x-release-please-version
|
|
39
39
|
deps: client,
|
|
40
40
|
tools: [
|
|
41
41
|
registerUserTools,
|
package/dist/tools/_shared.js
CHANGED
|
@@ -10,7 +10,17 @@ export const textResponse = rawTextResult;
|
|
|
10
10
|
// responses. Used wherever we validate the response of a `/pub/v3/messages*`
|
|
11
11
|
// call. Loose: unknown keys pass through (and survive into cached listData).
|
|
12
12
|
export const ApiRecipientSchema = z.looseObject({
|
|
13
|
-
|
|
13
|
+
// Live OFW payloads key the recipient's id as `userId` (verified against a
|
|
14
|
+
// real /pub/v3/messages record: `recipients[].user.userId === 3039201`). An
|
|
15
|
+
// earlier guess read `id`, which is absent — so every normalized recipient
|
|
16
|
+
// came out with `userId: 0`, breaking any "find my own recipient" match. Both
|
|
17
|
+
// are accepted (userId first, id fallback) so a backend that ever returns `id`
|
|
18
|
+
// still resolves.
|
|
19
|
+
user: z.looseObject({
|
|
20
|
+
userId: z.number().optional(),
|
|
21
|
+
id: z.number().optional(),
|
|
22
|
+
name: z.string().optional(),
|
|
23
|
+
}).optional(),
|
|
14
24
|
viewed: z.looseObject({ dateTime: z.string() }).nullable().optional(),
|
|
15
25
|
});
|
|
16
26
|
// Translates OFW API recipient shape into the cache's normalized Recipient.
|
|
@@ -28,7 +38,7 @@ export function mapRecipients(items) {
|
|
|
28
38
|
return (items ?? []).map((r) => {
|
|
29
39
|
const dt = r.viewed?.dateTime;
|
|
30
40
|
const viewedAt = typeof dt === 'string' && !dt.startsWith('1970-01-01') ? dt : null;
|
|
31
|
-
return { userId: r.user?.id ?? 0, name: r.user?.name ?? '', viewedAt };
|
|
41
|
+
return { userId: r.user?.userId ?? r.user?.id ?? 0, name: r.user?.name ?? '', viewedAt };
|
|
32
42
|
});
|
|
33
43
|
}
|
|
34
44
|
// True if any recipient has a *real* "First Viewed" time — i.e. present and
|
|
@@ -40,6 +50,62 @@ export function mapRecipients(items) {
|
|
|
40
50
|
export function hasRealView(recipients) {
|
|
41
51
|
return recipients.some((r) => r.viewedAt !== null && !r.viewedAt.startsWith('1970-01-01'));
|
|
42
52
|
}
|
|
53
|
+
// True when the once-scraped list flags themselves say the message is read.
|
|
54
|
+
// `showNeverViewed === false` is OFW's reliable "has been viewed" signal (per
|
|
55
|
+
// CLAUDE.md); `read === true` is the inbox list's own flag. Both are only ever
|
|
56
|
+
// captured at first sight, so they can go stale — they raise `read` but never
|
|
57
|
+
// lower it (see deriveRead).
|
|
58
|
+
function scrapeSaysRead(listData) {
|
|
59
|
+
if (typeof listData !== 'object' || listData === null)
|
|
60
|
+
return false;
|
|
61
|
+
const ld = listData;
|
|
62
|
+
return ld.read === true || ld.showNeverViewed === false;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Derive a message's authoritative read state from the cached record itself,
|
|
66
|
+
* rather than trusting the `read`/`showNeverViewed` flags scraped once from the
|
|
67
|
+
* list endpoint. Those flags are frozen at first sight and drift the moment a
|
|
68
|
+
* message is read after caching — most often when a body fetch
|
|
69
|
+
* (`ofw_get_message`) marks an inbox message read on OFW as a side effect,
|
|
70
|
+
* populating `fetchedBodyAt` and the recipient's `viewedAt` but leaving the
|
|
71
|
+
* stale `read: false` behind.
|
|
72
|
+
*
|
|
73
|
+
* The derivation is monotonic — every input can only turn read ON — so a later
|
|
74
|
+
* resync (which re-scrapes the list flags) can never flip a read message back
|
|
75
|
+
* to unread:
|
|
76
|
+
*
|
|
77
|
+
* - INBOX: the account holder is the recipient. When we know our own id
|
|
78
|
+
* (`selfUserId`), that recipient's `viewedAt` is authoritative; otherwise any
|
|
79
|
+
* recipient's `viewedAt` stands in (1:1 co-parent messaging). Fetching the
|
|
80
|
+
* body marks the message read on OFW, so a non-null `fetchedBodyAt` is also
|
|
81
|
+
* read=true. The stale scrape flag is only a last-resort fallback.
|
|
82
|
+
* - SENT: "read" means a *recipient* has opened it — tracked via their
|
|
83
|
+
* `viewedAt` (the detail endpoint's real timestamp) — never our own body
|
|
84
|
+
* fetch, which is always set for sent messages.
|
|
85
|
+
*/
|
|
86
|
+
export function deriveRead(row, selfUserId) {
|
|
87
|
+
if (row.folder === 'inbox') {
|
|
88
|
+
const viewed = selfUserId !== undefined
|
|
89
|
+
? row.recipients.some((r) => r.userId === selfUserId && r.viewedAt !== null)
|
|
90
|
+
: row.recipients.some((r) => r.viewedAt !== null);
|
|
91
|
+
return viewed || row.fetchedBodyAt !== null || scrapeSaysRead(row.listData);
|
|
92
|
+
}
|
|
93
|
+
return row.recipients.some((r) => r.viewedAt !== null) || scrapeSaysRead(row.listData);
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Return the row augmented with an authoritative top-level `read` boolean and a
|
|
97
|
+
* `listData` whose `read`/`showNeverViewed` flags are forced to agree with it —
|
|
98
|
+
* so a single response can never contradict itself (the reported bug: a record
|
|
99
|
+
* carrying `listData.read: false` alongside a populated recipient `viewedAt`).
|
|
100
|
+
* A non-object `listData` (null / legacy string) is passed through untouched.
|
|
101
|
+
*/
|
|
102
|
+
export function withReadState(row, selfUserId) {
|
|
103
|
+
const read = deriveRead(row, selfUserId);
|
|
104
|
+
const listData = (typeof row.listData === 'object' && row.listData !== null)
|
|
105
|
+
? { ...row.listData, read, showNeverViewed: !read }
|
|
106
|
+
: row.listData;
|
|
107
|
+
return { ...row, read, listData };
|
|
108
|
+
}
|
|
43
109
|
// Expand a user-provided path: ~ → home, relative → absolute. Re-exports
|
|
44
110
|
// @chrischall/mcp-utils' `expandPath`.
|
|
45
111
|
export const expandPath = expandPathUtil;
|
package/dist/tools/messages.js
CHANGED
|
@@ -2,7 +2,7 @@ import { z } from 'zod';
|
|
|
2
2
|
import { syncAll, fetchAttachmentMeta, fetchAttachmentMetaForMessage } from '../sync.js';
|
|
3
3
|
import { getAttachmentsDir, getDefaultInlineAttachments, getSyncMaxRequests, getWriteMode } from '../config.js';
|
|
4
4
|
import { basename, join } from 'node:path';
|
|
5
|
-
import { ApiRecipientSchema, expandPath, hasRealView, jsonResponse, mapRecipients, postMessageAndRefetch, textResponse, verifyWriteLanded } from './_shared.js';
|
|
5
|
+
import { ApiRecipientSchema, expandPath, hasRealView, jsonResponse, mapRecipients, postMessageAndRefetch, textResponse, verifyWriteLanded, withReadState } from './_shared.js';
|
|
6
6
|
import { parseLenient } from '@chrischall/mcp-utils';
|
|
7
7
|
// Schemas for the load-bearing fields of each /pub/v3 response this file
|
|
8
8
|
// reads (issue #83). Loose: unknown keys pass through into cached listData.
|
|
@@ -112,7 +112,11 @@ export function registerMessageTools(server, client, cacheProvider, attachmentIO
|
|
|
112
112
|
const cache = cacheProvider();
|
|
113
113
|
const filter = { folder, since: args.since, until: args.until, q: args.q };
|
|
114
114
|
const total = await cache.countMessages(filter);
|
|
115
|
-
|
|
115
|
+
// Reconcile each row's read state at read time: the cached list flags can be
|
|
116
|
+
// stale (a message read after it was first scraped), so `read` is derived
|
|
117
|
+
// from the record's own `viewedAt`/`fetchedBodyAt` and `listData` is forced
|
|
118
|
+
// to agree — see withReadState.
|
|
119
|
+
const messages = (await cache.listMessages({ ...filter, page, size })).map((m) => withReadState(m));
|
|
116
120
|
const payload = { messages, total, page, size };
|
|
117
121
|
if (total === 0) {
|
|
118
122
|
payload.note = 'No messages match these filters. If you expected results, check ofw_sync_messages was run, or relax the filters.';
|
|
@@ -205,7 +209,7 @@ export function registerMessageTools(server, client, cacheProvider, attachmentIO
|
|
|
205
209
|
// Backfill is best-effort. Fall through with whatever we have.
|
|
206
210
|
}
|
|
207
211
|
}
|
|
208
|
-
return jsonResponse({ ...row, attachments });
|
|
212
|
+
return jsonResponse({ ...withReadState(row), attachments });
|
|
209
213
|
}
|
|
210
214
|
const detail = parseLenient(MessageDetailSchema, await client.request('GET', `/pub/v3/messages/${encodeURIComponent(args.messageId)}`), { label: 'ofw-mcp', context: 'GET /pub/v3/messages/{id} (ofw_get_message)' });
|
|
211
215
|
// Derive the folder for a live-fetched message. A cached row (reached here
|
|
@@ -240,7 +244,7 @@ export function registerMessageTools(server, client, cacheProvider, attachmentIO
|
|
|
240
244
|
await fetchAttachmentMetaForMessage(client, detail.id, detail.files, cache);
|
|
241
245
|
}
|
|
242
246
|
const attachments = await cache.listAttachmentsForMessage(detail.id);
|
|
243
|
-
return jsonResponse({ ...row, attachments });
|
|
247
|
+
return jsonResponse({ ...withReadState(row), attachments });
|
|
244
248
|
});
|
|
245
249
|
if (allowSend)
|
|
246
250
|
server.registerTool('ofw_send_message', {
|
package/package.json
CHANGED
package/server.json
CHANGED
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
"url": "https://github.com/chrischall/ofw-mcp",
|
|
7
7
|
"source": "github"
|
|
8
8
|
},
|
|
9
|
-
"version": "2.6.
|
|
9
|
+
"version": "2.6.4",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "ofw-mcp",
|
|
14
|
-
"version": "2.6.
|
|
14
|
+
"version": "2.6.4",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
},
|
|
@@ -55,7 +55,7 @@ read on OFW):
|
|
|
55
55
|
|
|
56
56
|
```sh
|
|
57
57
|
curl -s "https://ofw.ourfamilywizard.com/pub/v3/messages/${ID}" "${AUTH_HEADERS[@]}" \
|
|
58
|
-
| jq '{id, subject, body, sentAt: .date.dateTime, from: .from.name, files, recipients: [.recipients[] | {id: .user.
|
|
58
|
+
| jq '{id, subject, body, sentAt: .date.dateTime, from: .from.name, files, recipients: [.recipients[] | {id: .user.userId, name: .user.name, viewedAt: .viewed.dateTime}]}'
|
|
59
59
|
```
|
|
60
60
|
|
|
61
61
|
## 3. Send a message / save a draft (write — confirm-by-re-GET)
|