openvibers/openvibe.sdk Commits
0.68.0: openvibe-sdk/food boxes (list, get with codes and score, cost), for OpenVibe.Food's food boxes (#95)
Changes
@@@ -3,6 +3,15 @@All notable changes to `openvibe-sdk`. The package follows semver; while it is `0.x`, a minorrelease may change an API and says so here.++## 0.68.0 (2026-10-28)++++- **`openvibe-sdk/food`: food boxes** (OpenVibe.Food's `/boxes`, 2026-10-28). Every meal kit, ready meal and food box++ side by side: `food.boxes.list({ kind, diet, q })`, `.get(slug)` (with the box's codes on OpenVibe.Coupons and its++ score on OpenVibe.Reviews, null when there are none) and `.cost(slug, { people, meals, weeks })` (what its stated price++ comes to).++ - Each box's facts were read on the service's own pages, with the page and the day (`sources`, `checked`).++ - Types: `FoodBox`, `FoodBoxCost`, `BoxKind`.++## 0.67.0 (2026-10-27)- **`openvibe-sdk/bot`: a machine's own models** (AI on your own machines; OpenVibe.Node v0.27.0's ai plugin).@@@ -2,14 +2,14 @@> Supported browser and server clients for the OpenVibe platform.--**Status:** alpha, v0.67.0 (roadmap Wave 2; developer apps from Wave 20; Media objects v2 from Wave 4; the Tools platform API; Search, Chat and AI clients from WS-F task 4; per-actor limits from WS-R task 4). Every release is tagged (see [CHANGELOG.md](CHANGELOG.md)). Tested against local stub servers and the built-in mock platform only, never against the live platform. Services pin release tags; each repository's `STATUS.json` names the one it uses.++**Status:** alpha, v0.68.0 (roadmap Wave 2; developer apps from Wave 20; Media objects v2 from Wave 4; the Tools platform API; Search, Chat and AI clients from WS-F task 4; per-actor limits from WS-R task 4). Every release is tagged (see [CHANGELOG.md](CHANGELOG.md)). Tested against local stub servers and the built-in mock platform only, never against the live platform. Services pin release tags; each repository's `STATUS.json` names the one it uses.**Plan:** OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026), §3.2; roadmap §30.**License:** MIT ([LICENSE](LICENSE)). This package is a library that apps outside the network embed, so it uses MIT. The OpenVibe services themselves stay AGPL-3.0.**If a capability is not in the SDK, it is not public.** Apps call services through `openvibe-sdk` and never build internal routes themselves. A route with no SDK wrapper is internal, even when you can reach it, and it can change without notice. To make a capability public, first define it in OpenVibe.Contracts, then wrap it here.```bash--npm install https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.67.0++npm install https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.68.0```It has no runtime dependencies. It needs Node ≥ 20, or any browser with `fetch`, Web Crypto and `TextDecoder`. There is no build step. The package is CommonJS with ESM entry points (`import` works). Each subpath has its own `.d.ts`. For a page with no bundler, `browser/openvibe-sdk.mjs` is one self-contained ES module of the browser-safe subpaths (see [Browser without a bundler](#browser-without-a-bundler)). It does not depend on `openvibe-contracts` at runtime: it copies the contract types it uses (from Contracts v0.86.0, a devDependency the tests check them against).@@@ -3,7 +3,7 @@"stage": "alpha","code": true,"wave": "W2",-- "version": "0.67.0",++ "version": "0.68.0","plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026), §3.2; roadmap §30","deployed": false,"deployment": "none: a package released as git tags; services and apps pin a tag",@@@ -1,4 +1,4 @@--// Generated by scripts/browser-bundle.js from browser.js (openvibe-sdk 0.67.0, MIT). Do not edit.++// Generated by scripts/browser-bundle.js from browser.js (openvibe-sdk 0.68.0, MIT). Do not edit.// One self-contained ES module: core at the top level plus the browser-safe subpaths as namespaces// (auth, registry, modules, realtime, media, community, bot, search, themes, chat, ai, jobs, tools, projects, geo). No server-only code: no service tokens, JWT verification, events, identity./* eslint-disable */@@@ -43,7 +43,7 @@ const { paginate, offsetPager } = __load(7);const semver = __load(6);/** The package version (test/version.test.js keeps it equal to package.json). */--const SDK_VERSION = '0.67.0';++const SDK_VERSION = '0.68.0';module.exports = {createClient,@@@ -1,12 +1,12 @@{"name": "openvibe-sdk",-- "version": "0.67.0",++ "version": "0.68.0","lockfileVersion": 3,"requires": true,"packages": {"": {"name": "openvibe-sdk",-- "version": "0.67.0",++ "version": "0.68.0","license": "MIT","bin": {"openvibe-download": "bin/openvibe-download.js"@@@ -1,6 +1,6 @@{"name": "openvibe-sdk",-- "version": "0.67.0",++ "version": "0.68.0","description": "Supported browser and server clients for the OpenVibe platform: auth (incl. developer apps), registry discovery, identity, user modules, events, realtime, media, community, Bot robots, search, chat, AI runs, Tools (registry, run API and jobs), VIP memberships and developer projects, the async PostgreSQL data layer (db) and the Valkey cache, limits store, queue and pub/sub, with deadlines, safe retries, problem errors and trace propagation.","main": "index.js","types": "types/index.d.ts",@@@ -8,7 +8,7 @@ const { paginate, offsetPager } = require('./paginate');const semver = require('./semver');/** The package version (test/version.test.js keeps it equal to package.json). */--const SDK_VERSION = '0.67.0';++const SDK_VERSION = '0.68.0';module.exports = {createClient,@@@ -1,12 +1,16 @@'use strict';/*** openvibe-sdk/food — OpenVibe.Food for apps: places to get food near you (food banks, pantries, free meals; from-- * OpenStreetMap), the food list with store prices, meal plans, a person's pantry with "what can I make", and recipe-- * ideas from the network's AI. Routes at https://openvibe.food/api/v1; tokens for audience openvibe.food.++ * OpenStreetMap), the food list with store prices, food boxes (every meal kit, ready meal and food box side by side,++ * with their codes and scores), meal plans, a person's pantry with "what can I make", and recipe ideas from the++ * network's AI. Routes at https://openvibe.food/api/v1; tokens for audience openvibe.food.** const food = createFoodClient(createClient({ tokenProvider }));* const near = await food.places({ kind: 'foodbank', lat: 45.52, lon: -122.68, radius: 10 }); // public* const { foods } = await food.foods.list({ group: 'vegetables', q: 'carrot' }); // public++ * const { boxes } = await food.boxes.list({ kind: 'cook', diet: 'vegetarian' }); // public++ * const { box, coupons, reviews } = await food.boxes.get('everyplate'); // public++ * const { cost } = await food.boxes.cost('everyplate', { people: 4, meals: 3 }); // public* const plan = await food.plans.create({ people: 2, days: 7, budget: 80, title: 'This week' }); // food.plan.write* await food.pantry.set(['rice', 'black-beans', 'onions']); // food.pantry.write* const { suggestions } = await food.pantry.get(); // { can_make, almost }@@@ -16,8 +20,10 @@* is deterministic (all six MyPlate groups, a shopping list, the cheapest store per line, a budget as a hard cap); the* pantry's suggestions are matches against basic recipes, not dietary advice. Creating a plan or asking for recipe* ideas makes a new one each time, so neither is retried on its own; setting the pantry is a PUT and is.++ * A box's facts were read by a person on the service's own pages, each with its page and day (sources, checked); a++ * fact nobody checked is null, and a box's cost is worked out only from a stated price per serving or meal.* Errors are OpenVibeError: food.plan.invalid, food.pantry.invalid, food.pantry.unknown_item, food.pantry_empty,-- * food.recipes_daily_limit (429), food.ai_off (503), food.plan.not_found, …++ * food.recipes_daily_limit (429), food.ai_off (503), food.plan.not_found, food.box.not_found, food.box.no_cost, …*/const enc = encodeURIComponent;const PLAN_ID_RE = /^pln_[0-9A-HJKMNP-TV-Z]{26}$/; // Food's own rule@@@ -39,6 +45,14 @@ function createFoodClient(client, { baseUrl } = {}) {list: ({ group, q, signal } = {}) => call({ path: '/api/v1/foods', query: { group, q }, signal }),get: (slug, { signal } = {}) => call({ path: `/api/v1/foods/${enc(String(slug))}`, signal }),},++ boxes: {++ /** Every food box (public): kind cook|ready|produce|meat|snacks|coffee|baby|pets, a diet, a search. */++ list: ({ kind, diet, q, signal } = {}) => call({ path: '/api/v1/boxes', query: { kind, diet, q }, signal }),++ /** One box with its codes on OpenVibe.Coupons and its score on OpenVibe.Reviews (null when there are none). */++ get: (slug, { signal } = {}) => call({ path: `/api/v1/boxes/${enc(String(slug))}`, signal }),++ /** What its stated price comes to for people × meals a week over weeks (food.box.no_cost when it states none). */++ cost: (slug, { people, meals, weeks, signal } = {}) => call({ path: `/api/v1/boxes/${enc(String(slug))}/cost`, query: { people, meals, weeks }, signal }),++ },plans: {/** A meal plan for 1–14 days (food.plan-create-request@1) → the plan, saved as the caller's. */create: ({ people, days, budget, title } = {}, { signal } = {}) => call({ method: 'POST', path: '/api/v1/plans', json: { people, days, budget, title }, idempotencyKey: false, signal }),@@@ -68,6 +68,9 @@ const ids = { plan: `pln_${ULID}`, task: `tsk_${ULID}`, agent: `agt_${ULID}`, meawait food.places({ kind: 'foodbank', lat: 45.5, lon: -122.6, radius: 10 });await food.foods.list({ group: 'vegetables', q: 'carrot' });await food.foods.get('carrots');++ await food.boxes.list({ kind: 'cook', diet: 'keto' });++ await food.boxes.get('everyplate');++ await food.boxes.cost('everyplate', { people: 4, meals: 3 });await food.plans.list({ limit: 5 });await food.plans.get(ids.plan);await food.pantry.get();@@@ -77,6 +80,7 @@ const ids = { plan: `pln_${ULID}`, task: `tsk_${ULID}`, agent: `agt_${ULID}`, meawait food.recipes.list();assert.deepStrictEqual(mark().map((r) => `${r.method} ${r.url}`), ['GET /api/v1/places?kind=foodbank&lat=45.5&lon=-122.6&radius=10', 'GET /api/v1/foods?group=vegetables&q=carrot', 'GET /api/v1/foods/carrots',++ 'GET /api/v1/boxes?kind=cook&diet=keto', 'GET /api/v1/boxes/everyplate', 'GET /api/v1/boxes/everyplate/cost?people=4&meals=3','GET /api/v1/plans?limit=5', `GET /api/v1/plans/${ids.plan}`, 'GET /api/v1/pantry', 'PUT /api/v1/pantry', 'PUT /api/v1/pantry','POST /api/v1/plans', 'POST /api/v1/recipes/suggest', 'GET /api/v1/recipes',]);@@@ -6,12 +6,29 @@ export interface PantryResult {suggestions: { can_make: Array<{ slug: string; name: string; serves: number; minutes: number }>; almost: Array<{ slug: string; name: string; serves: number; minutes: number; missing: string[] }> };note: string;}++export type BoxKind = 'cook' | 'ready' | 'produce' | 'meat' | 'snacks' | 'coffee' | 'baby' | 'pets';++/** A food box: every fact as its own pages stated it (sources, checked), null when nobody checked. */++export interface FoodBox {++ slug: string; name: string; kind: BoxKind; kind_name: string; url: string; host: string; what: string;++ cadence: string[]; people: number[]; meals_per_week: number[]; diets: string[];++ price: { from: number | null; to: number | null; unit: 'serving' | 'meal' | 'box' | 'month' | 'delivery' | 'day' | 'membership'; text: string; line: string | null } | null;++ shipping: { amount: number | null; to?: number | null; text: string } | null;++ ships_to: string | null; cancel: string | null; notes: string[]; sources: string[]; checked: string | null;++ links: { page: string; coupons: string; reviews: string };++}++type Range = { from: number; to: number };++export interface FoodBoxCost { servings: number; weeks: number; food: Range; shipping: Range | null; total: Range | null; basis: string }export interface FoodClient {places(opts?: Opts & { kind?: string; q?: string; lat?: number; lon?: number; radius?: number; limit?: number }): Promise<FoodJson>;foods: {list(opts?: Opts & { group?: string; q?: string }): Promise<{ foods: FoodJson[]; count: number; groups: FoodJson[]; source: FoodJson }>;get(slug: string, opts?: Opts): Promise<{ food: FoodJson; source: FoodJson }>;};++ boxes: {++ list(opts?: Opts & { kind?: BoxKind; diet?: string; q?: string }): Promise<{ boxes: FoodBox[]; count: number; kinds: FoodJson[]; diets: Record<string, string>; method: string }>;++ get(slug: string, opts?: Opts): Promise<{ box: FoodBox; coupons: { url: string; active: number; best: { code: string; title: string } | null } | null; reviews: { url: string; name: string; percent: number | null; total: number | null; line: string | null } | null }>;++ cost(slug: string, opts?: Opts & { people?: number; meals?: number; weeks?: number }): Promise<{ box: string; cost: FoodBoxCost }>;++ };plans: {create(req: { people: number; days: number; budget?: number | null; title?: string | null }, opts?: Opts): Promise<FoodJson>;list(opts?: Opts & { limit?: number }): Promise<{ plans: FoodJson[] }>;
@@ -3,6 +3,15 @@
@@ -3,6 +3,15 @@
All notable changes to `openvibe-sdk`. The package follows semver; while it is `0.x`, a minor
All notable changes to `openvibe-sdk`. The package follows semver; while it is `0.x`, a minor
release may change an API and says so here.
release may change an API and says so here.
+## 0.68.0 (2026-10-28)
+
+- **`openvibe-sdk/food`: food boxes** (OpenVibe.Food's `/boxes`, 2026-10-28). Every meal kit, ready meal and food box
+ side by side: `food.boxes.list({ kind, diet, q })`, `.get(slug)` (with the box's codes on OpenVibe.Coupons and its
+ score on OpenVibe.Reviews, null when there are none) and `.cost(slug, { people, meals, weeks })` (what its stated price
+ comes to).
+ - Each box's facts were read on the service's own pages, with the page and the day (`sources`, `checked`).
+ - Types: `FoodBox`, `FoodBoxCost`, `BoxKind`.
+
## 0.67.0 (2026-10-27)
## 0.67.0 (2026-10-27)
- **`openvibe-sdk/bot`: a machine's own models** (AI on your own machines; OpenVibe.Node v0.27.0's ai plugin).
- **`openvibe-sdk/bot`: a machine's own models** (AI on your own machines; OpenVibe.Node v0.27.0's ai plugin).
@@ -2,14 +2,14 @@
@@ -2,14 +2,14 @@
> Supported browser and server clients for the OpenVibe platform.
> Supported browser and server clients for the OpenVibe platform.
-**Status:** alpha, v0.67.0 (roadmap Wave 2; developer apps from Wave 20; Media objects v2 from Wave 4; the Tools platform API; Search, Chat and AI clients from WS-F task 4; per-actor limits from WS-R task 4). Every release is tagged (see [CHANGELOG.md](CHANGELOG.md)). Tested against local stub servers and the built-in mock platform only, never against the live platform. Services pin release tags; each repository's `STATUS.json` names the one it uses.
+**Status:** alpha, v0.68.0 (roadmap Wave 2; developer apps from Wave 20; Media objects v2 from Wave 4; the Tools platform API; Search, Chat and AI clients from WS-F task 4; per-actor limits from WS-R task 4). Every release is tagged (see [CHANGELOG.md](CHANGELOG.md)). Tested against local stub servers and the built-in mock platform only, never against the live platform. Services pin release tags; each repository's `STATUS.json` names the one it uses.
**Plan:** OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026), §3.2; roadmap §30.
**Plan:** OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026), §3.2; roadmap §30.
**License:** MIT ([LICENSE](LICENSE)). This package is a library that apps outside the network embed, so it uses MIT. The OpenVibe services themselves stay AGPL-3.0.
**License:** MIT ([LICENSE](LICENSE)). This package is a library that apps outside the network embed, so it uses MIT. The OpenVibe services themselves stay AGPL-3.0.
**If a capability is not in the SDK, it is not public.** Apps call services through `openvibe-sdk` and never build internal routes themselves. A route with no SDK wrapper is internal, even when you can reach it, and it can change without notice. To make a capability public, first define it in OpenVibe.Contracts, then wrap it here.
**If a capability is not in the SDK, it is not public.** Apps call services through `openvibe-sdk` and never build internal routes themselves. A route with no SDK wrapper is internal, even when you can reach it, and it can change without notice. To make a capability public, first define it in OpenVibe.Contracts, then wrap it here.
```bash
```bash
-npm install https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.67.0
+npm install https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.68.0
```
```
It has no runtime dependencies. It needs Node ≥ 20, or any browser with `fetch`, Web Crypto and `TextDecoder`. There is no build step. The package is CommonJS with ESM entry points (`import` works). Each subpath has its own `.d.ts`. For a page with no bundler, `browser/openvibe-sdk.mjs` is one self-contained ES module of the browser-safe subpaths (see [Browser without a bundler](#browser-without-a-bundler)). It does not depend on `openvibe-contracts` at runtime: it copies the contract types it uses (from Contracts v0.86.0, a devDependency the tests check them against).
It has no runtime dependencies. It needs Node ≥ 20, or any browser with `fetch`, Web Crypto and `TextDecoder`. There is no build step. The package is CommonJS with ESM entry points (`import` works). Each subpath has its own `.d.ts`. For a page with no bundler, `browser/openvibe-sdk.mjs` is one self-contained ES module of the browser-safe subpaths (see [Browser without a bundler](#browser-without-a-bundler)). It does not depend on `openvibe-contracts` at runtime: it copies the contract types it uses (from Contracts v0.86.0, a devDependency the tests check them against).
@@ -3,7 +3,7 @@
@@ -3,7 +3,7 @@
"stage": "alpha",
"stage": "alpha",
"code": true,
"code": true,
"wave": "W2",
"wave": "W2",
- "version": "0.67.0",
+ "version": "0.68.0",
"plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026), §3.2; roadmap §30",
"plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026), §3.2; roadmap §30",
"deployed": false,
"deployed": false,
"deployment": "none: a package released as git tags; services and apps pin a tag",
"deployment": "none: a package released as git tags; services and apps pin a tag",
@@ -1,4 +1,4 @@
@@ -1,4 +1,4 @@
-// Generated by scripts/browser-bundle.js from browser.js (openvibe-sdk 0.67.0, MIT). Do not edit.
+// Generated by scripts/browser-bundle.js from browser.js (openvibe-sdk 0.68.0, MIT). Do not edit.
// One self-contained ES module: core at the top level plus the browser-safe subpaths as namespaces
// One self-contained ES module: core at the top level plus the browser-safe subpaths as namespaces
// (auth, registry, modules, realtime, media, community, bot, search, themes, chat, ai, jobs, tools, projects, geo). No server-only code: no service tokens, JWT verification, events, identity.
// (auth, registry, modules, realtime, media, community, bot, search, themes, chat, ai, jobs, tools, projects, geo). No server-only code: no service tokens, JWT verification, events, identity.
/* eslint-disable */
/* eslint-disable */
@@ -43,7 +43,7 @@ const { paginate, offsetPager } = __load(7);
@@ -43,7 +43,7 @@ const { paginate, offsetPager } = __load(7);
const semver = __load(6);
const semver = __load(6);
/** The package version (test/version.test.js keeps it equal to package.json). */
/** The package version (test/version.test.js keeps it equal to package.json). */
-const SDK_VERSION = '0.67.0';
+const SDK_VERSION = '0.68.0';
module.exports = {
module.exports = {
createClient,
createClient,
@@ -1,12 +1,12 @@
@@ -1,12 +1,12 @@
{
{
"name": "openvibe-sdk",
"name": "openvibe-sdk",
- "version": "0.67.0",
+ "version": "0.68.0",
"lockfileVersion": 3,
"lockfileVersion": 3,
"requires": true,
"requires": true,
"packages": {
"packages": {
"": {
"": {
"name": "openvibe-sdk",
"name": "openvibe-sdk",
- "version": "0.67.0",
+ "version": "0.68.0",
"license": "MIT",
"license": "MIT",
"bin": {
"bin": {
"openvibe-download": "bin/openvibe-download.js"
"openvibe-download": "bin/openvibe-download.js"
@@ -1,6 +1,6 @@
@@ -1,6 +1,6 @@
{
{
"name": "openvibe-sdk",
"name": "openvibe-sdk",
- "version": "0.67.0",
+ "version": "0.68.0",
"description": "Supported browser and server clients for the OpenVibe platform: auth (incl. developer apps), registry discovery, identity, user modules, events, realtime, media, community, Bot robots, search, chat, AI runs, Tools (registry, run API and jobs), VIP memberships and developer projects, the async PostgreSQL data layer (db) and the Valkey cache, limits store, queue and pub/sub, with deadlines, safe retries, problem errors and trace propagation.",
"description": "Supported browser and server clients for the OpenVibe platform: auth (incl. developer apps), registry discovery, identity, user modules, events, realtime, media, community, Bot robots, search, chat, AI runs, Tools (registry, run API and jobs), VIP memberships and developer projects, the async PostgreSQL data layer (db) and the Valkey cache, limits store, queue and pub/sub, with deadlines, safe retries, problem errors and trace propagation.",
"main": "index.js",
"main": "index.js",
"types": "types/index.d.ts",
"types": "types/index.d.ts",
@@ -8,7 +8,7 @@ const { paginate, offsetPager } = require('./paginate');
@@ -8,7 +8,7 @@ const { paginate, offsetPager } = require('./paginate');
const semver = require('./semver');
const semver = require('./semver');
/** The package version (test/version.test.js keeps it equal to package.json). */
/** The package version (test/version.test.js keeps it equal to package.json). */
-const SDK_VERSION = '0.67.0';
+const SDK_VERSION = '0.68.0';
module.exports = {
module.exports = {
createClient,
createClient,
@@ -1,12 +1,16 @@
@@ -1,12 +1,16 @@
'use strict';
'use strict';
/**
/**
* openvibe-sdk/food — OpenVibe.Food for apps: places to get food near you (food banks, pantries, free meals; from
* openvibe-sdk/food — OpenVibe.Food for apps: places to get food near you (food banks, pantries, free meals; from
- * OpenStreetMap), the food list with store prices, meal plans, a person's pantry with "what can I make", and recipe
- * ideas from the network's AI. Routes at https://openvibe.food/api/v1; tokens for audience openvibe.food.
+ * OpenStreetMap), the food list with store prices, food boxes (every meal kit, ready meal and food box side by side,
+ * with their codes and scores), meal plans, a person's pantry with "what can I make", and recipe ideas from the
+ * network's AI. Routes at https://openvibe.food/api/v1; tokens for audience openvibe.food.
*
*
* const food = createFoodClient(createClient({ tokenProvider }));
* const food = createFoodClient(createClient({ tokenProvider }));
* const near = await food.places({ kind: 'foodbank', lat: 45.52, lon: -122.68, radius: 10 }); // public
* const near = await food.places({ kind: 'foodbank', lat: 45.52, lon: -122.68, radius: 10 }); // public
* const { foods } = await food.foods.list({ group: 'vegetables', q: 'carrot' }); // public
* const { foods } = await food.foods.list({ group: 'vegetables', q: 'carrot' }); // public
+ * const { boxes } = await food.boxes.list({ kind: 'cook', diet: 'vegetarian' }); // public
+ * const { box, coupons, reviews } = await food.boxes.get('everyplate'); // public
+ * const { cost } = await food.boxes.cost('everyplate', { people: 4, meals: 3 }); // public
* const plan = await food.plans.create({ people: 2, days: 7, budget: 80, title: 'This week' }); // food.plan.write
* const plan = await food.plans.create({ people: 2, days: 7, budget: 80, title: 'This week' }); // food.plan.write
* await food.pantry.set(['rice', 'black-beans', 'onions']); // food.pantry.write
* await food.pantry.set(['rice', 'black-beans', 'onions']); // food.pantry.write
* const { suggestions } = await food.pantry.get(); // { can_make, almost }
* const { suggestions } = await food.pantry.get(); // { can_make, almost }
@@ -16,8 +20,10 @@
@@ -16,8 +20,10 @@
* is deterministic (all six MyPlate groups, a shopping list, the cheapest store per line, a budget as a hard cap); the
* is deterministic (all six MyPlate groups, a shopping list, the cheapest store per line, a budget as a hard cap); the
* pantry's suggestions are matches against basic recipes, not dietary advice. Creating a plan or asking for recipe
* pantry's suggestions are matches against basic recipes, not dietary advice. Creating a plan or asking for recipe
* ideas makes a new one each time, so neither is retried on its own; setting the pantry is a PUT and is.
* ideas makes a new one each time, so neither is retried on its own; setting the pantry is a PUT and is.
+ * A box's facts were read by a person on the service's own pages, each with its page and day (sources, checked); a
+ * fact nobody checked is null, and a box's cost is worked out only from a stated price per serving or meal.
* Errors are OpenVibeError: food.plan.invalid, food.pantry.invalid, food.pantry.unknown_item, food.pantry_empty,
* Errors are OpenVibeError: food.plan.invalid, food.pantry.invalid, food.pantry.unknown_item, food.pantry_empty,
- * food.recipes_daily_limit (429), food.ai_off (503), food.plan.not_found, …
+ * food.recipes_daily_limit (429), food.ai_off (503), food.plan.not_found, food.box.not_found, food.box.no_cost, …
*/
*/
const enc = encodeURIComponent;
const enc = encodeURIComponent;
const PLAN_ID_RE = /^pln_[0-9A-HJKMNP-TV-Z]{26}$/; // Food's own rule
const PLAN_ID_RE = /^pln_[0-9A-HJKMNP-TV-Z]{26}$/; // Food's own rule
@@ -39,6 +45,14 @@ function createFoodClient(client, { baseUrl } = {}) {
@@ -39,6 +45,14 @@ function createFoodClient(client, { baseUrl } = {}) {
list: ({ group, q, signal } = {}) => call({ path: '/api/v1/foods', query: { group, q }, signal }),
list: ({ group, q, signal } = {}) => call({ path: '/api/v1/foods', query: { group, q }, signal }),
get: (slug, { signal } = {}) => call({ path: `/api/v1/foods/${enc(String(slug))}`, signal }),
get: (slug, { signal } = {}) => call({ path: `/api/v1/foods/${enc(String(slug))}`, signal }),
},
},
+ boxes: {
+ /** Every food box (public): kind cook|ready|produce|meat|snacks|coffee|baby|pets, a diet, a search. */
+ list: ({ kind, diet, q, signal } = {}) => call({ path: '/api/v1/boxes', query: { kind, diet, q }, signal }),
+ /** One box with its codes on OpenVibe.Coupons and its score on OpenVibe.Reviews (null when there are none). */
+ get: (slug, { signal } = {}) => call({ path: `/api/v1/boxes/${enc(String(slug))}`, signal }),
+ /** What its stated price comes to for people × meals a week over weeks (food.box.no_cost when it states none). */
+ cost: (slug, { people, meals, weeks, signal } = {}) => call({ path: `/api/v1/boxes/${enc(String(slug))}/cost`, query: { people, meals, weeks }, signal }),
+ },
plans: {
plans: {
/** A meal plan for 1–14 days (food.plan-create-request@1) → the plan, saved as the caller's. */
/** A meal plan for 1–14 days (food.plan-create-request@1) → the plan, saved as the caller's. */
create: ({ people, days, budget, title } = {}, { signal } = {}) => call({ method: 'POST', path: '/api/v1/plans', json: { people, days, budget, title }, idempotencyKey: false, signal }),
create: ({ people, days, budget, title } = {}, { signal } = {}) => call({ method: 'POST', path: '/api/v1/plans', json: { people, days, budget, title }, idempotencyKey: false, signal }),
@@ -68,6 +68,9 @@ const ids = { plan: `pln_${ULID}`, task: `tsk_${ULID}`, agent: `agt_${ULID}`, me
@@ -68,6 +68,9 @@ const ids = { plan: `pln_${ULID}`, task: `tsk_${ULID}`, agent: `agt_${ULID}`, me
await food.places({ kind: 'foodbank', lat: 45.5, lon: -122.6, radius: 10 });
await food.places({ kind: 'foodbank', lat: 45.5, lon: -122.6, radius: 10 });
await food.foods.list({ group: 'vegetables', q: 'carrot' });
await food.foods.list({ group: 'vegetables', q: 'carrot' });
await food.foods.get('carrots');
await food.foods.get('carrots');
+ await food.boxes.list({ kind: 'cook', diet: 'keto' });
+ await food.boxes.get('everyplate');
+ await food.boxes.cost('everyplate', { people: 4, meals: 3 });
await food.plans.list({ limit: 5 });
await food.plans.list({ limit: 5 });
await food.plans.get(ids.plan);
await food.plans.get(ids.plan);
await food.pantry.get();
await food.pantry.get();
@@ -77,6 +80,7 @@ const ids = { plan: `pln_${ULID}`, task: `tsk_${ULID}`, agent: `agt_${ULID}`, me
@@ -77,6 +80,7 @@ const ids = { plan: `pln_${ULID}`, task: `tsk_${ULID}`, agent: `agt_${ULID}`, me
await food.recipes.list();
await food.recipes.list();
assert.deepStrictEqual(mark().map((r) => `${r.method} ${r.url}`), [
assert.deepStrictEqual(mark().map((r) => `${r.method} ${r.url}`), [
'GET /api/v1/places?kind=foodbank&lat=45.5&lon=-122.6&radius=10', 'GET /api/v1/foods?group=vegetables&q=carrot', 'GET /api/v1/foods/carrots',
'GET /api/v1/places?kind=foodbank&lat=45.5&lon=-122.6&radius=10', 'GET /api/v1/foods?group=vegetables&q=carrot', 'GET /api/v1/foods/carrots',
+ 'GET /api/v1/boxes?kind=cook&diet=keto', 'GET /api/v1/boxes/everyplate', 'GET /api/v1/boxes/everyplate/cost?people=4&meals=3',
'GET /api/v1/plans?limit=5', `GET /api/v1/plans/${ids.plan}`, 'GET /api/v1/pantry', 'PUT /api/v1/pantry', 'PUT /api/v1/pantry',
'GET /api/v1/plans?limit=5', `GET /api/v1/plans/${ids.plan}`, 'GET /api/v1/pantry', 'PUT /api/v1/pantry', 'PUT /api/v1/pantry',
'POST /api/v1/plans', 'POST /api/v1/recipes/suggest', 'GET /api/v1/recipes',
'POST /api/v1/plans', 'POST /api/v1/recipes/suggest', 'GET /api/v1/recipes',
]);
]);
@@ -6,12 +6,29 @@ export interface PantryResult {
@@ -6,12 +6,29 @@ export interface PantryResult {
suggestions: { can_make: Array<{ slug: string; name: string; serves: number; minutes: number }>; almost: Array<{ slug: string; name: string; serves: number; minutes: number; missing: string[] }> };
suggestions: { can_make: Array<{ slug: string; name: string; serves: number; minutes: number }>; almost: Array<{ slug: string; name: string; serves: number; minutes: number; missing: string[] }> };
note: string;
note: string;
}
}
+export type BoxKind = 'cook' | 'ready' | 'produce' | 'meat' | 'snacks' | 'coffee' | 'baby' | 'pets';
+/** A food box: every fact as its own pages stated it (sources, checked), null when nobody checked. */
+export interface FoodBox {
+ slug: string; name: string; kind: BoxKind; kind_name: string; url: string; host: string; what: string;
+ cadence: string[]; people: number[]; meals_per_week: number[]; diets: string[];
+ price: { from: number | null; to: number | null; unit: 'serving' | 'meal' | 'box' | 'month' | 'delivery' | 'day' | 'membership'; text: string; line: string | null } | null;
+ shipping: { amount: number | null; to?: number | null; text: string } | null;
+ ships_to: string | null; cancel: string | null; notes: string[]; sources: string[]; checked: string | null;
+ links: { page: string; coupons: string; reviews: string };
+}
+type Range = { from: number; to: number };
+export interface FoodBoxCost { servings: number; weeks: number; food: Range; shipping: Range | null; total: Range | null; basis: string }
export interface FoodClient {
export interface FoodClient {
places(opts?: Opts & { kind?: string; q?: string; lat?: number; lon?: number; radius?: number; limit?: number }): Promise<FoodJson>;
places(opts?: Opts & { kind?: string; q?: string; lat?: number; lon?: number; radius?: number; limit?: number }): Promise<FoodJson>;
foods: {
foods: {
list(opts?: Opts & { group?: string; q?: string }): Promise<{ foods: FoodJson[]; count: number; groups: FoodJson[]; source: FoodJson }>;
list(opts?: Opts & { group?: string; q?: string }): Promise<{ foods: FoodJson[]; count: number; groups: FoodJson[]; source: FoodJson }>;
get(slug: string, opts?: Opts): Promise<{ food: FoodJson; source: FoodJson }>;
get(slug: string, opts?: Opts): Promise<{ food: FoodJson; source: FoodJson }>;
};
};
+ boxes: {
+ list(opts?: Opts & { kind?: BoxKind; diet?: string; q?: string }): Promise<{ boxes: FoodBox[]; count: number; kinds: FoodJson[]; diets: Record<string, string>; method: string }>;
+ get(slug: string, opts?: Opts): Promise<{ box: FoodBox; coupons: { url: string; active: number; best: { code: string; title: string } | null } | null; reviews: { url: string; name: string; percent: number | null; total: number | null; line: string | null } | null }>;
+ cost(slug: string, opts?: Opts & { people?: number; meals?: number; weeks?: number }): Promise<{ box: string; cost: FoodBoxCost }>;
+ };
plans: {
plans: {
create(req: { people: number; days: number; budget?: number | null; title?: string | null }, opts?: Opts): Promise<FoodJson>;
create(req: { people: number; days: number; budget?: number | null; title?: string | null }, opts?: Opts): Promise<FoodJson>;
list(opts?: Opts & { limit?: number }): Promise<{ plans: FoodJson[] }>;
list(opts?: Opts & { limit?: number }): Promise<{ plans: FoodJson[] }>;