@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.
@@ -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(["initialize", "save", "finalize", "record_cloud_backup"]);
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: "fullwell_local_household_load",
31
- title: "Load Fullwell's private local household journal",
32
- description: "Loads the bounded local Fullwell guest household from the active Codex home without contacting Fullwell's cloud service.",
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 household",
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: "fullwell_local_household_update",
44
- title: "Update Fullwell's private local household journal",
45
- description: "Initializes, revision-checks, saves, finalizes, or records cloud linkage for the bounded local Fullwell guest household.",
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: ["operation"],
241
+ required: ["expected_revision", "display_name"],
49
242
  properties: {
50
- operation: {
51
- type: "string",
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(root, message) {
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);