@fullwell/fullwell 1.1.10 → 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.
@@ -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,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: "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.",
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 household",
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: "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.",
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: ["operation"],
240
+ required: ["expected_revision", "display_name"],
49
241
  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" },
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(root, message) {
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);