ofw-mcp 2.19.2 → 2.19.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/README.md +47 -9
- package/dist/auth-password.js +39 -1
- package/dist/bundle.js +1114 -245
- package/dist/config.js +16 -0
- package/dist/extract/spreadsheet.js +33 -10
- package/dist/index.js +1 -1
- package/dist/sync.js +3 -2
- package/dist/timestamps.js +42 -0
- package/dist/tools/_confirm.js +49 -0
- package/dist/tools/_shared.js +79 -2
- package/dist/tools/attachments.js +85 -8
- package/dist/tools/calendar.js +143 -15
- package/dist/tools/draft-freshness.js +1 -1
- package/dist/tools/expenses.js +39 -5
- package/dist/tools/journal.js +14 -2
- package/dist/tools/messages.js +201 -34
- package/mint.yaml +27 -1
- package/package.json +2 -2
- package/server.json +2 -2
- package/skills/ofw/SKILL.md +3 -1
package/dist/tools/calendar.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { jsonResponse, textResponse } from './_shared.js';
|
|
2
|
+
import { jsonResponse, requestWrite, textResponse, UnconfirmedWriteError, unconfirmedWriteResponse } from './_shared.js';
|
|
3
|
+
import { CONFIRM_NOTE, confirmTokenParam, confirmWrite, stateRevision } from './_confirm.js';
|
|
3
4
|
import { getCalendarWritesAllowed } from '../config.js';
|
|
4
5
|
import { parseLenient } from '@chrischall/mcp-utils';
|
|
5
6
|
// OFW's real event-write API (reverse-engineered from the web app bundle):
|
|
@@ -104,6 +105,47 @@ function detailToWriteArgs(d) {
|
|
|
104
105
|
pickUpParentId: d.pickUpParent?.userId,
|
|
105
106
|
};
|
|
106
107
|
}
|
|
108
|
+
const SHARED = 'shared with co-parent';
|
|
109
|
+
const PRIVATE = 'private (only you)';
|
|
110
|
+
/**
|
|
111
|
+
* The human-readable form of an event for a confirmation preview: the fields
|
|
112
|
+
* a person checks (title, when, where, who sees it), with tagged users named
|
|
113
|
+
* wherever OFW's own event detail named them. An id OFW gave no name for is
|
|
114
|
+
* reported with `name: null` — never an invented one.
|
|
115
|
+
*/
|
|
116
|
+
function describeEvent(a, names = new Map()) {
|
|
117
|
+
const who = (id) => ({ userId: id, name: names.get(id) ?? null });
|
|
118
|
+
const out = {
|
|
119
|
+
title: a.title,
|
|
120
|
+
startDate: a.startDate,
|
|
121
|
+
endDate: a.endDate ?? a.startDate,
|
|
122
|
+
...(a.allDay ? { allDay: true } : { startTime: a.startTime, endTime: a.endTime }),
|
|
123
|
+
visibility: a.privateEvent ? PRIVATE : SHARED,
|
|
124
|
+
};
|
|
125
|
+
if (a.location)
|
|
126
|
+
out.location = a.location;
|
|
127
|
+
if (a.notes)
|
|
128
|
+
out.notes = a.notes;
|
|
129
|
+
if (a.children !== undefined)
|
|
130
|
+
out.children = a.children.map((id) => who(id));
|
|
131
|
+
if (a.eventParentId !== undefined)
|
|
132
|
+
out.eventParent = who(a.eventParentId);
|
|
133
|
+
if (a.dropOffParentId !== undefined)
|
|
134
|
+
out.dropOffParent = who(a.dropOffParentId);
|
|
135
|
+
if (a.pickUpParentId !== undefined)
|
|
136
|
+
out.pickUpParent = who(a.pickUpParentId);
|
|
137
|
+
return out;
|
|
138
|
+
}
|
|
139
|
+
/** userId → name for every tagged person the event detail names. */
|
|
140
|
+
function namesIn(d) {
|
|
141
|
+
const names = new Map();
|
|
142
|
+
for (const ref of [...(d.children ?? []), d.eventParent, d.dropOffParent, d.pickUpParent]) {
|
|
143
|
+
const name = ref?.name;
|
|
144
|
+
if (ref && typeof name === 'string' && name)
|
|
145
|
+
names.set(ref.userId, name);
|
|
146
|
+
}
|
|
147
|
+
return names;
|
|
148
|
+
}
|
|
107
149
|
export function registerCalendarTools(server, client) {
|
|
108
150
|
// Calendar writes land on the court-visible record with no draft stage, but
|
|
109
151
|
// events are reversible — 'all' mode, or 'drafts' + OFW_CALENDAR_WRITES=true.
|
|
@@ -123,14 +165,48 @@ export function registerCalendarTools(server, client) {
|
|
|
123
165
|
});
|
|
124
166
|
if (allowWrites)
|
|
125
167
|
server.registerTool('ofw_create_event', {
|
|
126
|
-
description: 'Create a calendar event in OurFamilyWizard. Unless privateEvent is true, the event is immediately visible to the co-parent — there is no draft stage.',
|
|
127
|
-
|
|
168
|
+
description: 'Create a calendar event in OurFamilyWizard. Unless privateEvent is true, the event is immediately visible to the co-parent — there is no draft stage — so a shared event is confirmed first (a private one is not). If the request fails without a definitive answer the result is EVENT_UNCONFIRMED: the event may already exist, so do NOT retry until ofw_list_events shows it did not land. ' + CONFIRM_NOTE,
|
|
169
|
+
// Additive, so not destructive — but it lands on a shared calendar
|
|
170
|
+
// outside this machine, which openWorldHint says to the host.
|
|
171
|
+
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
128
172
|
inputSchema: z.object({
|
|
129
173
|
title: z.string(),
|
|
130
174
|
...eventWriteFields,
|
|
175
|
+
confirmToken: confirmTokenParam,
|
|
131
176
|
}),
|
|
132
|
-
}, async (args) => {
|
|
133
|
-
const
|
|
177
|
+
}, async (args, ctx) => {
|
|
178
|
+
const { confirmToken, ...fields } = args;
|
|
179
|
+
const payload = buildEventPayload(fields);
|
|
180
|
+
if (!fields.privateEvent) {
|
|
181
|
+
const gate = await confirmWrite(ctx, {
|
|
182
|
+
tool: 'ofw_create_event',
|
|
183
|
+
action: 'ofw.event.create',
|
|
184
|
+
message: 'Review and confirm this OurFamilyWizard calendar event. It is shared with the co-parent and appears on their calendar immediately.',
|
|
185
|
+
target: 'event:new',
|
|
186
|
+
payload,
|
|
187
|
+
preview: {
|
|
188
|
+
action: 'Create shared OurFamilyWizard calendar event',
|
|
189
|
+
event: describeEvent(fields),
|
|
190
|
+
warning: 'Visible to the co-parent immediately; part of the court-visible record.',
|
|
191
|
+
},
|
|
192
|
+
confirmToken,
|
|
193
|
+
});
|
|
194
|
+
if (gate)
|
|
195
|
+
return gate;
|
|
196
|
+
}
|
|
197
|
+
let raw;
|
|
198
|
+
try {
|
|
199
|
+
raw = await requestWrite(client, 'POST', '/pub/v3/events', payload);
|
|
200
|
+
}
|
|
201
|
+
catch (e) {
|
|
202
|
+
if (!(e instanceof UnconfirmedWriteError))
|
|
203
|
+
throw e;
|
|
204
|
+
return unconfirmedWriteResponse(e, {
|
|
205
|
+
result: 'EVENT_UNCONFIRMED',
|
|
206
|
+
what: 'create this event',
|
|
207
|
+
checkWith: `ofw_list_events for ${fields.startDate} (look for "${fields.title}")`,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
134
210
|
const event = parseLenient(eventDetailSchema, raw, { label: 'ofw-mcp', context: 'POST /pub/v3/events', mode: 'strict' });
|
|
135
211
|
return jsonResponse({
|
|
136
212
|
note: `Event created. Use eventRecurrenceId ${event.eventRecurrenceId} as eventId for ofw_update_event/ofw_delete_event.`,
|
|
@@ -139,7 +215,7 @@ export function registerCalendarTools(server, client) {
|
|
|
139
215
|
});
|
|
140
216
|
if (allowWrites)
|
|
141
217
|
server.registerTool('ofw_update_event', {
|
|
142
|
-
description: 'Update an existing OurFamilyWizard calendar event. Fetches the event, applies the given changes, and writes the merged result back (OFW has no partial update).',
|
|
218
|
+
description: 'Update an existing OurFamilyWizard calendar event. Fetches the event, applies the given changes, and writes the merged result back (OFW has no partial update). A change to an event the co-parent can see (shared before or after the change) is confirmed first; the confirmation is bound to the event exactly as read, so if it changes on OFW in between (say the co-parent edited it) the update is refused instead of overwriting their edit. ' + CONFIRM_NOTE,
|
|
143
219
|
annotations: { destructiveHint: true },
|
|
144
220
|
inputSchema: z.object({
|
|
145
221
|
eventId: z.string().describe('Event id — the `id` from ofw_list_events / eventRecurrenceId from ofw_create_event'),
|
|
@@ -157,15 +233,41 @@ export function registerCalendarTools(server, client) {
|
|
|
157
233
|
eventParentId: eventWriteFields.eventParentId,
|
|
158
234
|
dropOffParentId: eventWriteFields.dropOffParentId,
|
|
159
235
|
pickUpParentId: eventWriteFields.pickUpParentId,
|
|
236
|
+
confirmToken: confirmTokenParam,
|
|
160
237
|
}),
|
|
161
|
-
}, async (args) => {
|
|
162
|
-
const { eventId, ...changes } = args;
|
|
238
|
+
}, async (args, ctx) => {
|
|
239
|
+
const { eventId, confirmToken, ...changes } = args;
|
|
163
240
|
const id = encodeURIComponent(eventId);
|
|
241
|
+
// Re-read on EVERY call (phase 1 and phase 2 alike): the merge base and
|
|
242
|
+
// the confirmation's revision both come from this read.
|
|
164
243
|
const rawDetail = await client.request('GET', `/pub/v3/events/${id}`);
|
|
165
244
|
const current = parseLenient(eventDetailSchema, rawDetail, { label: 'ofw-mcp', context: `GET /pub/v3/events/${eventId}`, mode: 'strict' });
|
|
166
245
|
const defined = Object.fromEntries(Object.entries(changes).filter(([, v]) => v !== undefined));
|
|
167
|
-
const
|
|
168
|
-
|
|
246
|
+
const base = detailToWriteArgs(current);
|
|
247
|
+
const merged = { ...base, ...defined };
|
|
248
|
+
const payload = buildEventPayload(merged);
|
|
249
|
+
if (current.publicFlag || !merged.privateEvent) {
|
|
250
|
+
const names = namesIn(current);
|
|
251
|
+
const gate = await confirmWrite(ctx, {
|
|
252
|
+
tool: 'ofw_update_event',
|
|
253
|
+
action: 'ofw.event.update',
|
|
254
|
+
message: `Review and confirm this change to the OurFamilyWizard event "${current.title}". The co-parent sees the updated event on their calendar.`,
|
|
255
|
+
target: `event:${eventId}`,
|
|
256
|
+
revision: stateRevision(base),
|
|
257
|
+
payload,
|
|
258
|
+
preview: {
|
|
259
|
+
action: 'Update OurFamilyWizard calendar event',
|
|
260
|
+
eventId,
|
|
261
|
+
before: describeEvent(base, names),
|
|
262
|
+
after: describeEvent(merged, names),
|
|
263
|
+
warning: 'The co-parent sees this change; part of the court-visible record.',
|
|
264
|
+
},
|
|
265
|
+
confirmToken,
|
|
266
|
+
});
|
|
267
|
+
if (gate)
|
|
268
|
+
return gate;
|
|
269
|
+
}
|
|
270
|
+
await client.request('PUT', `/pub/v3/events/${id}`, payload);
|
|
169
271
|
// PUT responses aren't documented — re-fetch the detail as authoritative state.
|
|
170
272
|
const rawAfter = await client.request('GET', `/pub/v3/events/${id}`);
|
|
171
273
|
const event = parseLenient(eventDetailSchema, rawAfter, { label: 'ofw-mcp', context: `GET /pub/v3/events/${eventId} (post-update)`, mode: 'strict' });
|
|
@@ -173,15 +275,41 @@ export function registerCalendarTools(server, client) {
|
|
|
173
275
|
});
|
|
174
276
|
if (allowWrites)
|
|
175
277
|
server.registerTool('ofw_delete_event', {
|
|
176
|
-
description: 'Delete an OurFamilyWizard calendar event',
|
|
177
|
-
annotations: { destructiveHint: true },
|
|
278
|
+
description: 'Delete an OurFamilyWizard calendar event. Reads the event first; deleting one the co-parent can see is confirmed first, with a preview of exactly which event (title, date, time) is removed, and is refused if the event changed on OFW after that preview. ' + CONFIRM_NOTE,
|
|
279
|
+
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
178
280
|
inputSchema: z.object({
|
|
179
281
|
eventId: z.string().describe('Event id — the `id` from ofw_list_events / eventRecurrenceId from ofw_create_event'),
|
|
180
282
|
includeFuture: z.boolean().describe('For repeating events: also delete future occurrences (default false)').optional(),
|
|
283
|
+
confirmToken: confirmTokenParam,
|
|
181
284
|
}),
|
|
182
|
-
}, async (args) => {
|
|
285
|
+
}, async (args, ctx) => {
|
|
183
286
|
const includeFuture = args.includeFuture ?? false;
|
|
184
|
-
|
|
185
|
-
|
|
287
|
+
const id = encodeURIComponent(args.eventId);
|
|
288
|
+
const rawDetail = await client.request('GET', `/pub/v3/events/${id}`);
|
|
289
|
+
const current = parseLenient(eventDetailSchema, rawDetail, { label: 'ofw-mcp', context: `GET /pub/v3/events/${args.eventId}`, mode: 'strict' });
|
|
290
|
+
if (current.publicFlag) {
|
|
291
|
+
const base = detailToWriteArgs(current);
|
|
292
|
+
const gate = await confirmWrite(ctx, {
|
|
293
|
+
tool: 'ofw_delete_event',
|
|
294
|
+
action: 'ofw.event.delete',
|
|
295
|
+
message: `Review and confirm deleting the OurFamilyWizard event "${current.title}". It disappears from the co-parent's calendar.`,
|
|
296
|
+
target: `event:${args.eventId}`,
|
|
297
|
+
revision: stateRevision(base),
|
|
298
|
+
payload: { eventId: args.eventId, includeFuture },
|
|
299
|
+
preview: {
|
|
300
|
+
action: 'Delete shared OurFamilyWizard calendar event',
|
|
301
|
+
eventId: args.eventId,
|
|
302
|
+
event: describeEvent(base, namesIn(current)),
|
|
303
|
+
includeFuture,
|
|
304
|
+
...(includeFuture ? { note: 'Future occurrences of this repeating event are deleted too.' } : {}),
|
|
305
|
+
warning: 'Removed from the co-parent\'s calendar.',
|
|
306
|
+
},
|
|
307
|
+
confirmToken: args.confirmToken,
|
|
308
|
+
});
|
|
309
|
+
if (gate)
|
|
310
|
+
return gate;
|
|
311
|
+
}
|
|
312
|
+
await client.request('DELETE', `/pub/v3/events/${id}?includeFuture=${includeFuture}`);
|
|
313
|
+
return textResponse(`Event ${args.eventId} ("${current.title}") deleted`);
|
|
186
314
|
});
|
|
187
315
|
}
|
|
@@ -11,7 +11,7 @@ export class DraftFreshnessError extends Error {
|
|
|
11
11
|
const FNV_OFFSET = 0xcbf29ce484222325n;
|
|
12
12
|
const FNV_PRIME = 0x100000001b3n;
|
|
13
13
|
const MASK64 = 0xffffffffffffffffn;
|
|
14
|
-
function fnv1a64(s) {
|
|
14
|
+
export function fnv1a64(s) {
|
|
15
15
|
let h = FNV_OFFSET;
|
|
16
16
|
for (let i = 0; i < s.length; i++) {
|
|
17
17
|
h = (h ^ BigInt(s.charCodeAt(i))) * FNV_PRIME & MASK64;
|
package/dist/tools/expenses.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { jsonResponse } from './_shared.js';
|
|
2
|
+
import { jsonResponse, requestWrite, UnconfirmedWriteError, unconfirmedWriteResponse } from './_shared.js';
|
|
3
|
+
import { CONFIRM_NOTE, confirmTokenParam, confirmWrite } from './_confirm.js';
|
|
3
4
|
import { offsetState, readUpstreamPaging, withPaginationFirst } from './pagination.js';
|
|
4
5
|
import { getWriteMode } from '../config.js';
|
|
5
6
|
export function registerExpenseTools(server, client) {
|
|
@@ -44,14 +45,47 @@ export function registerExpenseTools(server, client) {
|
|
|
44
45
|
});
|
|
45
46
|
if (allowWrites)
|
|
46
47
|
server.registerTool('ofw_create_expense', {
|
|
47
|
-
description: 'Log a new expense in OurFamilyWizard',
|
|
48
|
-
|
|
48
|
+
description: 'Log a new expense in OurFamilyWizard. The expense is a money claim that appears in the shared ledger in front of the co-parent immediately, and this server cannot delete it. If the request fails without a definitive answer the result is EXPENSE_UNCONFIRMED: the expense may already exist, so do NOT retry until ofw_list_expenses shows it did not land. ' + CONFIRM_NOTE,
|
|
49
|
+
// Not a harmless local write: the claim is co-parent-visible at once and
|
|
50
|
+
// this server has no way to take it back. destructiveHint keeps a host
|
|
51
|
+
// that auto-approves "non-destructive" tools from running it silently.
|
|
52
|
+
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
49
53
|
inputSchema: z.object({
|
|
50
54
|
amount: z.number().describe('Expense amount'),
|
|
51
55
|
description: z.string().describe('Expense description'),
|
|
56
|
+
confirmToken: confirmTokenParam,
|
|
52
57
|
}),
|
|
53
|
-
}, async (args) => {
|
|
54
|
-
const
|
|
58
|
+
}, async (args, ctx) => {
|
|
59
|
+
const { confirmToken, ...payload } = args;
|
|
60
|
+
const gate = await confirmWrite(ctx, {
|
|
61
|
+
tool: 'ofw_create_expense',
|
|
62
|
+
action: 'ofw.expense.create',
|
|
63
|
+
message: 'Review and confirm this OurFamilyWizard expense. It is logged in the shared ledger the co-parent sees immediately, and cannot be deleted through this server.',
|
|
64
|
+
target: 'expense:new',
|
|
65
|
+
payload,
|
|
66
|
+
preview: {
|
|
67
|
+
action: 'Log OurFamilyWizard expense',
|
|
68
|
+
amount: payload.amount,
|
|
69
|
+
description: payload.description,
|
|
70
|
+
warning: 'Visible to the co-parent immediately as a claim in the shared expense ledger; part of the court-visible record.',
|
|
71
|
+
},
|
|
72
|
+
confirmToken,
|
|
73
|
+
});
|
|
74
|
+
if (gate)
|
|
75
|
+
return gate;
|
|
76
|
+
let data;
|
|
77
|
+
try {
|
|
78
|
+
data = await requestWrite(client, 'POST', '/pub/v2/expense/expenses', payload);
|
|
79
|
+
}
|
|
80
|
+
catch (e) {
|
|
81
|
+
if (!(e instanceof UnconfirmedWriteError))
|
|
82
|
+
throw e;
|
|
83
|
+
return unconfirmedWriteResponse(e, {
|
|
84
|
+
result: 'EXPENSE_UNCONFIRMED',
|
|
85
|
+
what: 'log this expense',
|
|
86
|
+
checkWith: 'ofw_list_expenses (look for this amount and description among the newest expenses)',
|
|
87
|
+
});
|
|
88
|
+
}
|
|
55
89
|
return jsonResponse(data);
|
|
56
90
|
});
|
|
57
91
|
}
|
package/dist/tools/journal.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { jsonResponse } from './_shared.js';
|
|
2
|
+
import { jsonResponse, requestWrite, UnconfirmedWriteError, unconfirmedWriteResponse } from './_shared.js';
|
|
3
3
|
import { offsetState, readUpstreamPaging, withPaginationFirst } from './pagination.js';
|
|
4
4
|
import { getWriteMode } from '../config.js';
|
|
5
5
|
export function registerJournalTools(server, client) {
|
|
@@ -45,7 +45,19 @@ export function registerJournalTools(server, client) {
|
|
|
45
45
|
body: z.string().describe('Entry text content'),
|
|
46
46
|
}),
|
|
47
47
|
}, async (args) => {
|
|
48
|
-
|
|
48
|
+
let data;
|
|
49
|
+
try {
|
|
50
|
+
data = await requestWrite(client, 'POST', '/pub/v1/journals', args);
|
|
51
|
+
}
|
|
52
|
+
catch (e) {
|
|
53
|
+
if (!(e instanceof UnconfirmedWriteError))
|
|
54
|
+
throw e;
|
|
55
|
+
return unconfirmedWriteResponse(e, {
|
|
56
|
+
result: 'JOURNAL_UNCONFIRMED',
|
|
57
|
+
what: 'create this journal entry',
|
|
58
|
+
checkWith: 'ofw_list_journal_entries (look for this title among the newest entries)',
|
|
59
|
+
});
|
|
60
|
+
}
|
|
49
61
|
return jsonResponse(data);
|
|
50
62
|
});
|
|
51
63
|
}
|