@fullwell/fullwell 1.1.11 → 1.1.13
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 +12 -0
- package/README.md +9 -1
- package/install-metadata.json +2 -2
- 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 +326 -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 +46 -21
- 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,195 @@ 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
|
+
type: "object",
|
|
158
|
+
oneOf: [
|
|
159
|
+
{
|
|
160
|
+
type: "object",
|
|
161
|
+
required: ["operation"],
|
|
162
|
+
properties: {
|
|
163
|
+
operation: { const: "initialize" },
|
|
164
|
+
household_name: householdNameSchema,
|
|
165
|
+
},
|
|
166
|
+
additionalProperties: false,
|
|
167
|
+
},
|
|
168
|
+
updateOperationSchema("save", {
|
|
169
|
+
expected_revision: revisionSchema,
|
|
170
|
+
journal: { type: "object" },
|
|
171
|
+
}),
|
|
172
|
+
updateOperationSchema("rename_household", {
|
|
173
|
+
expected_revision: revisionSchema,
|
|
174
|
+
household_name: householdNameSchema,
|
|
175
|
+
}),
|
|
176
|
+
updateOperationSchema("finalize", { expected_revision: revisionSchema }),
|
|
177
|
+
updateOperationSchema("record_cloud_backup", {
|
|
178
|
+
expected_revision: revisionSchema,
|
|
179
|
+
user_id: { type: "string" },
|
|
180
|
+
household_id: { type: "string" },
|
|
181
|
+
repository_head: { type: "string" },
|
|
182
|
+
}),
|
|
183
|
+
updateOperationSchema("save_meal_planning_profile", {
|
|
184
|
+
expected_revision: revisionSchema,
|
|
185
|
+
idempotency_key: idempotencyKeySchema,
|
|
186
|
+
actor_label: actorLabelSchema,
|
|
187
|
+
constraints: mealConstraintsSchema,
|
|
188
|
+
}),
|
|
189
|
+
updateOperationSchema("review_meal_constraints", {
|
|
190
|
+
expected_revision: revisionSchema,
|
|
191
|
+
idempotency_key: idempotencyKeySchema,
|
|
192
|
+
actor_label: actorLabelSchema,
|
|
193
|
+
week_start: weekStartSchema,
|
|
194
|
+
constraint_revision: revisionSchema,
|
|
195
|
+
}),
|
|
196
|
+
updateOperationSchema("append_meal_proposal", {
|
|
197
|
+
expected_revision: revisionSchema,
|
|
198
|
+
idempotency_key: idempotencyKeySchema,
|
|
199
|
+
actor_label: actorLabelSchema,
|
|
200
|
+
week_start: weekStartSchema,
|
|
201
|
+
meal_date: { type: "string", format: "date" },
|
|
202
|
+
slot: localMealSlotSchema,
|
|
203
|
+
source: localMealSourceSchema,
|
|
204
|
+
servings: { type: ["integer", "null"], minimum: 1, maximum: 100 },
|
|
205
|
+
notes: nullableStringSchema,
|
|
206
|
+
constraint_revision: revisionSchema,
|
|
207
|
+
constraint_review_event_id: { type: "string" },
|
|
208
|
+
compatibility: { type: "string", enum: ["appears_compatible", "incomplete_evidence", "needs_recheck"] },
|
|
209
|
+
compatibility_caveat: { type: "string", minLength: 1, maxLength: 1_000 },
|
|
210
|
+
}),
|
|
211
|
+
updateOperationSchema("record_meal_plan_event", {
|
|
212
|
+
expected_revision: revisionSchema,
|
|
213
|
+
idempotency_key: idempotencyKeySchema,
|
|
214
|
+
actor_label: actorLabelSchema,
|
|
215
|
+
week_start: weekStartSchema,
|
|
216
|
+
event: withdrawalEventSchema,
|
|
217
|
+
}),
|
|
218
|
+
],
|
|
219
|
+
};
|
|
27
220
|
|
|
28
221
|
const localTools = [
|
|
29
222
|
{
|
|
30
|
-
name: "
|
|
31
|
-
title: "Load Fullwell's private local
|
|
32
|
-
description: "Loads the
|
|
223
|
+
name: "fullwell_local_profile_load",
|
|
224
|
+
title: "Load Fullwell's private local member profile",
|
|
225
|
+
description: "Loads the remembered local member display name and its deterministic first-household name without contacting Fullwell's cloud service.",
|
|
33
226
|
inputSchema: emptyObjectSchema,
|
|
34
227
|
annotations: {
|
|
35
|
-
title: "Load local Fullwell
|
|
228
|
+
title: "Load local Fullwell member profile",
|
|
36
229
|
readOnlyHint: true,
|
|
37
230
|
destructiveHint: false,
|
|
38
231
|
idempotentHint: true,
|
|
@@ -40,25 +233,44 @@ const localTools = [
|
|
|
40
233
|
},
|
|
41
234
|
},
|
|
42
235
|
{
|
|
43
|
-
name: "
|
|
44
|
-
title: "Update Fullwell's private local
|
|
45
|
-
description: "
|
|
236
|
+
name: "fullwell_local_profile_update",
|
|
237
|
+
title: "Update Fullwell's private local member profile",
|
|
238
|
+
description: "Creates or revision-checks the remembered local member display name without changing household authority or contacting Fullwell's cloud service.",
|
|
46
239
|
inputSchema: {
|
|
47
240
|
type: "object",
|
|
48
|
-
required: ["
|
|
241
|
+
required: ["expected_revision", "display_name"],
|
|
49
242
|
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" },
|
|
243
|
+
expected_revision: expectedProfileRevisionSchema,
|
|
244
|
+
display_name: localDisplayNameSchema,
|
|
59
245
|
},
|
|
60
246
|
additionalProperties: false,
|
|
61
247
|
},
|
|
248
|
+
annotations: {
|
|
249
|
+
title: "Update local Fullwell member profile",
|
|
250
|
+
readOnlyHint: false,
|
|
251
|
+
destructiveHint: false,
|
|
252
|
+
idempotentHint: false,
|
|
253
|
+
openWorldHint: false,
|
|
254
|
+
},
|
|
255
|
+
},
|
|
256
|
+
{
|
|
257
|
+
name: "fullwell_local_household_load",
|
|
258
|
+
title: "Load Fullwell's private local household journal",
|
|
259
|
+
description: "Loads the bounded local Fullwell guest household from the active Codex home without contacting Fullwell's cloud service.",
|
|
260
|
+
inputSchema: emptyObjectSchema,
|
|
261
|
+
annotations: {
|
|
262
|
+
title: "Load local Fullwell household",
|
|
263
|
+
readOnlyHint: true,
|
|
264
|
+
destructiveHint: false,
|
|
265
|
+
idempotentHint: true,
|
|
266
|
+
openWorldHint: false,
|
|
267
|
+
},
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
name: "fullwell_local_household_update",
|
|
271
|
+
title: "Update Fullwell's private local household journal",
|
|
272
|
+
description: "Initializes, revision-checks, saves, finalizes, records cloud linkage, or appends validated meal-planning state for the bounded local Fullwell guest household.",
|
|
273
|
+
inputSchema: householdUpdateSchema,
|
|
62
274
|
annotations: {
|
|
63
275
|
title: "Update local Fullwell household",
|
|
64
276
|
readOnlyHint: false,
|
|
@@ -85,6 +297,81 @@ const localTools = [
|
|
|
85
297
|
openWorldHint: false,
|
|
86
298
|
},
|
|
87
299
|
},
|
|
300
|
+
{
|
|
301
|
+
name: "fullwell_local_recipe_board_create",
|
|
302
|
+
title: "Create a private local Fullwell recipe board",
|
|
303
|
+
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.",
|
|
304
|
+
inputSchema: {
|
|
305
|
+
type: "object",
|
|
306
|
+
required: ["idempotency_key", "title", "context_label", "cards"],
|
|
307
|
+
properties: {
|
|
308
|
+
idempotency_key: { type: "string", minLength: 8, maxLength: 128 },
|
|
309
|
+
title: { type: "string", minLength: 1, maxLength: 300 },
|
|
310
|
+
context_label: nullableStringSchema,
|
|
311
|
+
cards: {
|
|
312
|
+
type: "array",
|
|
313
|
+
minItems: 1,
|
|
314
|
+
maxItems: 48,
|
|
315
|
+
items: {
|
|
316
|
+
type: "object",
|
|
317
|
+
required: [
|
|
318
|
+
"id",
|
|
319
|
+
"title",
|
|
320
|
+
"image_url",
|
|
321
|
+
"image_page_url",
|
|
322
|
+
"recipe_url",
|
|
323
|
+
"source_label",
|
|
324
|
+
"why_recommended",
|
|
325
|
+
"journal_statuses",
|
|
326
|
+
"proposed_slot",
|
|
327
|
+
"compatibility",
|
|
328
|
+
"compatibility_caveat",
|
|
329
|
+
],
|
|
330
|
+
properties: {
|
|
331
|
+
id: { type: "string", minLength: 1, maxLength: 120 },
|
|
332
|
+
title: { type: "string", minLength: 1, maxLength: 300 },
|
|
333
|
+
image_url: nullableStringSchema,
|
|
334
|
+
image_page_url: nullableStringSchema,
|
|
335
|
+
recipe_url: nullableStringSchema,
|
|
336
|
+
source_label: { type: "string", minLength: 1, maxLength: 200 },
|
|
337
|
+
why_recommended: { type: "string", minLength: 1, maxLength: 1_000 },
|
|
338
|
+
journal_statuses: {
|
|
339
|
+
type: "array",
|
|
340
|
+
maxItems: 3,
|
|
341
|
+
uniqueItems: true,
|
|
342
|
+
items: { type: "string", enum: ["Saved", "Cooked", "Liked"] },
|
|
343
|
+
},
|
|
344
|
+
proposed_slot: nullableStringSchema,
|
|
345
|
+
compatibility: { type: "string", enum: ["appears_compatible", "incomplete_evidence", "needs_recheck"] },
|
|
346
|
+
compatibility_caveat: { type: "string", minLength: 1, maxLength: 1_000 },
|
|
347
|
+
},
|
|
348
|
+
additionalProperties: false,
|
|
349
|
+
},
|
|
350
|
+
},
|
|
351
|
+
},
|
|
352
|
+
additionalProperties: false,
|
|
353
|
+
},
|
|
354
|
+
annotations: {
|
|
355
|
+
title: "Create private recipe board",
|
|
356
|
+
readOnlyHint: false,
|
|
357
|
+
destructiveHint: false,
|
|
358
|
+
idempotentHint: true,
|
|
359
|
+
openWorldHint: false,
|
|
360
|
+
},
|
|
361
|
+
},
|
|
362
|
+
{
|
|
363
|
+
name: "fullwell_local_whatsapp_runner_stop",
|
|
364
|
+
title: "Stop the local Fullwell WhatsApp runner",
|
|
365
|
+
description: "Stops and removes only the macOS Fullwell runner LaunchAgent while preserving its cloud connection, Keychain credentials, snapshots, receipts, and local journal.",
|
|
366
|
+
inputSchema: emptyObjectSchema,
|
|
367
|
+
annotations: {
|
|
368
|
+
title: "Stop local Fullwell WhatsApp runner",
|
|
369
|
+
readOnlyHint: false,
|
|
370
|
+
destructiveHint: true,
|
|
371
|
+
idempotentHint: true,
|
|
372
|
+
openWorldHint: false,
|
|
373
|
+
},
|
|
374
|
+
},
|
|
88
375
|
];
|
|
89
376
|
|
|
90
377
|
function jsonRpcResult(id, result) {
|
|
@@ -133,8 +420,15 @@ function assertEmptyArguments(value) {
|
|
|
133
420
|
}
|
|
134
421
|
}
|
|
135
422
|
|
|
136
|
-
async function callLocalTool(root, name, input) {
|
|
423
|
+
async function callLocalTool(root, name, input, controls) {
|
|
137
424
|
try {
|
|
425
|
+
if (name === "fullwell_local_profile_load") {
|
|
426
|
+
assertEmptyArguments(input);
|
|
427
|
+
return toolResult(await loadLocalProfile(root));
|
|
428
|
+
}
|
|
429
|
+
if (name === "fullwell_local_profile_update") {
|
|
430
|
+
return toolResult(await updateLocalProfile(root, assertPlainObject(input, "arguments")));
|
|
431
|
+
}
|
|
138
432
|
if (name === "fullwell_local_household_load") {
|
|
139
433
|
assertEmptyArguments(input);
|
|
140
434
|
return toolResult(await runRequest(root, { operation: "load" }));
|
|
@@ -150,6 +444,13 @@ async function callLocalTool(root, name, input) {
|
|
|
150
444
|
const args = assertPlainObject(input, "arguments");
|
|
151
445
|
return toolResult(await runRequest(root, { ...args, operation: "delete_collecting" }));
|
|
152
446
|
}
|
|
447
|
+
if (name === "fullwell_local_recipe_board_create") {
|
|
448
|
+
return toolResult(await createLocalRecipeBoard(root, assertPlainObject(input, "arguments")));
|
|
449
|
+
}
|
|
450
|
+
if (name === "fullwell_local_whatsapp_runner_stop") {
|
|
451
|
+
assertEmptyArguments(input);
|
|
452
|
+
return toolResult(await controls.stopRunner());
|
|
453
|
+
}
|
|
153
454
|
return null;
|
|
154
455
|
} catch (error) {
|
|
155
456
|
return toolError(error);
|
|
@@ -162,7 +463,11 @@ async function callLocalTool(root, name, input) {
|
|
|
162
463
|
* Domain validation remains in `local-household.mjs`, so the stdio adapter
|
|
163
464
|
* cannot drift from the file, revision, size, or prohibited-data boundary.
|
|
164
465
|
*/
|
|
165
|
-
export async function handleLocalHouseholdMcpMessage(
|
|
466
|
+
export async function handleLocalHouseholdMcpMessage(
|
|
467
|
+
root,
|
|
468
|
+
message,
|
|
469
|
+
controls = { stopRunner: stopLocalWhatsAppRunner },
|
|
470
|
+
) {
|
|
166
471
|
if (message === null || typeof message !== "object" || Array.isArray(message) || message.jsonrpc !== "2.0") {
|
|
167
472
|
return jsonRpcError(null, -32600, "Invalid Request");
|
|
168
473
|
}
|
|
@@ -183,7 +488,7 @@ export async function handleLocalHouseholdMcpMessage(root, message) {
|
|
|
183
488
|
if (message.method === "tools/call") {
|
|
184
489
|
const name = message.params?.name;
|
|
185
490
|
if (typeof name !== "string") return jsonRpcError(id, -32602, "Invalid params");
|
|
186
|
-
const result = await callLocalTool(root, name, message.params?.arguments ?? {});
|
|
491
|
+
const result = await callLocalTool(root, name, message.params?.arguments ?? {}, controls);
|
|
187
492
|
return result === null
|
|
188
493
|
? jsonRpcError(id, -32602, `Unknown local Fullwell tool: ${name}`)
|
|
189
494
|
: jsonRpcResult(id, result);
|