ofw-mcp 2.4.3 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +7 -3
- package/dist/auth.js +6 -14
- package/dist/bundle.js +404 -141
- package/dist/client.js +4 -10
- package/dist/config.js +23 -21
- package/dist/index.js +1 -1
- package/dist/sync.js +25 -9
- package/dist/tools/_shared.js +35 -9
- package/dist/tools/calendar.js +145 -29
- package/dist/tools/messages.js +40 -9
- package/package.json +4 -4
- package/server.json +8 -2
- package/skills/ofw-fpx/SKILL.md +106 -0
- package/skills/ofw-fpx/references/requests.md +252 -0
- package/dist/validate.js +0 -35
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.
|
|
9
|
+
"version": "2.5.0",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "ofw-mcp",
|
|
14
|
-
"version": "2.
|
|
14
|
+
"version": "2.5.0",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
},
|
|
@@ -34,6 +34,12 @@
|
|
|
34
34
|
"description": "Write-tool gate: \"none\" registers no write tools; \"drafts\" registers draft-level writes only (save/delete drafts, upload attachments); \"all\" registers everything (default). Unrecognized values fail closed to \"none\".",
|
|
35
35
|
"isRequired": false,
|
|
36
36
|
"format": "string"
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"name": "OFW_CALENDAR_WRITES",
|
|
40
|
+
"description": "Set to \"true\" to register calendar write tools (create/update/delete event) in \"drafts\" write mode. Events have no draft stage but are reversible. Never overrides \"none\".",
|
|
41
|
+
"isRequired": false,
|
|
42
|
+
"format": "string"
|
|
37
43
|
}
|
|
38
44
|
]
|
|
39
45
|
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ofw-fpx
|
|
3
|
+
description: >-
|
|
4
|
+
Access OurFamilyWizard (OFW) — messages, calendar, expenses, journal —
|
|
5
|
+
from a shell with the fpx CLI (@fetchproxy/cli) instead of running the
|
|
6
|
+
ofw-mcp server: capture the signed-in web app's Bearer token once via the
|
|
7
|
+
browser bridge, then curl the REST API directly. Use when you want OFW
|
|
8
|
+
data without the MCP, in a script, or on a machine where the MCP isn't
|
|
9
|
+
installed.
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# OurFamilyWizard via fpx + curl (no MCP)
|
|
13
|
+
|
|
14
|
+
OFW's app (`ofw.ourfamilywizard.com`) has no API key a script can request —
|
|
15
|
+
the only credential is the Bearer token the web app itself mints on login
|
|
16
|
+
and stores in `localStorage["auth"]` (with `localStorage["tokenExpiry"]`
|
|
17
|
+
alongside). Once you have that token the API itself has **no bot wall** —
|
|
18
|
+
`ofw-mcp`'s own `src/client.ts` calls it with plain Node `fetch` for every
|
|
19
|
+
request. So this skill is **hybrid**: `fpx` captures the token from a
|
|
20
|
+
signed-in browser tab ONCE, then plain `curl` does every read/write from
|
|
21
|
+
then on. fetchproxy never touches the actual API calls.
|
|
22
|
+
|
|
23
|
+
**This is a shared family-court record.** Every write here (`send`,
|
|
24
|
+
`create_event`, `create_expense`, `create_journal_entry`,
|
|
25
|
+
`upload_attachment`, the `delete_*`/bulk-delete calls) lands on the same
|
|
26
|
+
record the MCP's `OFW_WRITE_MODE` gate exists to protect. There is no
|
|
27
|
+
dry-run/confirm here — curl just does it. Treat every write like the MCP's
|
|
28
|
+
`all` mode: real, permanent, and visible to your co-parent.
|
|
29
|
+
|
|
30
|
+
## One-time setup
|
|
31
|
+
|
|
32
|
+
```sh
|
|
33
|
+
npm install -g @fetchproxy/cli # provides `fpx`
|
|
34
|
+
fpx profile add ofw --domain ourfamilywizard.com
|
|
35
|
+
fpx profile declare ofw --local-storage auth --local-storage tokenExpiry
|
|
36
|
+
fpx pair -p ofw # prints a pair code → approve in Transporter
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Requirements: the **Transporter** browser extension installed, with an
|
|
40
|
+
open, signed-in `ofw.ourfamilywizard.com` (or `www.ourfamilywizard.com`)
|
|
41
|
+
tab, and its Chrome **Site access** allowing `ourfamilywizard.com`. Pairing
|
|
42
|
+
persists across invocations.
|
|
43
|
+
|
|
44
|
+
## Capture the token (once per shell / whenever it goes stale)
|
|
45
|
+
|
|
46
|
+
```sh
|
|
47
|
+
LS=$(fpx local-storage auth tokenExpiry -p ofw)
|
|
48
|
+
TOKEN=$(jq -r '.auth' <<<"$LS")
|
|
49
|
+
EXPIRES=$(jq -r '.tokenExpiry' <<<"$LS")
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
If `auth` comes back empty, sign into OFW in the browser tab first — the
|
|
53
|
+
same precondition `ofw-mcp`'s own fetchproxy fallback documents in
|
|
54
|
+
`src/auth.ts`.
|
|
55
|
+
|
|
56
|
+
## Core call
|
|
57
|
+
|
|
58
|
+
Every request needs the bearer token plus OFW's two protocol headers
|
|
59
|
+
(sent on every call, not just login):
|
|
60
|
+
|
|
61
|
+
```sh
|
|
62
|
+
curl -s 'https://ofw.ourfamilywizard.com/pub/v2/profiles' \
|
|
63
|
+
-H "Authorization: Bearer $TOKEN" \
|
|
64
|
+
-H 'ofw-client: WebApplication' \
|
|
65
|
+
-H 'ofw-version: 1.0.0' \
|
|
66
|
+
| jq .
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
`ofw-version` is OFW's wire-protocol version (see `src/protocol.ts`),
|
|
70
|
+
unrelated to any package version — send it as-is. Writes add
|
|
71
|
+
`-H 'Content-Type: application/json' --data '...'` for JSON bodies, or
|
|
72
|
+
`-F` multipart fields for uploads/deletes — both shown per-endpoint in
|
|
73
|
+
`references/requests.md`.
|
|
74
|
+
|
|
75
|
+
## The one rule: re-GET after every message POST to confirm it landed
|
|
76
|
+
|
|
77
|
+
OFW's `POST /pub/v3/messages` response is minimal (`{"entityId": <id>}` or
|
|
78
|
+
legacy `{"id": <id>}`) and — worse — its draft-*replace* path silently
|
|
79
|
+
no-ops while still echoing success. Never trust the POST status alone for
|
|
80
|
+
a send or draft save: immediately `GET /pub/v3/messages/{id}` with the
|
|
81
|
+
returned id and check the body/subject actually match what you sent. See
|
|
82
|
+
§2/§3 in `references/requests.md`.
|
|
83
|
+
|
|
84
|
+
## Auth-error handling
|
|
85
|
+
|
|
86
|
+
- **401** — the token expired or was invalidated. OFW has no refresh-token
|
|
87
|
+
flow; re-mint by reloading/re-signing-in on the `ourfamilywizard.com`
|
|
88
|
+
tab, then re-run the capture step above.
|
|
89
|
+
- **429** — OFW's own client waits 2s and retries exactly once
|
|
90
|
+
(`src/client.ts`); do the same: `sleep 2` and resend the identical
|
|
91
|
+
request. A second 429 is a real rate-limit — back off further.
|
|
92
|
+
- Any other non-2xx is a real upstream error — surface the response body.
|
|
93
|
+
|
|
94
|
+
All 20 endpoint operations, request bodies, and `jq` projections are in
|
|
95
|
+
`references/requests.md`, transcribed from `src/tools/*.ts`, `src/sync.ts`,
|
|
96
|
+
and `src/tools/_shared.ts` — nothing here is guessed.
|
|
97
|
+
|
|
98
|
+
## Notes
|
|
99
|
+
|
|
100
|
+
- Base URL is `https://ofw.ourfamilywizard.com` for every endpoint (the
|
|
101
|
+
`www.` host serves the web app UI, not the API).
|
|
102
|
+
- `ofw-mcp` maintains a local SQLite message cache for fast list/search;
|
|
103
|
+
this skill has no cache — every list call here goes straight to OFW, and
|
|
104
|
+
`GET /pub/v3/messages/{id}` on an **unread inbox message marks it read**
|
|
105
|
+
on OFW, exactly as it does for the MCP.
|
|
106
|
+
- This project is developed and maintained by AI (Claude).
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
# OurFamilyWizard requests for fpx + curl
|
|
2
|
+
|
|
3
|
+
Base URL for every call: `https://ofw.ourfamilywizard.com`. Every request
|
|
4
|
+
carries these three headers (from `src/protocol.ts` / `src/client.ts`):
|
|
5
|
+
|
|
6
|
+
```sh
|
|
7
|
+
AUTH_HEADERS=(-H "Authorization: Bearer $TOKEN" -H 'ofw-client: WebApplication' -H 'ofw-version: 1.0.0')
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
`$TOKEN` comes from the one-time capture in `SKILL.md`. All paths, params,
|
|
11
|
+
and bodies below are transcribed from `src/tools/*.ts`, `src/sync.ts`, and
|
|
12
|
+
`src/tools/_shared.ts` — the exact shapes `ofw-mcp` sends.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## 1. Profile & dashboard
|
|
17
|
+
|
|
18
|
+
**Current user + co-parent profile:**
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
curl -s 'https://ofw.ourfamilywizard.com/pub/v2/profiles' "${AUTH_HEADERS[@]}" | jq .
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
**Dashboard summary (unread count, upcoming events, outstanding expenses).
|
|
25
|
+
Note: this call updates your last-seen status on OFW, same as opening the
|
|
26
|
+
web app's dashboard:**
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
curl -s 'https://ofw.ourfamilywizard.com/pub/v1/users/useraccountstatus' "${AUTH_HEADERS[@]}" | jq .
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## 2. Messages — folders, list, detail
|
|
33
|
+
|
|
34
|
+
**Folder IDs + unread counts** (needed before listing by folder):
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
curl -s 'https://ofw.ourfamilywizard.com/pub/v1/messageFolders?includeFolderCounts=true' "${AUTH_HEADERS[@]}" \
|
|
38
|
+
| jq '.systemFolders[] | {id, folderType}'
|
|
39
|
+
# folderType is one of INBOX / SENT_MESSAGES / DRAFTS
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
**List messages in a folder** (date-desc, 50/page is what the MCP's sync
|
|
43
|
+
uses; unread inbox items carry `showNeverViewed: true` — the reliable
|
|
44
|
+
unread signal, per CLAUDE.md):
|
|
45
|
+
|
|
46
|
+
```sh
|
|
47
|
+
FOLDER_ID=<id from above>
|
|
48
|
+
curl -s "https://ofw.ourfamilywizard.com/pub/v3/messages?folders=${FOLDER_ID}&page=1&size=50&sort=date&sortDirection=desc" \
|
|
49
|
+
"${AUTH_HEADERS[@]}" \
|
|
50
|
+
| jq '.data[] | {id, subject, sentAt: .date.dateTime, from: .from.name, showNeverViewed}'
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
**Message/draft detail by id** (GETting an unread inbox message marks it
|
|
54
|
+
read on OFW):
|
|
55
|
+
|
|
56
|
+
```sh
|
|
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.id, name: .user.name, viewedAt: .viewed.dateTime}]}'
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## 3. Send a message / save a draft (write — confirm-by-re-GET)
|
|
62
|
+
|
|
63
|
+
Both send and save-draft POST the same shape to `/pub/v3/messages`; only
|
|
64
|
+
`draft` (bool) differs. **Never pass `messageId`/an existing id in this
|
|
65
|
+
POST** — OFW's update-in-place endpoint silently no-ops on repeat edits
|
|
66
|
+
while echoing success. To "replace" a draft: POST a fresh one, confirm it
|
|
67
|
+
landed, then bulk-delete the old id (§4).
|
|
68
|
+
|
|
69
|
+
```sh
|
|
70
|
+
BODY=$(jq -n \
|
|
71
|
+
--arg subject 'Pickup time change' \
|
|
72
|
+
--arg body 'Can we move Friday pickup to 5pm instead of 4?' \
|
|
73
|
+
--argjson recipientIds '[12345]' \
|
|
74
|
+
--argjson myFileIDs '[]' \
|
|
75
|
+
--arg draft false \
|
|
76
|
+
--arg includeOriginal false \
|
|
77
|
+
--argjson replyToId null \
|
|
78
|
+
'{subject:$subject, body:$body, recipientIds:$recipientIds,
|
|
79
|
+
attachments:{myFileIDs:$myFileIDs}, draft:($draft=="true"),
|
|
80
|
+
includeOriginal:($includeOriginal=="true"), replyToId:$replyToId}')
|
|
81
|
+
|
|
82
|
+
RESP=$(curl -s -X POST 'https://ofw.ourfamilywizard.com/pub/v3/messages' \
|
|
83
|
+
"${AUTH_HEADERS[@]}" -H 'Content-Type: application/json' --data "$BODY")
|
|
84
|
+
|
|
85
|
+
NEW_ID=$(jq -r '.id // .entityId // empty' <<<"$RESP")
|
|
86
|
+
[ -n "$NEW_ID" ] || { echo "SEND UNCONFIRMED: no id in response: $RESP" >&2; exit 1; }
|
|
87
|
+
|
|
88
|
+
# Re-GET immediately — the only honest way to confirm the write landed.
|
|
89
|
+
DETAIL=$(curl -s "https://ofw.ourfamilywizard.com/pub/v3/messages/${NEW_ID}" "${AUTH_HEADERS[@]}")
|
|
90
|
+
jq -e --arg s 'Pickup time change' --arg b 'Can we move Friday pickup' \
|
|
91
|
+
'(.subject // "" | contains($s)) and (.body // "" | contains($b))' <<<"$DETAIL" >/dev/null \
|
|
92
|
+
&& echo "confirmed id=$NEW_ID" || echo "WARNING: re-fetched body/subject does not contain what was sent — verify on ourfamilywizard.com" >&2
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
For a **draft**, set `draft:true`; `subject`/`body` are the only required
|
|
96
|
+
fields (`recipientIds` may be `[]`).
|
|
97
|
+
|
|
98
|
+
To **reply**, set `replyToId` to the parent message id and
|
|
99
|
+
`includeOriginal:true` (OFW appends the original message to the body
|
|
100
|
+
server-side — that's why containment, not equality, is the right check
|
|
101
|
+
above).
|
|
102
|
+
|
|
103
|
+
## 4. Delete messages/drafts (bulk, multipart)
|
|
104
|
+
|
|
105
|
+
Same endpoint deletes both sent-message ids and draft ids — pass whichever
|
|
106
|
+
you mean:
|
|
107
|
+
|
|
108
|
+
```sh
|
|
109
|
+
curl -s -X DELETE 'https://ofw.ourfamilywizard.com/pub/v1/messages' \
|
|
110
|
+
"${AUTH_HEADERS[@]}" \
|
|
111
|
+
-F 'messageIds=111' -F 'messageIds=222' # repeat -F per id
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## 5. Attachments
|
|
115
|
+
|
|
116
|
+
**Upload a file to "My Files"** (multipart; `shareClass` is `PRIVATE` or
|
|
117
|
+
`SHARED`; matches the web UI's upload request in `src/tools/messages.ts`):
|
|
118
|
+
|
|
119
|
+
```sh
|
|
120
|
+
curl -s -X POST 'https://ofw.ourfamilywizard.com/pub/v3/myfiles/multipart' \
|
|
121
|
+
"${AUTH_HEADERS[@]}" \
|
|
122
|
+
-F "file=@/path/to/file.pdf;type=application/pdf" \
|
|
123
|
+
-F 'source=message' \
|
|
124
|
+
-F 'description=file.pdf' \
|
|
125
|
+
-F 'label=file.pdf' \
|
|
126
|
+
-F 'fileName=file.pdf' \
|
|
127
|
+
-F 'shareClass=PRIVATE' \
|
|
128
|
+
| jq '{fileId, fileName, fileType, sizeInBytes}'
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
The response's `fileId` is what you pass as `myFileIDs` in §3's POST body
|
|
132
|
+
(`attachments.myFileIDs`) to attach it to a message/draft.
|
|
133
|
+
|
|
134
|
+
**Attachment metadata:**
|
|
135
|
+
|
|
136
|
+
```sh
|
|
137
|
+
curl -s "https://ofw.ourfamilywizard.com/pub/v1/myfiles/${FILE_ID}" "${AUTH_HEADERS[@]}" \
|
|
138
|
+
| jq '{fileId, fileName, fileType, fileSize, label}'
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
**Download attachment bytes** (binary — write straight to a file, don't
|
|
142
|
+
pipe through `jq`):
|
|
143
|
+
|
|
144
|
+
```sh
|
|
145
|
+
curl -s "https://ofw.ourfamilywizard.com/pub/v1/myfiles/${FILE_ID}/data" \
|
|
146
|
+
"${AUTH_HEADERS[@]}" -o "./${FILE_ID}-download"
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## 6. Calendar
|
|
150
|
+
|
|
151
|
+
**List events** (`basic` or `detailed`; dates are `YYYY-MM-DD`):
|
|
152
|
+
|
|
153
|
+
```sh
|
|
154
|
+
curl -s "https://ofw.ourfamilywizard.com/pub/v1/calendar/basic?startDate=2026-07-01&endDate=2026-07-31" \
|
|
155
|
+
"${AUTH_HEADERS[@]}" | jq .
|
|
156
|
+
# swap "basic" for "detailed" for full event details
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
**Create an event** (write — court-visible; `eventFor` is
|
|
160
|
+
`neither|parent1|parent2`):
|
|
161
|
+
|
|
162
|
+
```sh
|
|
163
|
+
BODY=$(jq -n '{
|
|
164
|
+
title: "Soccer practice",
|
|
165
|
+
startDate: "2026-07-20T16:00:00",
|
|
166
|
+
endDate: "2026-07-20T17:30:00",
|
|
167
|
+
allDay: false,
|
|
168
|
+
location: "Community field",
|
|
169
|
+
reminder: "1 hour before",
|
|
170
|
+
privateEvent: false,
|
|
171
|
+
eventFor: "neither",
|
|
172
|
+
children: [67890]
|
|
173
|
+
}')
|
|
174
|
+
curl -s -X POST 'https://ofw.ourfamilywizard.com/pub/v1/calendar/events' \
|
|
175
|
+
"${AUTH_HEADERS[@]}" -H 'Content-Type: application/json' --data "$BODY" | jq .
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
**Update an event** (send only the fields you're changing):
|
|
179
|
+
|
|
180
|
+
```sh
|
|
181
|
+
curl -s -X PUT "https://ofw.ourfamilywizard.com/pub/v1/calendar/events/${EVENT_ID}" \
|
|
182
|
+
"${AUTH_HEADERS[@]}" -H 'Content-Type: application/json' \
|
|
183
|
+
--data '{"title":"Soccer practice (moved)","startDate":"2026-07-20T17:00:00"}' | jq .
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
**Delete an event:**
|
|
187
|
+
|
|
188
|
+
```sh
|
|
189
|
+
curl -s -X DELETE "https://ofw.ourfamilywizard.com/pub/v1/calendar/events/${EVENT_ID}" "${AUTH_HEADERS[@]}"
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
## 7. Expenses
|
|
193
|
+
|
|
194
|
+
**Totals (owed/paid):**
|
|
195
|
+
|
|
196
|
+
```sh
|
|
197
|
+
curl -s 'https://ofw.ourfamilywizard.com/pub/v2/expense/expenses/totals' "${AUTH_HEADERS[@]}" | jq .
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
**List expenses** (offset-based, 0-indexed `start`):
|
|
201
|
+
|
|
202
|
+
```sh
|
|
203
|
+
curl -s 'https://ofw.ourfamilywizard.com/pub/v2/expense/expenses?start=0&max=20' "${AUTH_HEADERS[@]}" | jq .
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
**Create an expense** (write):
|
|
207
|
+
|
|
208
|
+
```sh
|
|
209
|
+
curl -s -X POST 'https://ofw.ourfamilywizard.com/pub/v2/expense/expenses' \
|
|
210
|
+
"${AUTH_HEADERS[@]}" -H 'Content-Type: application/json' \
|
|
211
|
+
--data '{"amount": 45.00, "description": "Cleats for soccer"}' | jq .
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
## 8. Journal
|
|
215
|
+
|
|
216
|
+
**List entries** (offset-based, but **1-indexed** `start` — unlike
|
|
217
|
+
expenses):
|
|
218
|
+
|
|
219
|
+
```sh
|
|
220
|
+
curl -s 'https://ofw.ourfamilywizard.com/pub/v1/journals?start=1&max=10' "${AUTH_HEADERS[@]}" | jq .
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
**Create an entry** (write — journal entries are a permanent court record):
|
|
224
|
+
|
|
225
|
+
```sh
|
|
226
|
+
curl -s -X POST 'https://ofw.ourfamilywizard.com/pub/v1/journals' \
|
|
227
|
+
"${AUTH_HEADERS[@]}" -H 'Content-Type: application/json' \
|
|
228
|
+
--data '{"title": "Missed pickup", "body": "Co-parent arrived 45 min late without notice."}' | jq .
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
---
|
|
232
|
+
|
|
233
|
+
## Auth-error / retry recipe (wrap any of the above)
|
|
234
|
+
|
|
235
|
+
```sh
|
|
236
|
+
RESP_FILE=$(mktemp /tmp/ofw-resp.XXXXXX.json)
|
|
237
|
+
trap 'rm -f "$RESP_FILE"' EXIT
|
|
238
|
+
|
|
239
|
+
call() { curl -s -o "$RESP_FILE" -w '%{http_code}' "$@" "${AUTH_HEADERS[@]}"; }
|
|
240
|
+
|
|
241
|
+
STATUS=$(call 'https://ofw.ourfamilywizard.com/pub/v2/profiles')
|
|
242
|
+
if [ "$STATUS" = "429" ]; then
|
|
243
|
+
sleep 2
|
|
244
|
+
STATUS=$(call 'https://ofw.ourfamilywizard.com/pub/v2/profiles')
|
|
245
|
+
fi
|
|
246
|
+
if [ "$STATUS" = "401" ]; then
|
|
247
|
+
echo "token expired — reload/sign in on the ourfamilywizard.com tab, then re-run the fpx local-storage capture" >&2
|
|
248
|
+
exit 1
|
|
249
|
+
fi
|
|
250
|
+
[ "$STATUS" -lt 300 ] || { echo "OFW API error: $STATUS $(cat "$RESP_FILE")" >&2; exit 1; }
|
|
251
|
+
jq . "$RESP_FILE"
|
|
252
|
+
```
|
package/dist/validate.js
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Validate an OFW API response against a zod schema at the call site.
|
|
3
|
-
*
|
|
4
|
-
* Every OFW endpoint is reverse-engineered and undocumented, so a backend
|
|
5
|
-
* change on their side would otherwise flow `undefined` silently into the
|
|
6
|
-
* SQLite cache and persist (issue #83). Schemas are `.looseObject(...)`
|
|
7
|
-
* covering ONLY the fields the code actually reads — cosmetic API additions
|
|
8
|
-
* pass through untouched (and stay present in the parsed output, which
|
|
9
|
-
* matters for `listData`/`metadata` blobs cached verbatim).
|
|
10
|
-
*
|
|
11
|
-
* Two modes, chosen per call site:
|
|
12
|
-
* - `'lenient'` (default) — read/sync paths. On mismatch, log a structured
|
|
13
|
-
* warning to stderr naming the endpoint and fields, then return the RAW
|
|
14
|
-
* response unchanged so the existing `??` fallbacks keep the tool useful.
|
|
15
|
-
* - `'strict'` — write paths (send/save_draft verification, upload). On
|
|
16
|
-
* mismatch, throw: proceeding on an unverifiable response risks deleting
|
|
17
|
-
* a draft, mis-reporting a send, or caching an unusable fileId.
|
|
18
|
-
*
|
|
19
|
-
* The error/warning text is deliberately precise ("date.dateTime: expected
|
|
20
|
-
* string…") — it's the failure signal a maintainer (human or Claude) fixes
|
|
21
|
-
* in one session, vs. "some messages show the wrong date sometimes".
|
|
22
|
-
*/
|
|
23
|
-
export function parseOFW(schema, raw, ctx, mode = 'lenient') {
|
|
24
|
-
const result = schema.safeParse(raw);
|
|
25
|
-
if (result.success)
|
|
26
|
-
return result.data;
|
|
27
|
-
const issues = result.error.issues
|
|
28
|
-
.map((i) => `${i.path.join('.') || '(root)'}: ${i.message}`)
|
|
29
|
-
.join('; ');
|
|
30
|
-
const message = `OFW response for ${ctx} failed validation: ${issues}`;
|
|
31
|
-
if (mode === 'strict')
|
|
32
|
-
throw new Error(message);
|
|
33
|
-
console.error(`[ofw-mcp] WARNING: ${message} — continuing with the raw response; fields derived from it may be missing or wrong.`);
|
|
34
|
-
return raw;
|
|
35
|
-
}
|