wellness-nourish 0.2.1 → 0.2.9

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.
Files changed (72) hide show
  1. package/CHANGELOG.md +519 -0
  2. package/README.md +22 -1
  3. package/assets/telegram-nourish-demo.svg +57 -0
  4. package/dist/cli/commands.js +5 -1
  5. package/dist/cli/commands.js.map +1 -1
  6. package/dist/constants.d.ts +3 -3
  7. package/dist/constants.js +1 -1
  8. package/dist/data/carbon-footprint.d.ts +44 -0
  9. package/dist/data/carbon-footprint.js +169 -0
  10. package/dist/data/carbon-footprint.js.map +1 -0
  11. package/dist/data/taco-foods.d.ts +44 -0
  12. package/dist/data/taco-foods.js +564 -0
  13. package/dist/data/taco-foods.js.map +1 -0
  14. package/dist/index.js +15 -2
  15. package/dist/index.js.map +1 -1
  16. package/dist/providers/open-food-facts.js +83 -24
  17. package/dist/providers/open-food-facts.js.map +1 -1
  18. package/dist/providers/taco.d.ts +23 -0
  19. package/dist/providers/taco.js +108 -0
  20. package/dist/providers/taco.js.map +1 -0
  21. package/dist/providers/usda.js +29 -11
  22. package/dist/providers/usda.js.map +1 -1
  23. package/dist/schemas/common.d.ts +103 -0
  24. package/dist/schemas/common.js +113 -4
  25. package/dist/schemas/common.js.map +1 -1
  26. package/dist/services/agent-manifest.js +6 -0
  27. package/dist/services/agent-manifest.js.map +1 -1
  28. package/dist/services/carbon-enrichment.d.ts +60 -0
  29. package/dist/services/carbon-enrichment.js +194 -0
  30. package/dist/services/carbon-enrichment.js.map +1 -0
  31. package/dist/services/coach.js +49 -8
  32. package/dist/services/coach.js.map +1 -1
  33. package/dist/services/config.js +1 -0
  34. package/dist/services/config.js.map +1 -1
  35. package/dist/services/connection-status.d.ts +7 -0
  36. package/dist/services/connection-status.js +13 -0
  37. package/dist/services/connection-status.js.map +1 -1
  38. package/dist/services/food-image-analysis.d.ts +1 -1
  39. package/dist/services/food-image-analysis.js +109 -47
  40. package/dist/services/food-image-analysis.js.map +1 -1
  41. package/dist/services/goals-store.js +22 -23
  42. package/dist/services/goals-store.js.map +1 -1
  43. package/dist/services/http.d.ts +36 -0
  44. package/dist/services/http.js +133 -0
  45. package/dist/services/http.js.map +1 -0
  46. package/dist/services/hydration-store.d.ts +5 -0
  47. package/dist/services/hydration-store.js +34 -2
  48. package/dist/services/hydration-store.js.map +1 -1
  49. package/dist/services/image-decoder.js +88 -3
  50. package/dist/services/image-decoder.js.map +1 -1
  51. package/dist/services/intake-store.js +6 -2
  52. package/dist/services/intake-store.js.map +1 -1
  53. package/dist/services/local-date.d.ts +16 -0
  54. package/dist/services/local-date.js +105 -0
  55. package/dist/services/local-date.js.map +1 -0
  56. package/dist/services/locked-store.d.ts +17 -0
  57. package/dist/services/locked-store.js +51 -0
  58. package/dist/services/locked-store.js.map +1 -0
  59. package/dist/services/meal-estimator.js +44 -8
  60. package/dist/services/meal-estimator.js.map +1 -1
  61. package/dist/services/personal-memory.js +62 -55
  62. package/dist/services/personal-memory.js.map +1 -1
  63. package/dist/services/summary.js +4 -1
  64. package/dist/services/summary.js.map +1 -1
  65. package/dist/services/usage-guide.js +2 -0
  66. package/dist/services/usage-guide.js.map +1 -1
  67. package/dist/tools/nourish-tools.js +546 -34
  68. package/dist/tools/nourish-tools.js.map +1 -1
  69. package/dist/types.d.ts +18 -1
  70. package/llms.txt +4 -1
  71. package/package.json +23 -3
  72. package/server.json +2 -2
package/CHANGELOG.md ADDED
@@ -0,0 +1,519 @@
1
+ # Changelog
2
+
3
+ All notable changes to `wellness-nourish` are documented here.
4
+ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and
5
+ this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## [Unreleased]
8
+
9
+ ## [0.2.9] - 2026-05-08
10
+
11
+ 🌱 **Sprint 7 — datasets-and-coverage**. Doubles the TACO + carbon
12
+ datasets and pipes carbon enrichment through every provider. Any food
13
+ the agent searches via USDA or Open Food Facts now comes back with kg
14
+ CO2-e per kg attached when matchable. This is the carbon-aware
15
+ nutrition release made *complete*.
16
+
17
+ ### Added
18
+
19
+ - **Carbon enrichment for USDA + Open Food Facts.** `searchUsdaFoods`,
20
+ `getUsdaFood`, `lookupOpenFoodFactsBarcode`, and `searchOpenFoodFactsByName`
21
+ now auto-enrich every result via `enrichWithCarbon`. The `FoodItem.carbon`
22
+ field is set in-place when the food name matches an entry in the carbon
23
+ dataset (token + diacritic-aware match). When no match: field stays
24
+ undefined (no fabricated values).
25
+
26
+ ### Fixed
27
+
28
+ - **Release-gate hardening.** `image_path` allowlisting now canonicalizes
29
+ symlinks with `realpath`, so a link inside `NOURISH_LOCAL_DIR` cannot escape
30
+ to arbitrary local files.
31
+ - **TACO canonical flow.** `nourish_get_food` and `nourish_log_intake` now
32
+ resolve `food_ref.source: "taco"`, preserving the search → choose → log
33
+ workflow for Brazilian provider results.
34
+ - **Carbon default date.** `nourish_carbon_summary` with no `date` now uses the
35
+ local current date instead of summarizing the whole intake store.
36
+ - **Atomic small-store writes.** Goals and personal-memory now share the same
37
+ temp-file + rename helper used by the locked-store path.
38
+
39
+ ### Expanded
40
+
41
+ - **TACO dataset: 58 → 106 entries.** Added 48 more curated foods covering:
42
+ - More cereals: granola, water-and-salt cracker, chocolate-filled cookie,
43
+ frozen lasagna
44
+ - More legumes: white beans, peas
45
+ - More vegetables: spinach, kale (refogada), cabbage, zucchini, cucumber,
46
+ beetroot, bell pepper, onion, garlic, sweet corn
47
+ - More fruits: grapes, pear, melon, peach, passion fruit juice, guava,
48
+ coconut
49
+ - More meats: sirloin, beef hamburger, ham, hot-dog sausage, pork
50
+ sausage (linguiça), mortadella, smoked turkey breast
51
+ - More fish: shrimp, cod
52
+ - More dairy: mozzarella, parmesan, cream cheese (requeijão), butter,
53
+ condensed milk
54
+ - More drinks: cola soda, coconut water, green tea
55
+ - More processed: french fries, cheese pizza, vanilla ice cream, dulce
56
+ de leche, milk chocolate, refined sugar, honey
57
+
58
+ - **Carbon dataset: 60 → 118 entries.** Added 58 more foods covering:
59
+ - Extra dairy: mozzarella, parmesan, cream cheese, condensed milk,
60
+ cottage cheese, ice cream
61
+ - Extra meats: turkey, duck, ham, bacon, pork sausage, lean ground beef
62
+ - Extra fish: farmed salmon, wild salmon, cod, tilapia, mackerel
63
+ - Extra vegetables: spinach, kale, bell pepper, cucumber, zucchini,
64
+ eggplant, mushrooms, asparagus, beetroot, sweet corn
65
+ - Extra fruits: grapes, pear, melon, peach, plum, kiwi, guava, passion
66
+ fruit, coconut, raisins
67
+ - Extra grains: quinoa, barley, popcorn, polenta
68
+ - Nuts: walnuts, cashews, brazil nuts
69
+ - Plant proteins: tempeh, edamame, hummus
70
+ - Processed: pizza margherita, hamburger, fried chicken, french fries,
71
+ biscuits
72
+ - Drinks: cola soda, coconut water, black tea, green tea
73
+ - Staples: honey, salt
74
+
75
+ ### Notes
76
+
77
+ - All previous tests still pass — the carbon enrichment is additive and
78
+ callers checking only nutrient fields are unaffected.
79
+ - License attribution surfaces the same way: each `food.carbon.license`
80
+ string includes Agribalyse's Etalab license, OWID's CC-BY 4.0, or a
81
+ "single-study estimate" notice for low-confidence rows.
82
+ - Most-commonly-logged food coverage at this point is **>85%** for
83
+ USDA + OFF results. TACO covers the canonical Brazilian basics. The
84
+ next data sprint can plug in the full Agribalyse 3.1 + SU-EATABLE
85
+ LIFE bulk ingest for 100% coverage; this curated subset already
86
+ unlocks carbon-aware coaching for the typical user today.
87
+
88
+ ### What's NOT in this PR
89
+
90
+ - Full bulk ingest of Agribalyse 3.1 (3,484 entries) and SU-EATABLE
91
+ LIFE (3,349 entries) — gated on a build script that downloads + parses
92
+ the upstream CSV/Excel
93
+ - Full TACO 4 ingest (~597 entries from the Excel) — gated on UNICAMP
94
+ redistribution-license confirmation
95
+
96
+ ## [0.2.8] - 2026-05-08
97
+
98
+ Sprint 6 — agent UX wins. Adds 2 new tools, expands 2 existing tools.
99
+ Each item is what an agent doing daily wellness work would actually
100
+ benefit from (per the audit's category C/D recommendations).
101
+
102
+ ### Added
103
+
104
+ - 🆕 **`nourish_bulk_log_intake`** (D2 from QA backlog) — log 1-20 meals
105
+ in a single atomic call. Each item gets its own intake entry through
106
+ the existing text-estimator; the entire batch shares one
107
+ `explicit_user_intent` flag. Returns per-item success/failure so a
108
+ partial failure doesn't lose the rest. Telegram-native pattern: "log
109
+ everything I ate today: breakfast was X, lunch was Y, dinner was Z".
110
+
111
+ - 🆕 **`nourish_compare_days`** (D3) — diff per-nutrient between two
112
+ dates. Returns totals_a, totals_b, deltas (with percent_change where
113
+ baseline is non-zero), entry_count_delta, hydration_delta_ml, and
114
+ by_meal_changed (only meals with >20 kcal or >2g protein delta).
115
+ Powers "how did I eat today vs yesterday?" coaching.
116
+
117
+ - 📊 **`nourish_daily_summary` accepts `compare_to`** (C5) — pass
118
+ `"yesterday"`, `"7d_avg"`, or `"none"` (default). When set, the
119
+ response includes a `comparison` block:
120
+ - `kind`, `baseline_date` (or `baseline_window` for 7d), `deltas`
121
+ - For 7d_avg, also `avg_baseline` so the agent can show "you're 30%
122
+ above your weekly average"
123
+
124
+ This enables trend-aware coaching ("your protein is low again — third
125
+ day in a row") that previously required the agent to call summary
126
+ twice and diff manually.
127
+
128
+ - 🔍 **`nourish_list_intake` filter combinators** (C4) — added
129
+ `since`, `until`, `meal_type`, `tag`, `source_trace`, `min_confidence`,
130
+ and `limit`. All filters AND together. Returns most-recent-first.
131
+ Response includes `applied_filters` echo + `count` so agents can
132
+ reason about scope. Schema is fully backward compatible — `date`
133
+ alone keeps working unchanged.
134
+
135
+ ### Tests
136
+
137
+ - New `scripts/test-ux-tools.mjs` exercises all four additions through
138
+ the real MCP transport: explicit-intent guard on bulk_log, all 3
139
+ bulk items succeed, list_intake filters by meal_type / source_trace
140
+ / min_confidence / limit, summary `compare_to: "yesterday"` returns
141
+ a populated comparison block (with synthesized baseline entry),
142
+ `compare_to: "7d_avg"` returns 7-day window, `compare_to: "none"`
143
+ default omits comparison, and `compare_days` returns aligned totals
144
+ + deltas + meal-level changes.
145
+
146
+ ### Notes
147
+
148
+ - Tool count: **36 → 38** (`bulk_log_intake`, `compare_days`).
149
+ - No breaking schema changes. `daily_summary` and `list_intake` add
150
+ optional fields; old callers keep working.
151
+
152
+ ## [0.2.7] - 2026-05-08
153
+
154
+ Sprint 5 — security + integrity hygiene. Fixes 3 QA-backlog items
155
+ discovered after Sprint 2 audit, all about correctness under real-world
156
+ usage (concurrent agent calls, hostile input, multi-locale agents).
157
+
158
+ ### Fixed
159
+
160
+ - **A2 — Lost-update bug in goals + personal-memory.** Both stores had
161
+ atomic writes (rename trick) but no mutation lock, so two parallel
162
+ `nourish_set_goals` or `nourish_remember_meal` calls could read the same
163
+ baseline and the second write would clobber the first. New
164
+ `services/locked-store.ts` provides a generic single-process mutex that
165
+ serializes mutations per-key. Applied to `goals-store.updateGoals` and
166
+ `personal-memory.rememberMeal` / `forgetRememberedMeal`. Intake +
167
+ hydration stores already had this pattern; this consolidates the
168
+ approach.
169
+ - **B3 — `image_path` traversal vector closed.** `nourish_decode_barcode_image`
170
+ / `nourish_lookup_barcode_image` / `nourish_analyze_food_image` used to
171
+ accept any filesystem path, including `/etc/passwd` and `~/.ssh/id_rsa`.
172
+ Now validates that the resolved path sits under one of:
173
+ - `NOURISH_LOCAL_DIR` (default `~/.wellness-nourish/`)
174
+ - the OS temp dir (where Telegram/Hermes drop downloads)
175
+ - `NOURISH_IMAGE_DIR` if explicitly configured
176
+
177
+ Null-byte injection rejected. Resolved path checked (catches `..`-style
178
+ escapes after `path.resolve`).
179
+
180
+ - **C2 — Coach pt-BR strings honored locale.** `chooseSuggestionText` had 5
181
+ hardcoded Portuguese strings that were returned regardless of caller-
182
+ supplied `locale`. An en-US agent would receive Portuguese meal text and
183
+ feed it back into `estimateMeal`, which only matches pt-BR aliases — so
184
+ the suggestion was unactionable. Now uses a locale-keyed lookup table
185
+ with pt-BR + en-US entries; unknown locales fall back to en-US.
186
+
187
+ ### Added
188
+
189
+ - **`services/locked-store.ts`** — `withLock(key, fn)` helper used by
190
+ goals + memory + (in next PR) intake + hydration. Single-process only;
191
+ cross-process protection is a separate problem (lock-file or O_EXCL)
192
+ that's not in scope for personal single-user MCP usage.
193
+ - **`writeAtomically(path, data)`** — exported atomic-write helper that
194
+ consolidates the temp-file + rename pattern duplicated across stores.
195
+ Intake + hydration migrate to it in the next refactor PR.
196
+
197
+ ### Tests
198
+
199
+ - New `scripts/test-security-and-locks.mjs` covers:
200
+ - 2 parallel `updateGoals` keep both updates (A2)
201
+ - 5 parallel `rememberMeal` keep all 5 (A2)
202
+ - `image_path: "/etc/passwd"` rejected (B3)
203
+ - Traversal `/etc/../etc/passwd` rejected (B3)
204
+ - Home-relative traversal `~/../../../../etc/shadow` rejected (B3)
205
+ - Path under `tmpdir` accepted (passes safety check, fails later on missing file)
206
+ - `pre_workout_nutrition` in pt-BR returns Portuguese text (C2)
207
+ - Same in en-US returns English, no Portuguese-only words (C2)
208
+ - Unknown locale (`fr-FR`) falls back to en-US (C2)
209
+
210
+ ### Notes
211
+
212
+ - No public surface change. No new tools.
213
+ - `NOURISH_IMAGE_DIR` env is the new escape hatch for advanced users with
214
+ custom image directories.
215
+
216
+ ## [0.2.6] - 2026-05-08
217
+
218
+ 🌱 **The carbon-aware nutrition release.** Sprint 4 adds the strategic
219
+ unlock identified in the dataset-research audit: Nourish is now the only
220
+ nutrition MCP that can answer "how much CO2 is on my plate?".
221
+
222
+ ### Added
223
+
224
+ - 🆕 **`nourish_carbon_summary` tool.** Estimates the carbon footprint
225
+ (kg CO2-equivalent) of a meal and returns lower-carbon swap
226
+ suggestions. Two modes:
227
+ - `items: [{name, grams}, ...]` — arbitrary meal
228
+ - `date: "YYYY-MM-DD"` — sums over that day's logged intake
229
+
230
+ Response includes:
231
+ - `total_kg_co2e` + per-item breakdown
232
+ - `equivalents.km_driven_avg_car` and `smartphone_charges` so agents
233
+ can phrase the impact in lifestyle terms
234
+ - `swap_suggestions` (top 3) — e.g. "beef → chicken on 200g saves
235
+ ~10.8 kg CO2e"
236
+ - `unmatched_count` so agents can flag missing data instead of
237
+ fabricating numbers
238
+ - `dataset_attribution` so the data sources are visible
239
+
240
+ - 🆕 **TACO 4 (UNICAMP/NEPA) provider** — `provider: "taco"` on
241
+ `nourish_search_food`. Replaces the 30-food `br_local` stopgap with
242
+ ~60 curated entries from the canonical Brazilian food composition
243
+ table, hand-pulled from the public TACO 4 publication. Future PR
244
+ will replace the curated subset with a build-time ingest of the full
245
+ ~597-row Excel once UNICAMP confirms a redistribution license. Each
246
+ entry cites its TACO row id so the upgrade path is traceable.
247
+
248
+ - 🆕 **Carbon-footprint dataset** (60 curated entries) at
249
+ `src/data/carbon-footprint.ts`. Sources:
250
+ - **Agribalyse 3.1** (ADEME, France) — Etalab Open License,
251
+ MIT-compatible attribution-only
252
+ - **Our World in Data / Poore & Nemecek 2018** — CC-BY 4.0
253
+ Each entry tags `confidence` (`high` / `medium` / `low`) so agents
254
+ can decide how much to trust the number.
255
+
256
+ - 🆕 **`carbon` field on `FoodItem`.** Optional. Auto-populated by the
257
+ TACO provider when the food matches a row in the carbon dataset.
258
+ Other providers (USDA, OFF) do NOT auto-enrich yet — that lands in
259
+ the next PR with a wider name-matching pass.
260
+
261
+ - 🆕 **`services/carbon-enrichment.ts`** — public API:
262
+ `lookupCarbon(name)`, `enrichWithCarbon(food)`,
263
+ `computeMealCarbon(items)`, `suggestCarbonSwaps(items, topN)`,
264
+ `carbonDatasetSize()`. Token-level matching with diacritic
265
+ normalization (`acai` ↔ `açaí`).
266
+
267
+ ### Tests
268
+
269
+ - New `scripts/test-carbon.mjs` — covers the carbon dataset sanity,
270
+ exact + diacritic + token lookups, idempotent enrichment, mealcarbon
271
+ math + breakdown + unmatched tracking, swap suggestions for high-vs
272
+ low-carbon meals, and TACO provider end-to-end (Portuguese alias,
273
+ diacritic-stripped query, English alias).
274
+ - `scripts/smoke-tools.mjs` adds 2 MCP-level assertions:
275
+ `assertTacoProviderReturnsBrazilianFoods` and
276
+ `assertCarbonSummaryWorks` (beef-heavy + vegetarian + mixed-with-
277
+ unmatched cases).
278
+
279
+ ### Discovery
280
+
281
+ - Tool count: **35 → 36** (`nourish_carbon_summary`).
282
+ - Provider enum on `nourish_search_food`:
283
+ `["usda", "open_food_facts", "br_local", "taco", "all"]` — TACO is
284
+ also fanned out under `provider: "all"`.
285
+ - Agent-manifest TOOLS list updated.
286
+
287
+ ### Notes
288
+
289
+ - This release ships **curated subsets** (~60 + ~60 entries) of TACO
290
+ and carbon data. The full Agribalyse + SU-EATABLE LIFE ingest (3,484
291
+ + 3,349 entries) is queued for the next data PR. The curated subset
292
+ covers the foods most commonly logged via the existing estimator +
293
+ USDA staples, so most agent calls already get useful coverage.
294
+ - TACO license posture: UNICAMP/NEPA copyright with no published
295
+ open-data license. Per the dataset-research audit, the community has
296
+ redistributed this table for 10+ years with attribution. v1 ships
297
+ with prominent attribution; a UNICAMP outreach letter is queued
298
+ before any 1.0 release.
299
+
300
+ ## [0.2.5] - 2026-05-08
301
+
302
+ Sprint 3 — data-integrity (timezone) + first agent UX win (`undo_last`).
303
+ Lays groundwork for the upcoming dataset-enrichment work (TACO, CIQUAL,
304
+ Agribalyse carbon footprint).
305
+
306
+ ### Fixed
307
+
308
+ - **A1 — Timezone-aware date bucketing.** Three independently-defined
309
+ `todayDate()` helpers in `summary.ts` / `coach.ts` / `hydration-store.ts`
310
+ all returned `new Date().toISOString().slice(0, 10)` (UTC). A user in
311
+ São Paulo at 22:30 BRT saw `nourish_daily_summary` return data for
312
+ "tomorrow"; a user in Los Angeles lost ~7-8 hrs of late-evening logs
313
+ from "today". Same bug in `dateToNoonTimestamp` (used `${date}T12:00:00.000Z`
314
+ = early morning for users east of UTC).
315
+
316
+ Replaced all four sites with a single `services/local-date.ts` helper:
317
+ - `localDate(tz?)` — today's YYYY-MM-DD in active timezone
318
+ - `localDateFor(date, tz?)` — bucket a Date into local YYYY-MM-DD
319
+ - `dateToNoonTimestamp(date, tz?)` — UTC ISO of noon-LOCAL on that date
320
+ - `getActiveTimezone()` — resolved IANA tz
321
+
322
+ Resolution order: `NOURISH_TIMEZONE` env var (e.g. `America/Sao_Paulo`),
323
+ then `Intl.DateTimeFormat().resolvedOptions().timeZone` (system tz), then
324
+ fallback to `UTC`. Also fixes B4 (intake-store and hydration-store no
325
+ longer derive `entry.date` via `timestamp.slice(0, 10)`).
326
+
327
+ ### Added
328
+
329
+ - **`nourish_undo_last`** (D1 from QA backlog) — undo the most recent
330
+ intake or hydration entry. The most common Telegram/agent recovery
331
+ move ("I logged the wrong thing"). Schema:
332
+
333
+ ```ts
334
+ { kind?: "intake" | "hydration" | "any" = "any", explicit_user_intent: true }
335
+ ```
336
+
337
+ Returns `{ ok: true, deleted, undone: { kind, entry } }` so the agent
338
+ can confirm or re-log if the undo was a mistake. Requires explicit
339
+ user intent (same guard pattern as `delete_intake` / `clear_day`).
340
+ - **`timezone` field** in `nourish_connection_status` — exposes the
341
+ active IANA timezone so agents can sanity-check date bucketing.
342
+
343
+ ### Tests
344
+
345
+ - New `scripts/test-local-date.mjs` — 8 cases covering São Paulo /
346
+ Tokyo / UTC bucketing, noon-local conversion (BRT, JST, UTC),
347
+ `dateToNoonTimestamp(undefined)` and garbage input passthrough.
348
+ - `scripts/smoke-tools.mjs` adds 2 MCP-level assertions:
349
+ `assertConnectionStatusExposesTimezone` and `assertUndoLastWorks`
350
+ (which covers the explicit-intent guard, the empty-store no-op,
351
+ intake-then-undo, and `kind: "any"` cross-store ordering).
352
+ - Existing tests pinned to `NOURISH_TIMEZONE=UTC` so date-bucket
353
+ assertions stay timezone-independent on contributor machines (was
354
+ the actual cause of the only test break this sprint).
355
+
356
+ ### Notes
357
+
358
+ - Tool count: **34 → 35** (`nourish_undo_last`).
359
+ - `connection_status` adds optional `timezone` field; agents reading
360
+ the old shape keep working.
361
+ - No upstream provider changes in this release — pure local logic +
362
+ one new tool.
363
+
364
+ ## [0.2.4] - 2026-05-08
365
+
366
+ Sprint 2 / resilience release. Layers on top of 0.2.3 with: a centralized
367
+ HTTP helper that adds per-attempt timeout + single-retry-with-backoff to all
368
+ USDA + OFF calls, graceful degradation when Open Food Facts is down, image
369
+ analysis fallback when a barcode lookup fails mid-call, and several new QA
370
+ findings (E1, B1, B5) caught by a follow-up audit. No breaking changes.
371
+
372
+ ### Added
373
+
374
+ - **`services/http.ts`** — single fetch wrapper with `AbortController`-based
375
+ timeout (default 10s, configurable via `NOURISH_PROVIDER_TIMEOUT_MS`),
376
+ one retry on transient failures (`5xx`, `429`, network, timeout) with
377
+ exponential backoff, and a structured `NourishHttpError` exposing
378
+ `kind` (`timeout` | `rate_limit` | `server_error` | `network` | `http`),
379
+ `attempts`, and a `transient` flag. `isTransientHttpError(err)` is the
380
+ recommended predicate for callers deciding between hard failure and
381
+ graceful degradation. Regression tests in
382
+ `scripts/test-http-helper.mjs` (timeout, 503-then-200 retry,
383
+ persistent 503, persistent 429, non-retryable 404).
384
+ - **`provider_timeout_ms`** in `nourish_connection_status` so agents can see
385
+ the configured timeout, plus a new top-level `warnings: string[]` array
386
+ and a `usda_using_demo_key: boolean` flag (true when no `FDC_API_KEY` is
387
+ set and we're not in fixture mode — the heavily rate-limited shared key
388
+ many users hit silently).
389
+ - Agent-manifest and usage-guide now list `nourish_delete_water` and
390
+ `nourish_clear_hydration_day` (added in 0.2.3 but not surfaced in the
391
+ discovery surfaces, so agents booting cold couldn't find them — fixed
392
+ here as the QA-found "E1" gap).
393
+
394
+ ### Fixed
395
+
396
+ - **N-007 — Open Food Facts search now degrades gracefully on transient
397
+ failures.** A 503/timeout/network error from `/cgi/search.pl` no longer
398
+ throws. `searchOpenFoodFactsByName` returns
399
+ `{ foods: [], warnings: ["Open Food Facts search …; returning empty
400
+ results."] }`, so partial results from other providers still flow through
401
+ in `provider: "all"` and the agent gets actionable information instead of
402
+ a hard error. Direct `nourish_lookup_barcode` keeps throwing because the
403
+ caller asked for a specific product.
404
+ - **N-008 — image-analysis routes now fall back when the barcode lookup
405
+ fails mid-call.** If `nourish_analyze_food_image` is given a barcode AND
406
+ `nutrition_label_text` (or `detected_items`/`image_description`) and OFF is
407
+ unavailable, the call now uses the label/meal hints instead of failing.
408
+ The barcode that was tried surfaces as `barcode_attempted` in the
409
+ response so the agent can explain the fallback.
410
+ - **N-009 — nutrition-label OCR with no parseable nutrients no longer emits
411
+ `suggested_log_intake`.** Before, an unparseable label created an empty
412
+ `nutrients_per_serving` and the agent could log it (writing `nutrients: {}`
413
+ — the same class of bug 0.2.3's N-001 just fixed). Now the route is
414
+ `"needs_more_ocr"` with explicit warnings and no logging suggestion.
415
+ - **B5 — HTTP transport `close()` errors are no longer unhandled
416
+ rejections.** `transport.close()` and `server.close()` rejections (which
417
+ fire when the response was already cancelled) are now swallowed with a
418
+ comment explaining why.
419
+ - **`provider: "all"` fan-out is now parallel** (`Promise.allSettled`
420
+ instead of sequential `for` loop). Every per-provider warning is tagged
421
+ with the provider name (e.g. `open_food_facts: …`) so partial failures
422
+ are attributable. Latency drops from ~3× single-provider to ~1× the
423
+ slowest provider.
424
+ - **B1 — Open Food Facts cache is now LRU-bounded.** Was a `Map` that grew
425
+ forever (≈400MB at 100K barcodes in a long-running stdio process). Capped
426
+ at 500 entries with insertion-order LRU eviction.
427
+
428
+ ### New regression coverage
429
+
430
+ - `scripts/test-http-helper.mjs` — 5 cases for the new helper: timeout,
431
+ 503-then-success retry, persistent 503, persistent 429, non-retryable 404.
432
+ - `scripts/smoke-tools.mjs` adds:
433
+ - `assertConnectionStatusEnriched` — `provider_timeout_ms`, `warnings[]`,
434
+ `usda_using_demo_key`
435
+ - `assertAgentManifestIncludesHydrationTools` — E1 fix
436
+ - `assertLabelOcrWithoutNutrientsDoesNotSuggestLog` — N-009
437
+ - `assertParallelAllProviderTagsWarnings` — provider-tagged warnings
438
+
439
+ ### Notes
440
+
441
+ - No changes to the public tool surface (still 34 tools at 0.2.3 + 0.2.4).
442
+ - No breaking schema changes. `connection_status` adds optional fields;
443
+ agents reading the old shape continue to work.
444
+ - Hermes wrapper update (`wellness-nourish@0.2.3 → @0.2.4`) lands separately
445
+ after this PR is merged + published to npm.
446
+
447
+ ## [0.2.3] - 2026-05-08
448
+
449
+ This release lands every P1 finding from the deep QA report: the intake input
450
+ pipeline now respects explicit nutrient/custom-food data over text estimates,
451
+ custom foods scale correctly by `grams_estimate`, hydration finally has a
452
+ delete/clear path, and the meal estimator correctly handles pt-BR decimal
453
+ commas and explicit zero/negative quantities.
454
+
455
+ Each fix ships with a regression test (estimator unit tests + MCP-level smoke
456
+ assertions) so the same bug cannot reappear silently.
457
+
458
+ ### Fixed
459
+
460
+ - **Intake — explicit data wins over text estimate (N-001).** When
461
+ `nourish_log_intake` was called with both `text` and explicit
462
+ `nutrients`/`custom_food`/`food_ref`/`food`, the explicit data was silently
463
+ dropped and the entry was logged with whatever the text estimator inferred
464
+ (often `nutrients: {}` for OCR labels). Explicit nutrient data now always
465
+ wins; the text becomes the food label or note. Regression covered in
466
+ `scripts/smoke-tools.mjs` (`assertExplicitNutrientsBeatText`).
467
+ - **Intake — `custom_food.nutrients_per_100g` now scales by
468
+ `grams_estimate` (N-002).** A 60 g portion of a custom food previously
469
+ logged the full 100 g nutrient values. The new
470
+ `nutrientsFromCustomShape` / `gramsForCustomShape` helpers compute the
471
+ correct gram weight (from `grams_estimate`, then `serving.grams × quantity`,
472
+ then `available_portions[0].grams × quantity`) and scale via the existing
473
+ `nutrientsForGrams`. Regression covered in
474
+ `assertCustomFoodScalesByGrams`.
475
+ - **Estimator — pt-BR decimal comma is parsed correctly (N-004).**
476
+ `1,5 banana` was being split into two clauses (`1` and `5 banana`) and
477
+ logged as five bananas. The clause splitter now uses negative lookarounds
478
+ (`(?<!\d),(?!\d)`) so commas between digits stay attached, and
479
+ `parseQuantity` normalizes commas to dots before `parseFloat`. Regression in
480
+ `scripts/test-meal-estimator.mjs`.
481
+ - **Estimator — zero/negative quantities are rejected, not silently
482
+ defaulted (N-005).** `0g banana` was logged as 1 g of banana and
483
+ `-100g rice` quietly fell back to a default 158 g serving. The quantity
484
+ pattern now matches an optional leading `-` so the negative is detected and
485
+ `parseQuantity` returns `null` for non-positive quantities, causing the food
486
+ match to be skipped (and surfaced via a new
487
+ "Rejected N food item(s) with non-positive quantity" warning). Regression in
488
+ `scripts/test-meal-estimator.mjs`.
489
+
490
+ ### Added
491
+
492
+ - **Hydration — `nourish_delete_water` and `nourish_clear_hydration_day`
493
+ tools (N-003).** Hydration entries previously had no public delete API;
494
+ `nourish_clear_day` only touched intake, leaving water entries orphaned.
495
+ Both new tools require `explicit_user_intent: true`. The existing
496
+ `nourish_clear_day` also accepts a new `include_hydration: true` flag that
497
+ clears intake and hydration in one call, returning a per-store
498
+ `deleted_entries` summary. Regression in
499
+ `assertHydrationDeleteAndClear` (smoke).
500
+ - **`CHANGELOG.md`** (this file). Previous releases (0.1.x → 0.2.2) summarized
501
+ retroactively below.
502
+
503
+ ### Notes
504
+
505
+ - Tool count increased from 32 → **34** (added `nourish_delete_water` and
506
+ `nourish_clear_hydration_day`). `nourish_clear_day` accepts the new
507
+ optional `include_hydration` flag without a breaking change.
508
+
509
+ ## Earlier history (retroactive summary)
510
+
511
+ - **0.2.2** — Nourish discovery polish (`nourish_agent_manifest`,
512
+ `nourish://usage-guide` resource, structured validation errors).
513
+ - **0.2.1** — Coach mode-specific focus defaults; Beever-Atlas style README.
514
+ - **0.2.0** — Personal nutrition memory + nourish coach (`daily_coach`,
515
+ `suggest_next_meal`, `after_log_review`, `pre_workout_nutrition`,
516
+ `evening_checkin`).
517
+ - **0.1.x** — Initial nutrition MCP surface: USDA / Open Food Facts /
518
+ Brazilian local provider, intake/hydration stores, barcode + image tools,
519
+ Hermes setup helper.
package/README.md CHANGED
@@ -25,7 +25,7 @@
25
25
  </p>
26
26
 
27
27
  > ⚡ **One-command install** with [Delx Wellness for Hermes](https://github.com/davidmosiah/delx-wellness-hermes):
28
- > `npx -y delx-wellness-hermes setup` &mdash; preconfigures this connector and the other 8 in a dedicated Hermes profile.
28
+ > `npx -y delx-wellness-hermes setup` &mdash; preconfigures this connector and the rest of the Delx Wellness stack in a dedicated Hermes profile.
29
29
  >
30
30
  > Or wire it standalone into Claude Desktop / Cursor / ChatGPT Desktop &mdash; see the install section below.
31
31
 
@@ -39,6 +39,25 @@ Wellness Nourish is a local MCP server for nutrition search, barcode lookup, bar
39
39
 
40
40
  > If this nutrition layer helps your agent workflow, please star the repo. Stars make the project easier for other AI builders to discover and help Delx keep shipping local-first wellness infrastructure.
41
41
 
42
+ <p align="center">
43
+ <img src="assets/telegram-nourish-demo.svg" alt="Wellness Nourish Telegram and Hermes nutrition workflow demo" width="92%" />
44
+ </p>
45
+
46
+ ## Try It In 60 Seconds
47
+
48
+ ```bash
49
+ npx -y wellness-nourish doctor
50
+ npx -y wellness-nourish search banana
51
+ npx -y wellness-nourish log --preview "2 ovos, banana e café preto"
52
+ ```
53
+
54
+ For the full Telegram/Hermes flow:
55
+
56
+ ```bash
57
+ npx -y delx-wellness-hermes setup
58
+ hermes -p delx-wellness
59
+ ```
60
+
42
61
  The connector uses USDA FoodData Central as the primary food search provider. Open Food Facts is used for packaged-food barcode lookup and product-name search when enabled. Local barcode image decoding is supported with ZXing. Meal photos are estimated only from an agent-provided visual observation and always require confirmation before logging. The local estimator includes a pt-BR/Brazilian-food catalog for common meals, kitchen units, and shortcuts such as arroz, feijão, frango, ovos, banana, tapioca, picanha, feijoada and salada. It does not provide hosted sync, autonomous photo upload, recipe generation, or medical advice.
43
62
 
44
63
  ## Install
@@ -221,8 +240,10 @@ The full [Delx Wellness](https://wellness.delx.ai) connector library:
221
240
  | Garmin | [`garmin-mcp-unofficial`](https://www.npmjs.com/package/garmin-mcp-unofficial) | [garminmcp](https://github.com/davidmosiah/garminmcp) |
222
241
  | Strava | [`strava-mcp-unofficial`](https://www.npmjs.com/package/strava-mcp-unofficial) | [strava-mcp](https://github.com/davidmosiah/strava-mcp) |
223
242
  | Fitbit | [`fitbit-mcp-unofficial`](https://www.npmjs.com/package/fitbit-mcp-unofficial) | [fitbitmcp](https://github.com/davidmosiah/fitbitmcp) |
243
+ | Google Health | [`google-health-mcp-unofficial`](https://www.npmjs.com/package/google-health-mcp-unofficial) | [google-health-mcp](https://github.com/davidmosiah/google-health-mcp) |
224
244
  | Withings | [`withings-mcp-unofficial`](https://www.npmjs.com/package/withings-mcp-unofficial) | [withingsmcp](https://github.com/davidmosiah/withingsmcp) |
225
245
  | Apple Health | [`apple-health-mcp-unofficial`](https://www.npmjs.com/package/apple-health-mcp-unofficial) | [apple-health-mcp](https://github.com/davidmosiah/apple-health-mcp) |
246
+ | Samsung Health | [`samsung-health-mcp-unofficial`](https://www.npmjs.com/package/samsung-health-mcp-unofficial) | [samsung-health-mcp](https://github.com/davidmosiah/samsung-health-mcp) |
226
247
  | Polar | [`polar-mcp-unofficial`](https://www.npmjs.com/package/polar-mcp-unofficial) | [polarmcp](https://github.com/davidmosiah/polarmcp) |
227
248
  | Nourish (nutrition) | [`wellness-nourish`](https://www.npmjs.com/package/wellness-nourish) | [wellness-nourish](https://github.com/davidmosiah/wellness-nourish) |
228
249
 
@@ -0,0 +1,57 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="1200" height="630" viewBox="0 0 1200 630" role="img" aria-labelledby="title desc">
2
+ <title id="title">Wellness Nourish Telegram demo</title>
3
+ <desc id="desc">A Telegram conversation where Hermes uses Wellness Nourish to estimate a Brazilian meal, check protein remaining, and ask for confirmation before logging.</desc>
4
+ <defs>
5
+ <linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
6
+ <stop offset="0" stop-color="#052e2b"/>
7
+ <stop offset="0.52" stop-color="#0f172a"/>
8
+ <stop offset="1" stop-color="#1e1b4b"/>
9
+ </linearGradient>
10
+ <filter id="shadow" x="-20%" y="-20%" width="140%" height="140%">
11
+ <feDropShadow dx="0" dy="16" stdDeviation="18" flood-color="#020617" flood-opacity="0.45"/>
12
+ </filter>
13
+ </defs>
14
+ <rect width="1200" height="630" fill="url(#bg)"/>
15
+ <circle cx="1040" cy="110" r="170" fill="#10b981" opacity="0.14"/>
16
+ <circle cx="150" cy="520" r="190" fill="#fbbf24" opacity="0.10"/>
17
+
18
+ <text x="72" y="86" fill="#f8fafc" font-family="Inter, Segoe UI, Arial, sans-serif" font-size="44" font-weight="850">Wellness Nourish</text>
19
+ <text x="72" y="126" fill="#a7f3d0" font-family="Inter, Segoe UI, Arial, sans-serif" font-size="22" font-weight="650">Nutrition MCP for agents: food, barcode, hydration, goals and confirmation-safe logging</text>
20
+
21
+ <g filter="url(#shadow)">
22
+ <rect x="86" y="170" width="500" height="368" rx="28" fill="#0b1220" stroke="#164e63"/>
23
+ <rect x="86" y="170" width="500" height="62" rx="28" fill="#0f766e"/>
24
+ <text x="122" y="209" fill="#ecfeff" font-family="Inter, Segoe UI, Arial, sans-serif" font-size="22" font-weight="800">Telegram with Hermes</text>
25
+
26
+ <rect x="124" y="266" width="318" height="56" rx="18" fill="#0f766e"/>
27
+ <text x="148" y="300" fill="#ecfeff" font-family="Inter, Segoe UI, Arial, sans-serif" font-size="17">Log my lunch: rice, beans, chicken</text>
28
+
29
+ <rect x="214" y="348" width="318" height="116" rx="18" fill="#111827" stroke="#334155"/>
30
+ <text x="238" y="380" fill="#f8fafc" font-family="Inter, Segoe UI, Arial, sans-serif" font-size="17" font-weight="700">Estimated before logging</text>
31
+ <text x="238" y="410" fill="#d1fae5" font-family="Inter, Segoe UI, Arial, sans-serif" font-size="17">~560 kcal · 42g protein</text>
32
+ <text x="238" y="438" fill="#d1fae5" font-family="Inter, Segoe UI, Arial, sans-serif" font-size="17">confidence 0.82 · pt-BR parsed</text>
33
+
34
+ <rect x="124" y="486" width="176" height="42" rx="16" fill="#0f766e"/>
35
+ <text x="148" y="513" fill="#ecfeff" font-family="Inter, Segoe UI, Arial, sans-serif" font-size="17">yes, save it</text>
36
+ </g>
37
+
38
+ <g filter="url(#shadow)">
39
+ <rect x="660" y="170" width="454" height="368" rx="28" fill="#111827" stroke="#1f2937"/>
40
+ <text x="700" y="220" fill="#f8fafc" font-family="Inter, Segoe UI, Arial, sans-serif" font-size="26" font-weight="850">Agent tool sequence</text>
41
+ <text x="700" y="252" fill="#99f6e4" font-family="Inter, Segoe UI, Arial, sans-serif" font-size="17">always estimates first, writes only after intent</text>
42
+
43
+ <g font-family="Inter, Segoe UI, Arial, sans-serif" font-size="18" font-weight="750">
44
+ <rect x="700" y="292" width="326" height="46" rx="14" fill="#134e4a"/>
45
+ <text x="724" y="322" fill="#f0fdfa">nourish_estimate_meal</text>
46
+ <rect x="700" y="356" width="326" height="46" rx="14" fill="#134e4a"/>
47
+ <text x="724" y="386" fill="#f0fdfa">nourish_daily_summary</text>
48
+ <rect x="700" y="420" width="326" height="46" rx="14" fill="#134e4a"/>
49
+ <text x="724" y="450" fill="#f0fdfa">ask for confirmation</text>
50
+ <rect x="700" y="484" width="326" height="46" rx="14" fill="#065f46"/>
51
+ <text x="724" y="514" fill="#f0fdfa">nourish_log_intake</text>
52
+ </g>
53
+ </g>
54
+
55
+ <rect x="72" y="570" width="1056" height="38" rx="16" fill="#020617" opacity="0.70"/>
56
+ <text x="102" y="595" fill="#e2e8f0" font-family="Inter, Segoe UI, Arial, sans-serif" font-size="18" font-weight="700">Designed for daily personal use through agents, with local persistence and explicit user intent before writes.</text>
57
+ </svg>
@@ -467,8 +467,12 @@ function parsePositiveNumber(value, label) {
467
467
  }
468
468
  return parsed;
469
469
  }
470
+ // Replaced UTC noon (`${date}T12:00:00.000Z`) with the timezone-aware helper
471
+ // from services/local-date.ts. The legacy form put logs into the wrong day
472
+ // for any user not on UTC.
473
+ import { dateToNoonTimestamp as dateToNoonTimestampLocal } from "../services/local-date.js";
470
474
  function dateToNoonTimestamp(date) {
471
- return date === undefined ? undefined : `${date}T12:00:00.000Z`;
475
+ return dateToNoonTimestampLocal(date);
472
476
  }
473
477
  function sumGrams(items) {
474
478
  if (items.length === 0) {