@fullwell/fullwell 1.1.11 → 1.1.12
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/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/CHANGELOG.md +7 -0
- package/README.md +9 -1
- package/install-metadata.json +1 -1
- package/package.json +1 -1
- package/references/mcp-tool-contract.md +12 -1
- package/references/meal-planning-and-food-constraints.md +53 -0
- package/references/privacy-and-sharing.md +8 -0
- package/references/semantic-food-rules.md +4 -0
- package/references/weekly-meal-planning-automation.md +69 -0
- package/runtime/local-household-mcp.mjs +325 -21
- package/runtime/local-household.mjs +1128 -49
- package/runtime/local-profile.mjs +146 -0
- package/runtime/local-recipe-board.mjs +358 -0
- package/runtime/local-runner-control.mjs +61 -0
- package/skills/manage-household-food-journal/SKILL.md +45 -20
- package/skills/plan-household-meals/SKILL.md +57 -0
|
@@ -8,11 +8,24 @@ import {
|
|
|
8
8
|
activeCodexHome,
|
|
9
9
|
runRequest,
|
|
10
10
|
} from "./local-household.mjs";
|
|
11
|
+
import { loadLocalProfile, updateLocalProfile } from "./local-profile.mjs";
|
|
12
|
+
import { createLocalRecipeBoard } from "./local-recipe-board.mjs";
|
|
13
|
+
import { stopLocalWhatsAppRunner } from "./local-runner-control.mjs";
|
|
11
14
|
|
|
12
15
|
const SERVER_NAME = "fullwell-local";
|
|
13
16
|
const SERVER_VERSION = "1";
|
|
14
17
|
const MAX_MESSAGE_BYTES = 20 * 1024 * 1024;
|
|
15
|
-
const UPDATE_OPERATIONS = new Set([
|
|
18
|
+
const UPDATE_OPERATIONS = new Set([
|
|
19
|
+
"initialize",
|
|
20
|
+
"save",
|
|
21
|
+
"rename_household",
|
|
22
|
+
"finalize",
|
|
23
|
+
"record_cloud_backup",
|
|
24
|
+
"save_meal_planning_profile",
|
|
25
|
+
"review_meal_constraints",
|
|
26
|
+
"append_meal_proposal",
|
|
27
|
+
"record_meal_plan_event",
|
|
28
|
+
]);
|
|
16
29
|
|
|
17
30
|
const emptyObjectSchema = {
|
|
18
31
|
type: "object",
|
|
@@ -24,15 +37,194 @@ const revisionSchema = {
|
|
|
24
37
|
type: "integer",
|
|
25
38
|
minimum: 1,
|
|
26
39
|
};
|
|
40
|
+
const expectedProfileRevisionSchema = {
|
|
41
|
+
type: "integer",
|
|
42
|
+
minimum: 0,
|
|
43
|
+
};
|
|
44
|
+
const localDisplayNameSchema = {
|
|
45
|
+
type: "string",
|
|
46
|
+
minLength: 1,
|
|
47
|
+
maxLength: 108,
|
|
48
|
+
};
|
|
49
|
+
const householdNameSchema = {
|
|
50
|
+
type: "string",
|
|
51
|
+
minLength: 1,
|
|
52
|
+
maxLength: 120,
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const nullableStringSchema = { type: ["string", "null"] };
|
|
56
|
+
const localMealSlotSchema = {
|
|
57
|
+
oneOf: [
|
|
58
|
+
{
|
|
59
|
+
type: "object",
|
|
60
|
+
required: ["kind"],
|
|
61
|
+
properties: { kind: { type: "string", enum: ["breakfast", "lunch", "dinner", "snack"] } },
|
|
62
|
+
additionalProperties: false,
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
type: "object",
|
|
66
|
+
required: ["kind", "label"],
|
|
67
|
+
properties: {
|
|
68
|
+
kind: { const: "custom" },
|
|
69
|
+
label: { type: "string", minLength: 1, maxLength: 80 },
|
|
70
|
+
},
|
|
71
|
+
additionalProperties: false,
|
|
72
|
+
},
|
|
73
|
+
],
|
|
74
|
+
};
|
|
75
|
+
const localMealSourceSchema = {
|
|
76
|
+
oneOf: [
|
|
77
|
+
{
|
|
78
|
+
type: "object",
|
|
79
|
+
required: ["kind", "title"],
|
|
80
|
+
properties: {
|
|
81
|
+
kind: { const: "freeform" },
|
|
82
|
+
title: { type: "string", minLength: 1, maxLength: 300 },
|
|
83
|
+
},
|
|
84
|
+
additionalProperties: false,
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
type: "object",
|
|
88
|
+
required: ["kind", "item_id", "item_revision", "liked_evidence_ids"],
|
|
89
|
+
properties: {
|
|
90
|
+
kind: { const: "journal_recipe" },
|
|
91
|
+
item_id: { type: "string" },
|
|
92
|
+
item_revision: { type: "string" },
|
|
93
|
+
liked_evidence_ids: { type: "array", minItems: 1, maxItems: 100, items: { type: "string" } },
|
|
94
|
+
},
|
|
95
|
+
additionalProperties: false,
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
type: "object",
|
|
99
|
+
required: ["kind", "title", "canonical_url", "site_name", "discovered_at"],
|
|
100
|
+
properties: {
|
|
101
|
+
kind: { const: "external_recipe" },
|
|
102
|
+
title: { type: "string", minLength: 1, maxLength: 300 },
|
|
103
|
+
canonical_url: { type: "string", format: "uri" },
|
|
104
|
+
site_name: { type: "string", minLength: 1, maxLength: 200 },
|
|
105
|
+
discovered_at: { type: "string", format: "date-time" },
|
|
106
|
+
},
|
|
107
|
+
additionalProperties: false,
|
|
108
|
+
},
|
|
109
|
+
],
|
|
110
|
+
};
|
|
111
|
+
const mealConstraintsSchema = {
|
|
112
|
+
oneOf: [
|
|
113
|
+
{
|
|
114
|
+
type: "object",
|
|
115
|
+
required: ["status", "time_zone", "reviewed_at"],
|
|
116
|
+
properties: {
|
|
117
|
+
status: { const: "confirmed_none" },
|
|
118
|
+
time_zone: { type: "string" },
|
|
119
|
+
reviewed_at: { type: "string", format: "date-time" },
|
|
120
|
+
},
|
|
121
|
+
additionalProperties: false,
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
type: "object",
|
|
125
|
+
required: ["status", "time_zone", "allergy_labels", "sensitivity_labels", "reviewed_at"],
|
|
126
|
+
properties: {
|
|
127
|
+
status: { const: "recorded" },
|
|
128
|
+
time_zone: { type: "string" },
|
|
129
|
+
allergy_labels: { type: "array", maxItems: 30, items: { type: "string", minLength: 1, maxLength: 120 } },
|
|
130
|
+
sensitivity_labels: { type: "array", maxItems: 30, items: { type: "string", minLength: 1, maxLength: 120 } },
|
|
131
|
+
reviewed_at: { type: "string", format: "date-time" },
|
|
132
|
+
},
|
|
133
|
+
additionalProperties: false,
|
|
134
|
+
},
|
|
135
|
+
],
|
|
136
|
+
};
|
|
137
|
+
const idempotencyKeySchema = { type: "string", minLength: 8, maxLength: 128 };
|
|
138
|
+
const actorLabelSchema = { type: "string", minLength: 1, maxLength: 80 };
|
|
139
|
+
const weekStartSchema = { type: "string", format: "date" };
|
|
140
|
+
const withdrawalEventSchema = {
|
|
141
|
+
type: "object",
|
|
142
|
+
required: ["kind", "proposal_id", "reason"],
|
|
143
|
+
properties: {
|
|
144
|
+
kind: { const: "proposal_withdrawn" },
|
|
145
|
+
proposal_id: { type: "string" },
|
|
146
|
+
reason: nullableStringSchema,
|
|
147
|
+
},
|
|
148
|
+
additionalProperties: false,
|
|
149
|
+
};
|
|
150
|
+
const updateOperationSchema = (operation, properties = {}) => ({
|
|
151
|
+
type: "object",
|
|
152
|
+
required: ["operation", ...Object.keys(properties)],
|
|
153
|
+
properties: { operation: { const: operation }, ...properties },
|
|
154
|
+
additionalProperties: false,
|
|
155
|
+
});
|
|
156
|
+
const householdUpdateSchema = {
|
|
157
|
+
oneOf: [
|
|
158
|
+
{
|
|
159
|
+
type: "object",
|
|
160
|
+
required: ["operation"],
|
|
161
|
+
properties: {
|
|
162
|
+
operation: { const: "initialize" },
|
|
163
|
+
household_name: householdNameSchema,
|
|
164
|
+
},
|
|
165
|
+
additionalProperties: false,
|
|
166
|
+
},
|
|
167
|
+
updateOperationSchema("save", {
|
|
168
|
+
expected_revision: revisionSchema,
|
|
169
|
+
journal: { type: "object" },
|
|
170
|
+
}),
|
|
171
|
+
updateOperationSchema("rename_household", {
|
|
172
|
+
expected_revision: revisionSchema,
|
|
173
|
+
household_name: householdNameSchema,
|
|
174
|
+
}),
|
|
175
|
+
updateOperationSchema("finalize", { expected_revision: revisionSchema }),
|
|
176
|
+
updateOperationSchema("record_cloud_backup", {
|
|
177
|
+
expected_revision: revisionSchema,
|
|
178
|
+
user_id: { type: "string" },
|
|
179
|
+
household_id: { type: "string" },
|
|
180
|
+
repository_head: { type: "string" },
|
|
181
|
+
}),
|
|
182
|
+
updateOperationSchema("save_meal_planning_profile", {
|
|
183
|
+
expected_revision: revisionSchema,
|
|
184
|
+
idempotency_key: idempotencyKeySchema,
|
|
185
|
+
actor_label: actorLabelSchema,
|
|
186
|
+
constraints: mealConstraintsSchema,
|
|
187
|
+
}),
|
|
188
|
+
updateOperationSchema("review_meal_constraints", {
|
|
189
|
+
expected_revision: revisionSchema,
|
|
190
|
+
idempotency_key: idempotencyKeySchema,
|
|
191
|
+
actor_label: actorLabelSchema,
|
|
192
|
+
week_start: weekStartSchema,
|
|
193
|
+
constraint_revision: revisionSchema,
|
|
194
|
+
}),
|
|
195
|
+
updateOperationSchema("append_meal_proposal", {
|
|
196
|
+
expected_revision: revisionSchema,
|
|
197
|
+
idempotency_key: idempotencyKeySchema,
|
|
198
|
+
actor_label: actorLabelSchema,
|
|
199
|
+
week_start: weekStartSchema,
|
|
200
|
+
meal_date: { type: "string", format: "date" },
|
|
201
|
+
slot: localMealSlotSchema,
|
|
202
|
+
source: localMealSourceSchema,
|
|
203
|
+
servings: { type: ["integer", "null"], minimum: 1, maximum: 100 },
|
|
204
|
+
notes: nullableStringSchema,
|
|
205
|
+
constraint_revision: revisionSchema,
|
|
206
|
+
constraint_review_event_id: { type: "string" },
|
|
207
|
+
compatibility: { type: "string", enum: ["appears_compatible", "incomplete_evidence", "needs_recheck"] },
|
|
208
|
+
compatibility_caveat: { type: "string", minLength: 1, maxLength: 1_000 },
|
|
209
|
+
}),
|
|
210
|
+
updateOperationSchema("record_meal_plan_event", {
|
|
211
|
+
expected_revision: revisionSchema,
|
|
212
|
+
idempotency_key: idempotencyKeySchema,
|
|
213
|
+
actor_label: actorLabelSchema,
|
|
214
|
+
week_start: weekStartSchema,
|
|
215
|
+
event: withdrawalEventSchema,
|
|
216
|
+
}),
|
|
217
|
+
],
|
|
218
|
+
};
|
|
27
219
|
|
|
28
220
|
const localTools = [
|
|
29
221
|
{
|
|
30
|
-
name: "
|
|
31
|
-
title: "Load Fullwell's private local
|
|
32
|
-
description: "Loads the
|
|
222
|
+
name: "fullwell_local_profile_load",
|
|
223
|
+
title: "Load Fullwell's private local member profile",
|
|
224
|
+
description: "Loads the remembered local member display name and its deterministic first-household name without contacting Fullwell's cloud service.",
|
|
33
225
|
inputSchema: emptyObjectSchema,
|
|
34
226
|
annotations: {
|
|
35
|
-
title: "Load local Fullwell
|
|
227
|
+
title: "Load local Fullwell member profile",
|
|
36
228
|
readOnlyHint: true,
|
|
37
229
|
destructiveHint: false,
|
|
38
230
|
idempotentHint: true,
|
|
@@ -40,25 +232,44 @@ const localTools = [
|
|
|
40
232
|
},
|
|
41
233
|
},
|
|
42
234
|
{
|
|
43
|
-
name: "
|
|
44
|
-
title: "Update Fullwell's private local
|
|
45
|
-
description: "
|
|
235
|
+
name: "fullwell_local_profile_update",
|
|
236
|
+
title: "Update Fullwell's private local member profile",
|
|
237
|
+
description: "Creates or revision-checks the remembered local member display name without changing household authority or contacting Fullwell's cloud service.",
|
|
46
238
|
inputSchema: {
|
|
47
239
|
type: "object",
|
|
48
|
-
required: ["
|
|
240
|
+
required: ["expected_revision", "display_name"],
|
|
49
241
|
properties: {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
enum: ["initialize", "save", "finalize", "record_cloud_backup"],
|
|
53
|
-
},
|
|
54
|
-
expected_revision: revisionSchema,
|
|
55
|
-
journal: { type: "object" },
|
|
56
|
-
user_id: { type: "string" },
|
|
57
|
-
household_id: { type: "string" },
|
|
58
|
-
repository_head: { type: "string" },
|
|
242
|
+
expected_revision: expectedProfileRevisionSchema,
|
|
243
|
+
display_name: localDisplayNameSchema,
|
|
59
244
|
},
|
|
60
245
|
additionalProperties: false,
|
|
61
246
|
},
|
|
247
|
+
annotations: {
|
|
248
|
+
title: "Update local Fullwell member profile",
|
|
249
|
+
readOnlyHint: false,
|
|
250
|
+
destructiveHint: false,
|
|
251
|
+
idempotentHint: false,
|
|
252
|
+
openWorldHint: false,
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
name: "fullwell_local_household_load",
|
|
257
|
+
title: "Load Fullwell's private local household journal",
|
|
258
|
+
description: "Loads the bounded local Fullwell guest household from the active Codex home without contacting Fullwell's cloud service.",
|
|
259
|
+
inputSchema: emptyObjectSchema,
|
|
260
|
+
annotations: {
|
|
261
|
+
title: "Load local Fullwell household",
|
|
262
|
+
readOnlyHint: true,
|
|
263
|
+
destructiveHint: false,
|
|
264
|
+
idempotentHint: true,
|
|
265
|
+
openWorldHint: false,
|
|
266
|
+
},
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
name: "fullwell_local_household_update",
|
|
270
|
+
title: "Update Fullwell's private local household journal",
|
|
271
|
+
description: "Initializes, revision-checks, saves, finalizes, records cloud linkage, or appends validated meal-planning state for the bounded local Fullwell guest household.",
|
|
272
|
+
inputSchema: householdUpdateSchema,
|
|
62
273
|
annotations: {
|
|
63
274
|
title: "Update local Fullwell household",
|
|
64
275
|
readOnlyHint: false,
|
|
@@ -85,6 +296,81 @@ const localTools = [
|
|
|
85
296
|
openWorldHint: false,
|
|
86
297
|
},
|
|
87
298
|
},
|
|
299
|
+
{
|
|
300
|
+
name: "fullwell_local_recipe_board_create",
|
|
301
|
+
title: "Create a private local Fullwell recipe board",
|
|
302
|
+
description: "Creates one bounded static recipe-board snapshot beneath Fullwell's private local directory without searching, fetching images, opening a browser, or changing a journal.",
|
|
303
|
+
inputSchema: {
|
|
304
|
+
type: "object",
|
|
305
|
+
required: ["idempotency_key", "title", "context_label", "cards"],
|
|
306
|
+
properties: {
|
|
307
|
+
idempotency_key: { type: "string", minLength: 8, maxLength: 128 },
|
|
308
|
+
title: { type: "string", minLength: 1, maxLength: 300 },
|
|
309
|
+
context_label: nullableStringSchema,
|
|
310
|
+
cards: {
|
|
311
|
+
type: "array",
|
|
312
|
+
minItems: 1,
|
|
313
|
+
maxItems: 48,
|
|
314
|
+
items: {
|
|
315
|
+
type: "object",
|
|
316
|
+
required: [
|
|
317
|
+
"id",
|
|
318
|
+
"title",
|
|
319
|
+
"image_url",
|
|
320
|
+
"image_page_url",
|
|
321
|
+
"recipe_url",
|
|
322
|
+
"source_label",
|
|
323
|
+
"why_recommended",
|
|
324
|
+
"journal_statuses",
|
|
325
|
+
"proposed_slot",
|
|
326
|
+
"compatibility",
|
|
327
|
+
"compatibility_caveat",
|
|
328
|
+
],
|
|
329
|
+
properties: {
|
|
330
|
+
id: { type: "string", minLength: 1, maxLength: 120 },
|
|
331
|
+
title: { type: "string", minLength: 1, maxLength: 300 },
|
|
332
|
+
image_url: nullableStringSchema,
|
|
333
|
+
image_page_url: nullableStringSchema,
|
|
334
|
+
recipe_url: nullableStringSchema,
|
|
335
|
+
source_label: { type: "string", minLength: 1, maxLength: 200 },
|
|
336
|
+
why_recommended: { type: "string", minLength: 1, maxLength: 1_000 },
|
|
337
|
+
journal_statuses: {
|
|
338
|
+
type: "array",
|
|
339
|
+
maxItems: 3,
|
|
340
|
+
uniqueItems: true,
|
|
341
|
+
items: { type: "string", enum: ["Saved", "Cooked", "Liked"] },
|
|
342
|
+
},
|
|
343
|
+
proposed_slot: nullableStringSchema,
|
|
344
|
+
compatibility: { type: "string", enum: ["appears_compatible", "incomplete_evidence", "needs_recheck"] },
|
|
345
|
+
compatibility_caveat: { type: "string", minLength: 1, maxLength: 1_000 },
|
|
346
|
+
},
|
|
347
|
+
additionalProperties: false,
|
|
348
|
+
},
|
|
349
|
+
},
|
|
350
|
+
},
|
|
351
|
+
additionalProperties: false,
|
|
352
|
+
},
|
|
353
|
+
annotations: {
|
|
354
|
+
title: "Create private recipe board",
|
|
355
|
+
readOnlyHint: false,
|
|
356
|
+
destructiveHint: false,
|
|
357
|
+
idempotentHint: true,
|
|
358
|
+
openWorldHint: false,
|
|
359
|
+
},
|
|
360
|
+
},
|
|
361
|
+
{
|
|
362
|
+
name: "fullwell_local_whatsapp_runner_stop",
|
|
363
|
+
title: "Stop the local Fullwell WhatsApp runner",
|
|
364
|
+
description: "Stops and removes only the macOS Fullwell runner LaunchAgent while preserving its cloud connection, Keychain credentials, snapshots, receipts, and local journal.",
|
|
365
|
+
inputSchema: emptyObjectSchema,
|
|
366
|
+
annotations: {
|
|
367
|
+
title: "Stop local Fullwell WhatsApp runner",
|
|
368
|
+
readOnlyHint: false,
|
|
369
|
+
destructiveHint: true,
|
|
370
|
+
idempotentHint: true,
|
|
371
|
+
openWorldHint: false,
|
|
372
|
+
},
|
|
373
|
+
},
|
|
88
374
|
];
|
|
89
375
|
|
|
90
376
|
function jsonRpcResult(id, result) {
|
|
@@ -133,8 +419,15 @@ function assertEmptyArguments(value) {
|
|
|
133
419
|
}
|
|
134
420
|
}
|
|
135
421
|
|
|
136
|
-
async function callLocalTool(root, name, input) {
|
|
422
|
+
async function callLocalTool(root, name, input, controls) {
|
|
137
423
|
try {
|
|
424
|
+
if (name === "fullwell_local_profile_load") {
|
|
425
|
+
assertEmptyArguments(input);
|
|
426
|
+
return toolResult(await loadLocalProfile(root));
|
|
427
|
+
}
|
|
428
|
+
if (name === "fullwell_local_profile_update") {
|
|
429
|
+
return toolResult(await updateLocalProfile(root, assertPlainObject(input, "arguments")));
|
|
430
|
+
}
|
|
138
431
|
if (name === "fullwell_local_household_load") {
|
|
139
432
|
assertEmptyArguments(input);
|
|
140
433
|
return toolResult(await runRequest(root, { operation: "load" }));
|
|
@@ -150,6 +443,13 @@ async function callLocalTool(root, name, input) {
|
|
|
150
443
|
const args = assertPlainObject(input, "arguments");
|
|
151
444
|
return toolResult(await runRequest(root, { ...args, operation: "delete_collecting" }));
|
|
152
445
|
}
|
|
446
|
+
if (name === "fullwell_local_recipe_board_create") {
|
|
447
|
+
return toolResult(await createLocalRecipeBoard(root, assertPlainObject(input, "arguments")));
|
|
448
|
+
}
|
|
449
|
+
if (name === "fullwell_local_whatsapp_runner_stop") {
|
|
450
|
+
assertEmptyArguments(input);
|
|
451
|
+
return toolResult(await controls.stopRunner());
|
|
452
|
+
}
|
|
153
453
|
return null;
|
|
154
454
|
} catch (error) {
|
|
155
455
|
return toolError(error);
|
|
@@ -162,7 +462,11 @@ async function callLocalTool(root, name, input) {
|
|
|
162
462
|
* Domain validation remains in `local-household.mjs`, so the stdio adapter
|
|
163
463
|
* cannot drift from the file, revision, size, or prohibited-data boundary.
|
|
164
464
|
*/
|
|
165
|
-
export async function handleLocalHouseholdMcpMessage(
|
|
465
|
+
export async function handleLocalHouseholdMcpMessage(
|
|
466
|
+
root,
|
|
467
|
+
message,
|
|
468
|
+
controls = { stopRunner: stopLocalWhatsAppRunner },
|
|
469
|
+
) {
|
|
166
470
|
if (message === null || typeof message !== "object" || Array.isArray(message) || message.jsonrpc !== "2.0") {
|
|
167
471
|
return jsonRpcError(null, -32600, "Invalid Request");
|
|
168
472
|
}
|
|
@@ -183,7 +487,7 @@ export async function handleLocalHouseholdMcpMessage(root, message) {
|
|
|
183
487
|
if (message.method === "tools/call") {
|
|
184
488
|
const name = message.params?.name;
|
|
185
489
|
if (typeof name !== "string") return jsonRpcError(id, -32602, "Invalid params");
|
|
186
|
-
const result = await callLocalTool(root, name, message.params?.arguments ?? {});
|
|
490
|
+
const result = await callLocalTool(root, name, message.params?.arguments ?? {}, controls);
|
|
187
491
|
return result === null
|
|
188
492
|
? jsonRpcError(id, -32602, `Unknown local Fullwell tool: ${name}`)
|
|
189
493
|
: jsonRpcResult(id, result);
|