forge-openclaw-plugin 0.2.104 → 0.2.105
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/openclaw/tools.js +7 -4
- package/dist/server/server/src/app.js +34 -4
- package/dist/server/server/src/health-weight-loss.js +208 -6
- package/dist/server/server/src/openapi.js +4 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/skills/forge-openclaw/SKILL.md +8 -1
- package/skills/forge-openclaw/entity_conversation_playbooks.md +14 -3
package/dist/openclaw/tools.js
CHANGED
|
@@ -270,10 +270,12 @@ const healthLinkInputSchema = () => Type.Object({
|
|
|
270
270
|
relationshipType: Type.Optional(Type.String({ minLength: 1 }))
|
|
271
271
|
});
|
|
272
272
|
const nutritionMealItemInputSchema = () => Type.Object({
|
|
273
|
+
foodId: Type.Optional(optionalNullableString()),
|
|
273
274
|
name: Type.String({ minLength: 1 }),
|
|
274
275
|
brand: optionalNullableString(),
|
|
275
276
|
quantity: Type.Number({ minimum: 0 }),
|
|
276
277
|
unit: optionalNullableString(),
|
|
278
|
+
grams: Type.Optional(Type.Union([Type.Number(), Type.Null()])),
|
|
277
279
|
caloriesKcal: Type.Optional(Type.Union([Type.Number(), Type.Null()])),
|
|
278
280
|
proteinG: Type.Optional(Type.Union([Type.Number(), Type.Null()])),
|
|
279
281
|
carbsG: Type.Optional(Type.Union([Type.Number(), Type.Null()])),
|
|
@@ -304,16 +306,17 @@ const nutritionFoodLogSchema = () => Type.Object({
|
|
|
304
306
|
mealLabel: optionalNullableString(),
|
|
305
307
|
source: Type.Optional(Type.Union([
|
|
306
308
|
Type.Literal("manual"),
|
|
309
|
+
Type.Literal("search"),
|
|
307
310
|
Type.Literal("barcode"),
|
|
308
311
|
Type.Literal("chatgpt"),
|
|
309
312
|
Type.Literal("photo"),
|
|
310
|
-
Type.Literal("
|
|
313
|
+
Type.Literal("saved_meal")
|
|
311
314
|
])),
|
|
312
315
|
confirmationState: Type.Optional(Type.Union([
|
|
313
316
|
Type.Literal("candidate"),
|
|
314
317
|
Type.Literal("confirmed"),
|
|
315
|
-
Type.Literal("
|
|
316
|
-
Type.Literal("
|
|
318
|
+
Type.Literal("needs_review"),
|
|
319
|
+
Type.Literal("discarded")
|
|
317
320
|
])),
|
|
318
321
|
satietyScore: Type.Optional(Type.Union([Type.Number(), Type.Null()])),
|
|
319
322
|
hungerBefore: Type.Optional(Type.Union([Type.Number(), Type.Null()])),
|
|
@@ -878,7 +881,7 @@ export function registerForgePluginTools(api, config) {
|
|
|
878
881
|
api.registerTool({
|
|
879
882
|
name: "forge_log_food",
|
|
880
883
|
label: "Forge Log Food",
|
|
881
|
-
description: "Create a confirmed or candidate food log
|
|
884
|
+
description: "Create a confirmed or candidate food log. Search first and pass foodId when reusing a catalog food; for custom foods without foodId, caloriesKcal, proteinG, carbsG, and fatG are required.",
|
|
882
885
|
parameters: nutritionFoodLogSchema(),
|
|
883
886
|
async execute(_toolCallId, params) {
|
|
884
887
|
const typed = params;
|
|
@@ -3416,6 +3416,7 @@ const AGENT_ONBOARDING_ENTITY_CONVERSATION_PLAYBOOKS = [
|
|
|
3416
3416
|
"Ask whether the question is fat-loss pace, food intake, protein/fiber sufficiency, sport fuel, visual look, water retention, cravings, gut comfort, energy, or one meal reaction.",
|
|
3417
3417
|
"Use forge_get_weight_loss_overview before asking the user to reconstruct recent food, weight, workouts, or subjective state from memory.",
|
|
3418
3418
|
"Use forge_parse_food_log_with_chatgpt only for rough meal text or photo descriptions through Forge's configured openai-codex ChatGPT subscription connection, not a metered OpenAI Platform API path.",
|
|
3419
|
+
"Before forge_log_food, search foods or barcode lookup first; reuse a matching result with item.foodId, and create custom/no-foodId items only with researched calories, protein, carbohydrate, and fat.",
|
|
3419
3420
|
"Use the dedicated nutrition tools for food logs, body check-ins, appearance check-ins, subjective food effects, gut check-ins, nutrition patterns, and N-of-1 experiments instead of generic batch CRUD.",
|
|
3420
3421
|
"Ask for the one outcome metric that would make a nutrition experiment interpretable before turning repeated observations into a hypothesis."
|
|
3421
3422
|
]
|
|
@@ -4051,7 +4052,7 @@ function enrichConversationPlaybookWithRouteInfo(playbook) {
|
|
|
4051
4052
|
...buildPlaybookRouteInfo(playbook.focus)
|
|
4052
4053
|
};
|
|
4053
4054
|
}
|
|
4054
|
-
const AGENT_ONBOARDING_TOOL_INPUT_CATALOG = [
|
|
4055
|
+
export const AGENT_ONBOARDING_TOOL_INPUT_CATALOG = [
|
|
4055
4056
|
{
|
|
4056
4057
|
toolName: "forge_get_user_directory",
|
|
4057
4058
|
summary: "Read the live human/bot directory and directional relationship graph.",
|
|
@@ -4299,10 +4300,36 @@ const AGENT_ONBOARDING_TOOL_INPUT_CATALOG = [
|
|
|
4299
4300
|
notes: [
|
|
4300
4301
|
"The API path is /api/v1/health/weight-loss and the UI route is /weight-loss.",
|
|
4301
4302
|
"Use the dedicated nutrition logging tools for food, body, appearance, subjective, gut, and experiment mutations.",
|
|
4303
|
+
"Before logging a custom food, search the local/public nutrition catalog; if there is no match, research calories and macros from reliable internet nutrition sources before creating the custom item.",
|
|
4302
4304
|
"Food parsing uses Forge's configured openai-codex ChatGPT subscription connection, not the metered OpenAI Platform API."
|
|
4303
4305
|
],
|
|
4304
4306
|
example: '{"userIds":["user_operator"]}'
|
|
4305
4307
|
},
|
|
4308
|
+
{
|
|
4309
|
+
toolName: "forge_search_foods | forge_search_nutrition_foods",
|
|
4310
|
+
summary: "Search Forge's local custom/cache database plus Open Food Facts and USDA-backed public nutrition sources before logging a food item.",
|
|
4311
|
+
whenToUse: "Use before forge_log_food whenever the user names a food, brand, meal component, or custom food that might already exist.",
|
|
4312
|
+
inputShape: "{ query: string, limit?: number, userIds?: string[] }",
|
|
4313
|
+
requiredFields: ["query"],
|
|
4314
|
+
notes: [
|
|
4315
|
+
"Reuse a returned food by passing its id as item.foodId to forge_log_food.",
|
|
4316
|
+
"The local results include custom foods previously added to Forge, so this prevents duplicate custom-food records.",
|
|
4317
|
+
"If no result is good enough, research nutrition facts on the internet before creating a custom item."
|
|
4318
|
+
],
|
|
4319
|
+
example: '{"query":"Greek yogurt 2%","limit":5}'
|
|
4320
|
+
},
|
|
4321
|
+
{
|
|
4322
|
+
toolName: "forge_lookup_nutrition_barcode",
|
|
4323
|
+
summary: "Lookup a packaged food by barcode before logging it, using Forge's nutrition catalog adapters.",
|
|
4324
|
+
whenToUse: "Use when the user gives a barcode or a packaged-food photo/label includes one.",
|
|
4325
|
+
inputShape: "{ barcode: string, userIds?: string[] }",
|
|
4326
|
+
requiredFields: ["barcode"],
|
|
4327
|
+
notes: [
|
|
4328
|
+
"Reuse the returned food by passing its id as item.foodId to forge_log_food.",
|
|
4329
|
+
"If barcode lookup fails, search by name and brand before creating a custom food."
|
|
4330
|
+
],
|
|
4331
|
+
example: '{"barcode":"737628064502"}'
|
|
4332
|
+
},
|
|
4306
4333
|
{
|
|
4307
4334
|
toolName: "forge_parse_food_log_with_chatgpt",
|
|
4308
4335
|
summary: "Parse natural-language food text or a photo description into a candidate Forge nutrition log through the openai-codex ChatGPT subscription connection.",
|
|
@@ -4320,13 +4347,16 @@ const AGENT_ONBOARDING_TOOL_INPUT_CATALOG = [
|
|
|
4320
4347
|
toolName: "forge_log_food",
|
|
4321
4348
|
summary: "Create a food log with explicit food items, calories, macros, quality tags, hunger, satiety, cravings, and context.",
|
|
4322
4349
|
whenToUse: "Use for confirmed manual entries, search/barcode-selected foods, or corrected ChatGPT candidates.",
|
|
4323
|
-
inputShape: "{ mealLabel?: string, loggedAt?: string, source?: string, confirmationState?: string, notes?: string, items: Array<{ name, quantity, unit?, caloriesKcal?, proteinG?, carbsG?, fatG?, fiberG?, tags? }>, userIds?: string[] }",
|
|
4350
|
+
inputShape: "{ mealLabel?: string, loggedAt?: string, source?: string, confirmationState?: string, notes?: string, items: Array<{ foodId?, name, quantity, unit?, grams?, caloriesKcal?, proteinG?, carbsG?, fatG?, fiberG?, tags? }>, userIds?: string[] }",
|
|
4324
4351
|
requiredFields: ["items"],
|
|
4325
4352
|
notes: [
|
|
4353
|
+
"Always call forge_search_foods/forge_search_nutrition_foods or barcode lookup first and pass item.foodId when there is a good match.",
|
|
4354
|
+
"If item.foodId is omitted, Forge creates or reuses a local custom food. Custom foods must include caloriesKcal, proteinG, carbsG, and fatG; name-only custom foods are rejected.",
|
|
4355
|
+
"If the user does not know those nutrition facts, look them up on the internet from a reliable source before calling forge_log_food, or log a candidate only after estimating all four required values.",
|
|
4326
4356
|
"Prefer confirmed entries when the user gave concrete amounts.",
|
|
4327
4357
|
"Use candidate entries for rough estimates that need later correction."
|
|
4328
4358
|
],
|
|
4329
|
-
example: '{"mealLabel":"post-workout","items":[{"name":"Greek yogurt","quantity":250,"unit":"g"
|
|
4359
|
+
example: '{"mealLabel":"post-workout","items":[{"foodId":"food_123","name":"Greek yogurt","quantity":250,"unit":"g"}]}'
|
|
4330
4360
|
},
|
|
4331
4361
|
{
|
|
4332
4362
|
toolName: "forge_log_body_checkin | forge_log_appearance_checkin | forge_log_subjective_food_effect | forge_log_gut_checkin",
|
|
@@ -4710,7 +4740,7 @@ function buildAgentOnboardingPayload(request) {
|
|
|
4710
4740
|
movement: "Forge Movement is the first-class mobility surface. It is a timeline of stays and trips: stays capture time spent in the same place, and trips capture travel between places. Use it for time-in-place questions, travel-history review, specific stay or trip edits, selected-span aggregates, known places, and links to other Forge records rather than pretending stays and trips are normal batch CRUD entities.",
|
|
4711
4741
|
lifeForce: "Life Force is Forge's energy-budget and fatigue model. Read it through the dedicated life-force state and update it through focused profile, weekday-template, and fatigue-signal routes rather than generic entity CRUD.",
|
|
4712
4742
|
workbench: "Workbench is Forge's graph-flow execution system. Treat flows, runs, published outputs, node results, and latest-node-output reads as a dedicated API family instead of a normal entity-batch surface.",
|
|
4713
|
-
weightLoss: "Weight Loss is Forge's nutrition, body-composition, sport-fueling, appearance, gut-comfort, craving, and subjective-energy surface. Read it through the health overview route and use the dedicated nutrition tools for food logs, body check-ins, appearance check-ins, subjective effects, gut check-ins, and N-of-1 experiments instead of inventing batch CRUD records.",
|
|
4743
|
+
weightLoss: "Weight Loss is Forge's nutrition, body-composition, sport-fueling, appearance, gut-comfort, craving, and subjective-energy surface. Read it through the health overview route and use the dedicated nutrition tools for food logs, reusable searched/custom food catalog entries, body check-ins, appearance check-ins, subjective effects, gut check-ins, and N-of-1 experiments instead of inventing batch CRUD records.",
|
|
4714
4744
|
psyche: "Forge Psyche is the reflective domain for values, patterns, behaviors, beliefs, modes, flashcards, and trigger reports. It is sensitive and should be handled deliberately."
|
|
4715
4745
|
},
|
|
4716
4746
|
psycheSubmoduleModel: {
|
|
@@ -37,6 +37,7 @@ const mealItemInputSchema = z.preprocess((value) => {
|
|
|
37
37
|
id: z.string().trim().min(1).optional(),
|
|
38
38
|
foodId: z.string().trim().min(1).nullable().optional(),
|
|
39
39
|
name: z.string().trim().min(1),
|
|
40
|
+
brand: z.string().trim().nullable().optional(),
|
|
40
41
|
quantity: z.coerce.number().positive().default(1),
|
|
41
42
|
unit: z.string().trim().min(1).default("serving"),
|
|
42
43
|
grams: optionalNumberSchema,
|
|
@@ -54,6 +55,29 @@ const mealItemInputSchema = z.preprocess((value) => {
|
|
|
54
55
|
nutrients: z.record(z.string(), z.unknown()).default({}),
|
|
55
56
|
confidence: z.coerce.number().min(0).max(1).default(0.65)
|
|
56
57
|
}));
|
|
58
|
+
const requiredNutritionFields = [
|
|
59
|
+
"calories",
|
|
60
|
+
"proteinGrams",
|
|
61
|
+
"carbohydrateGrams",
|
|
62
|
+
"fatGrams"
|
|
63
|
+
];
|
|
64
|
+
function hasNutritionValue(value) {
|
|
65
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0;
|
|
66
|
+
}
|
|
67
|
+
function missingRequiredNutritionFields(input) {
|
|
68
|
+
return requiredNutritionFields.filter((field) => !hasNutritionValue(input[field]));
|
|
69
|
+
}
|
|
70
|
+
function nutritionRequirementError(path, name) {
|
|
71
|
+
return new z.ZodError([
|
|
72
|
+
{
|
|
73
|
+
code: z.ZodIssueCode.custom,
|
|
74
|
+
path,
|
|
75
|
+
message: `Food item "${name}" must include calories, proteinGrams, ` +
|
|
76
|
+
"carbohydrateGrams, and fatGrams. Search Forge/public nutrition " +
|
|
77
|
+
"sources first, or add a custom food only after researching those facts."
|
|
78
|
+
}
|
|
79
|
+
]);
|
|
80
|
+
}
|
|
57
81
|
export const nutritionFoodSearchSchema = z.object({
|
|
58
82
|
query: z.string().trim().min(1),
|
|
59
83
|
limit: z.coerce.number().int().min(1).max(25).default(12)
|
|
@@ -575,6 +599,141 @@ function mapFood(row) {
|
|
|
575
599
|
updatedAt: row.updated_at
|
|
576
600
|
};
|
|
577
601
|
}
|
|
602
|
+
function getFoodCatalogById(foodId) {
|
|
603
|
+
const row = getDatabase()
|
|
604
|
+
.prepare(`SELECT * FROM nutrition_food_catalog WHERE id = ?`)
|
|
605
|
+
.get(foodId);
|
|
606
|
+
return row ? mapFood(row) : null;
|
|
607
|
+
}
|
|
608
|
+
function consumedGramQuantity(input) {
|
|
609
|
+
if (hasNutritionValue(input.grams) && input.grams > 0) {
|
|
610
|
+
return input.grams;
|
|
611
|
+
}
|
|
612
|
+
const unit = normalizeCustomFoodToken(input.unit);
|
|
613
|
+
if (hasNutritionValue(input.quantity) &&
|
|
614
|
+
input.quantity > 0 &&
|
|
615
|
+
["g", "gram", "grams"].includes(unit)) {
|
|
616
|
+
return input.quantity;
|
|
617
|
+
}
|
|
618
|
+
return null;
|
|
619
|
+
}
|
|
620
|
+
function catalogScaleFactor(input, food) {
|
|
621
|
+
const consumedGrams = consumedGramQuantity(input);
|
|
622
|
+
if (consumedGrams != null &&
|
|
623
|
+
hasNutritionValue(food.servingGrams) &&
|
|
624
|
+
food.servingGrams > 0) {
|
|
625
|
+
return consumedGrams / food.servingGrams;
|
|
626
|
+
}
|
|
627
|
+
const unit = normalizeCustomFoodToken(input.unit);
|
|
628
|
+
if (hasNutritionValue(input.quantity) &&
|
|
629
|
+
input.quantity > 0 &&
|
|
630
|
+
(!unit || ["serving", "servings"].includes(unit))) {
|
|
631
|
+
return input.quantity;
|
|
632
|
+
}
|
|
633
|
+
return 1;
|
|
634
|
+
}
|
|
635
|
+
function scaledCatalogNutrition(value, scaleFactor) {
|
|
636
|
+
return hasNutritionValue(value) ? round(value * scaleFactor, 2) : undefined;
|
|
637
|
+
}
|
|
638
|
+
function normalizeCustomFoodToken(value) {
|
|
639
|
+
return (value ?? "")
|
|
640
|
+
.trim()
|
|
641
|
+
.toLowerCase()
|
|
642
|
+
.normalize("NFKD")
|
|
643
|
+
.replace(/[\u0300-\u036f]/g, "")
|
|
644
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
645
|
+
.replace(/^-+|-+$/g, "");
|
|
646
|
+
}
|
|
647
|
+
function customFoodServingKey(input) {
|
|
648
|
+
if (hasNutritionValue(input.grams) && input.grams > 0) {
|
|
649
|
+
return `${round(input.grams, 1)}g`;
|
|
650
|
+
}
|
|
651
|
+
return `${round(input.quantity, 3)}-${normalizeCustomFoodToken(input.unit) || "serving"}`;
|
|
652
|
+
}
|
|
653
|
+
function customFoodSourceId(input) {
|
|
654
|
+
const nameKey = normalizeCustomFoodToken(input.name);
|
|
655
|
+
const brandKey = normalizeCustomFoodToken(input.brand ?? "");
|
|
656
|
+
return [nameKey, brandKey, customFoodServingKey(input)]
|
|
657
|
+
.filter(Boolean)
|
|
658
|
+
.join(":");
|
|
659
|
+
}
|
|
660
|
+
function cacheCustomFood(input) {
|
|
661
|
+
return cacheFood({
|
|
662
|
+
source: "custom",
|
|
663
|
+
sourceId: customFoodSourceId(input),
|
|
664
|
+
name: input.name,
|
|
665
|
+
brand: input.brand ?? "",
|
|
666
|
+
servingLabel: input.unit && input.unit !== "serving"
|
|
667
|
+
? `${input.quantity} ${input.unit}`
|
|
668
|
+
: "1 serving",
|
|
669
|
+
servingGrams: input.grams ?? null,
|
|
670
|
+
calories: input.calories,
|
|
671
|
+
proteinGrams: input.proteinGrams,
|
|
672
|
+
carbohydrateGrams: input.carbohydrateGrams,
|
|
673
|
+
fatGrams: input.fatGrams,
|
|
674
|
+
fiberGrams: input.fiberGrams ?? null,
|
|
675
|
+
sugarGrams: input.sugarGrams ?? null,
|
|
676
|
+
sodiumMg: input.sodiumMg ?? null,
|
|
677
|
+
potassiumMg: input.potassiumMg ?? null,
|
|
678
|
+
tags: Array.from(new Set(["custom", ...input.tags])),
|
|
679
|
+
nutrients: {
|
|
680
|
+
...input.nutrients,
|
|
681
|
+
customFood: true,
|
|
682
|
+
nutritionBasis: "agent_or_user_supplied"
|
|
683
|
+
},
|
|
684
|
+
confidence: input.confidence
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
function resolveMealItemForInsert(input, options) {
|
|
688
|
+
let item = input;
|
|
689
|
+
if (input.foodId) {
|
|
690
|
+
const food = getFoodCatalogById(input.foodId);
|
|
691
|
+
if (!food) {
|
|
692
|
+
throw new z.ZodError([
|
|
693
|
+
{
|
|
694
|
+
code: z.ZodIssueCode.custom,
|
|
695
|
+
path: ["items", "foodId"],
|
|
696
|
+
message: `Food catalog item ${input.foodId} was not found. Search foods again before logging.`
|
|
697
|
+
}
|
|
698
|
+
]);
|
|
699
|
+
}
|
|
700
|
+
const scaleFactor = catalogScaleFactor(input, food);
|
|
701
|
+
item = {
|
|
702
|
+
...input,
|
|
703
|
+
name: input.name || food.name,
|
|
704
|
+
calories: input.calories ?? scaledCatalogNutrition(food.calories, scaleFactor),
|
|
705
|
+
proteinGrams: input.proteinGrams ??
|
|
706
|
+
scaledCatalogNutrition(food.proteinGrams, scaleFactor),
|
|
707
|
+
carbohydrateGrams: input.carbohydrateGrams ??
|
|
708
|
+
scaledCatalogNutrition(food.carbohydrateGrams, scaleFactor),
|
|
709
|
+
fatGrams: input.fatGrams ?? scaledCatalogNutrition(food.fatGrams, scaleFactor),
|
|
710
|
+
fiberGrams: input.fiberGrams ??
|
|
711
|
+
scaledCatalogNutrition(food.fiberGrams, scaleFactor),
|
|
712
|
+
sugarGrams: input.sugarGrams ??
|
|
713
|
+
scaledCatalogNutrition(food.sugarGrams, scaleFactor),
|
|
714
|
+
sodiumMg: input.sodiumMg ?? scaledCatalogNutrition(food.sodiumMg, scaleFactor),
|
|
715
|
+
potassiumMg: input.potassiumMg ??
|
|
716
|
+
scaledCatalogNutrition(food.potassiumMg, scaleFactor),
|
|
717
|
+
caffeineMg: input.caffeineMg ??
|
|
718
|
+
scaledCatalogNutrition(food.caffeineMg, scaleFactor),
|
|
719
|
+
alcoholGrams: input.alcoholGrams ??
|
|
720
|
+
scaledCatalogNutrition(food.alcoholGrams, scaleFactor),
|
|
721
|
+
nutrients: Object.keys(input.nutrients).length > 0
|
|
722
|
+
? input.nutrients
|
|
723
|
+
: food.nutrients,
|
|
724
|
+
confidence: input.confidence ?? food.confidence
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
const missing = missingRequiredNutritionFields(item);
|
|
728
|
+
if (missing.length > 0) {
|
|
729
|
+
throw nutritionRequirementError(["items"], item.name);
|
|
730
|
+
}
|
|
731
|
+
if (!item.foodId && options.cacheConfirmedCustomFood) {
|
|
732
|
+
const customFood = cacheCustomFood(item);
|
|
733
|
+
item = { ...item, foodId: customFood.id };
|
|
734
|
+
}
|
|
735
|
+
return item;
|
|
736
|
+
}
|
|
578
737
|
function mapItem(row) {
|
|
579
738
|
return {
|
|
580
739
|
id: row.id,
|
|
@@ -685,9 +844,10 @@ function listFoodLogs(userId, limit = 120) {
|
|
|
685
844
|
const itemsByLog = readMealItems(rows.map((row) => row.id));
|
|
686
845
|
return rows.map((row) => mapFoodLog(row, itemsByLog.get(row.id) ?? []));
|
|
687
846
|
}
|
|
688
|
-
function insertMealItem(logId, input) {
|
|
847
|
+
function insertMealItem(logId, input, options) {
|
|
848
|
+
const item = resolveMealItemForInsert(input, options);
|
|
689
849
|
const now = nowIso();
|
|
690
|
-
const id =
|
|
850
|
+
const id = item.id ?? newId("meal_item");
|
|
691
851
|
getDatabase()
|
|
692
852
|
.prepare(`INSERT INTO nutrition_meal_items (
|
|
693
853
|
id, log_id, food_id, name, quantity, unit, grams, calories,
|
|
@@ -695,7 +855,41 @@ function insertMealItem(logId, input) {
|
|
|
695
855
|
sodium_mg, potassium_mg, caffeine_mg, alcohol_grams, tags_json,
|
|
696
856
|
nutrients_json, confidence, created_at, updated_at
|
|
697
857
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
698
|
-
.run(id, logId,
|
|
858
|
+
.run(id, logId, item.foodId ?? null, item.name, item.quantity, item.unit, item.grams ?? null, item.calories ?? null, item.proteinGrams ?? null, item.carbohydrateGrams ?? null, item.fatGrams ?? null, item.fiberGrams ?? null, item.sugarGrams ?? null, item.sodiumMg ?? null, item.potassiumMg ?? null, item.caffeineMg ?? null, item.alcoholGrams ?? null, jsonString(item.tags), jsonString(item.nutrients), item.confidence, now, now);
|
|
859
|
+
}
|
|
860
|
+
function cacheConfirmedCustomMealItems(logId) {
|
|
861
|
+
const rows = readMealItems([logId]).get(logId) ?? [];
|
|
862
|
+
for (const row of rows) {
|
|
863
|
+
if (row.food_id) {
|
|
864
|
+
continue;
|
|
865
|
+
}
|
|
866
|
+
const item = resolveMealItemForInsert({
|
|
867
|
+
id: row.id,
|
|
868
|
+
foodId: null,
|
|
869
|
+
name: row.name,
|
|
870
|
+
quantity: row.quantity,
|
|
871
|
+
unit: row.unit,
|
|
872
|
+
grams: row.grams ?? undefined,
|
|
873
|
+
calories: row.calories ?? undefined,
|
|
874
|
+
proteinGrams: row.protein_grams ?? undefined,
|
|
875
|
+
carbohydrateGrams: row.carbohydrate_grams ?? undefined,
|
|
876
|
+
fatGrams: row.fat_grams ?? undefined,
|
|
877
|
+
fiberGrams: row.fiber_grams ?? undefined,
|
|
878
|
+
sugarGrams: row.sugar_grams ?? undefined,
|
|
879
|
+
sodiumMg: row.sodium_mg ?? undefined,
|
|
880
|
+
potassiumMg: row.potassium_mg ?? undefined,
|
|
881
|
+
caffeineMg: row.caffeine_mg ?? undefined,
|
|
882
|
+
alcoholGrams: row.alcohol_grams ?? undefined,
|
|
883
|
+
tags: parseJson(row.tags_json, []),
|
|
884
|
+
nutrients: parseJson(row.nutrients_json, {}),
|
|
885
|
+
confidence: row.confidence
|
|
886
|
+
}, { cacheConfirmedCustomFood: true });
|
|
887
|
+
getDatabase()
|
|
888
|
+
.prepare(`UPDATE nutrition_meal_items
|
|
889
|
+
SET food_id = ?, updated_at = ?
|
|
890
|
+
WHERE id = ?`)
|
|
891
|
+
.run(item.foodId ?? null, nowIso(), row.id);
|
|
892
|
+
}
|
|
699
893
|
}
|
|
700
894
|
export function createNutritionFoodLog(input) {
|
|
701
895
|
const parsed = nutritionFoodLogCreateSchema.parse(input);
|
|
@@ -713,7 +907,9 @@ export function createNutritionFoodLog(input) {
|
|
|
713
907
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
714
908
|
.run(id, userId, loggedAt, parsed.mealLabel, parsed.source, parsed.confirmationState, parsed.notes, parsed.placeId ?? null, parsed.stayId ?? null, parsed.workoutId ?? null, parsed.sleepId ?? null, logDayKey, jsonString(parsed.imageRefs), jsonString(parsed.parserProvenance), jsonString(parsed.links), now, now);
|
|
715
909
|
for (const item of parsed.items) {
|
|
716
|
-
insertMealItem(id, item
|
|
910
|
+
insertMealItem(id, item, {
|
|
911
|
+
cacheConfirmedCustomFood: parsed.confirmationState === "confirmed"
|
|
912
|
+
});
|
|
717
913
|
}
|
|
718
914
|
});
|
|
719
915
|
return getNutritionFoodLogById(id);
|
|
@@ -726,6 +922,7 @@ export function patchNutritionFoodLog(logId, input) {
|
|
|
726
922
|
}
|
|
727
923
|
const nextLoggedAt = parsed.loggedAt ?? existing.loggedAt;
|
|
728
924
|
const nextDayKey = normalizeDayKey(parsed.dayKey, nextLoggedAt);
|
|
925
|
+
const nextConfirmationState = parsed.confirmationState ?? existing.confirmationState;
|
|
729
926
|
const now = nowIso();
|
|
730
927
|
runInTransaction(() => {
|
|
731
928
|
getDatabase()
|
|
@@ -735,15 +932,20 @@ export function patchNutritionFoodLog(logId, input) {
|
|
|
735
932
|
day_key = ?, image_refs_json = ?, parser_provenance_json = ?,
|
|
736
933
|
links_json = ?, updated_at = ?
|
|
737
934
|
WHERE id = ?`)
|
|
738
|
-
.run(nextLoggedAt, parsed.mealLabel ?? existing.mealLabel, parsed.source ?? existing.source,
|
|
935
|
+
.run(nextLoggedAt, parsed.mealLabel ?? existing.mealLabel, parsed.source ?? existing.source, nextConfirmationState, parsed.notes ?? existing.notes, parsed.placeId !== undefined ? parsed.placeId : existing.placeId, parsed.stayId !== undefined ? parsed.stayId : existing.stayId, parsed.workoutId !== undefined ? parsed.workoutId : existing.workoutId, parsed.sleepId !== undefined ? parsed.sleepId : existing.sleepId, nextDayKey, jsonString(parsed.imageRefs ?? existing.imageRefs), jsonString(parsed.parserProvenance ?? existing.parserProvenance), jsonString(parsed.links ?? existing.links), now, logId);
|
|
739
936
|
if (parsed.items) {
|
|
740
937
|
getDatabase()
|
|
741
938
|
.prepare(`DELETE FROM nutrition_meal_items WHERE log_id = ?`)
|
|
742
939
|
.run(logId);
|
|
743
940
|
for (const item of parsed.items) {
|
|
744
|
-
insertMealItem(logId, item
|
|
941
|
+
insertMealItem(logId, item, {
|
|
942
|
+
cacheConfirmedCustomFood: nextConfirmationState === "confirmed"
|
|
943
|
+
});
|
|
745
944
|
}
|
|
746
945
|
}
|
|
946
|
+
else if (nextConfirmationState === "confirmed") {
|
|
947
|
+
cacheConfirmedCustomMealItems(logId);
|
|
948
|
+
}
|
|
747
949
|
});
|
|
748
950
|
return getNutritionFoodLogById(logId);
|
|
749
951
|
}
|
|
@@ -5286,11 +5286,14 @@ export function buildOpenApiDocument() {
|
|
|
5286
5286
|
type: "object",
|
|
5287
5287
|
additionalProperties: false,
|
|
5288
5288
|
required: ["name", "quantity"],
|
|
5289
|
+
description: "Meal item input. Pass foodId from /foods/search or /foods/barcode when reusing a catalog food. If foodId is omitted, Forge treats the item as a custom food and requires calories, proteinGrams, carbohydrateGrams, and fatGrams.",
|
|
5289
5290
|
properties: {
|
|
5291
|
+
foodId: nullable({ type: "string" }),
|
|
5290
5292
|
name: { type: "string" },
|
|
5291
5293
|
brand: nullable({ type: "string" }),
|
|
5292
5294
|
quantity: { type: "number" },
|
|
5293
5295
|
unit: nullable({ type: "string" }),
|
|
5296
|
+
grams: nullable({ type: "number" }),
|
|
5294
5297
|
calories: nullable({ type: "number" }),
|
|
5295
5298
|
proteinGrams: nullable({ type: "number" }),
|
|
5296
5299
|
carbohydrateGrams: nullable({ type: "number" }),
|
|
@@ -5926,6 +5929,7 @@ export function buildOpenApiDocument() {
|
|
|
5926
5929
|
post: {
|
|
5927
5930
|
tags: ["Health"],
|
|
5928
5931
|
summary: "Search nutrition foods across local and public catalogs",
|
|
5932
|
+
description: "Searches Forge's local nutrition_food_catalog first, including custom foods, then public Open Food Facts and USDA-backed sources. Reuse returned ids as NutritionMealItemInput.foodId before creating a new custom food.",
|
|
5929
5933
|
requestBody: {
|
|
5930
5934
|
content: {
|
|
5931
5935
|
"application/json": {
|
package/openclaw.plugin.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"id": "forge-openclaw-plugin",
|
|
3
3
|
"name": "Forge",
|
|
4
4
|
"description": "Curated OpenClaw adapter for the Forge collaboration API, UI entrypoint, and localhost auto-start runtime.",
|
|
5
|
-
"version": "0.2.
|
|
5
|
+
"version": "0.2.105",
|
|
6
6
|
"activation": {
|
|
7
7
|
"onStartup": true,
|
|
8
8
|
"onCapabilities": [
|
package/package.json
CHANGED
|
@@ -208,6 +208,13 @@ Health rule:
|
|
|
208
208
|
`forge_log_appearance_checkin`, `forge_log_subjective_food_effect`,
|
|
209
209
|
`forge_log_gut_checkin`, `forge_get_nutrition_patterns`,
|
|
210
210
|
`forge_start_nutrition_experiment`, and `forge_update_nutrition_experiment`.
|
|
211
|
+
Before logging food, search Forge's nutrition catalog first. If a result matches,
|
|
212
|
+
pass that result's `id` as `item.foodId` to `forge_log_food` so Forge reuses the
|
|
213
|
+
local/custom food database instead of creating a duplicate. If no result matches
|
|
214
|
+
and you are creating a custom food, look up nutrition facts on the internet or
|
|
215
|
+
another reliable public nutrition source before logging it. Custom/no-`foodId`
|
|
216
|
+
items must include at least calories plus protein, carbohydrate, and fat
|
|
217
|
+
(`caloriesKcal`, `proteinG`, `carbsG`, `fatG`); never save a name-only food.
|
|
211
218
|
`forge_parse_food_log_with_chatgpt` must use Forge's configured `openai-codex`
|
|
212
219
|
ChatGPT subscription connection, not a metered OpenAI Platform API path.
|
|
213
220
|
- In `forge_get_agent_onboarding.entityRouteModel.readModelOnlySurfaces`, operator,
|
|
@@ -702,7 +709,7 @@ Use the health tools when the request is about sleep or sports review:
|
|
|
702
709
|
- `forge_get_training_load_overview` to inspect cardiovascular load, HR zone balance, zone-time buckets, smart training modes, acute/chronic stress, high-intensity pressure, VO2max context, next-workout guidance, and training target fit
|
|
703
710
|
- `forge_get_weight_loss_overview` to inspect calorie balance, protein/fiber targets, body trend, food quality, training fuel, subjective energy, gut comfort, aesthetic look, hypotheses, and experiments
|
|
704
711
|
- `forge_parse_food_log_with_chatgpt` to convert rough meal text or a photo description into a candidate food log through the configured `openai-codex` ChatGPT subscription connection
|
|
705
|
-
- `forge_log_food`, `forge_log_body_checkin`, `forge_log_appearance_checkin`, `forge_log_subjective_food_effect`, and `forge_log_gut_checkin` to preserve the user's food, body-composition, visual-look, energy/craving/performance, and gut-health evidence
|
|
712
|
+
- `forge_log_food`, `forge_log_body_checkin`, `forge_log_appearance_checkin`, `forge_log_subjective_food_effect`, and `forge_log_gut_checkin` to preserve the user's food, body-composition, visual-look, energy/craving/performance, and gut-health evidence. For `forge_log_food`, search first and reuse `item.foodId`; custom/no-`foodId` items require calories plus protein, carbohydrate, and fat, researched before logging when the user does not provide them.
|
|
706
713
|
- `forge_get_nutrition_patterns`, `forge_start_nutrition_experiment`, and `forge_update_nutrition_experiment` to turn repeated food/body observations into testable N-of-1 hypotheses
|
|
707
714
|
- `forge_update_sleep_session` to add sleep-quality notes, tags, or links back to Forge entities after review
|
|
708
715
|
- `forge_update_workout_session` to add subjective effort, mood, meaning, tags, or links on one workout after review
|
|
@@ -415,7 +415,10 @@ still knowing the exact write/read family before it acts.
|
|
|
415
415
|
hypotheses, or nutrition experiments. Use `forge_parse_food_log_with_chatgpt`
|
|
416
416
|
for rough meal text/photo descriptions through the configured `openai-codex`
|
|
417
417
|
ChatGPT subscription connection, then use `forge_log_food` and the body,
|
|
418
|
-
appearance, subjective, gut, and experiment tools for durable evidence.
|
|
418
|
+
appearance, subjective, gut, and experiment tools for durable evidence. Search
|
|
419
|
+
foods first and reuse returned `foodId` values. If a custom food is needed,
|
|
420
|
+
research calories plus protein, carbohydrate, and fat from reliable internet
|
|
421
|
+
nutrition sources before logging; name-only custom foods are invalid.
|
|
419
422
|
- `movement`: specialized domain surface. Use the dedicated movement routes for day,
|
|
420
423
|
month, all-time, timeline, places, trip detail, selection aggregates, manual
|
|
421
424
|
overlays, and repair actions.
|
|
@@ -1598,7 +1601,11 @@ Arc:
|
|
|
1598
1601
|
4. Use `forge_log_food`, `forge_log_body_checkin`,
|
|
1599
1602
|
`forge_log_appearance_checkin`, `forge_log_subjective_food_effect`, and
|
|
1600
1603
|
`forge_log_gut_checkin` to preserve the user's actual evidence.
|
|
1601
|
-
5.
|
|
1604
|
+
5. For `forge_log_food`, call `forge_search_foods` or barcode lookup first. Reuse
|
|
1605
|
+
a matching result through `item.foodId`. If there is no match, create a custom
|
|
1606
|
+
food only after researching calories, protein, carbohydrate, and fat; include
|
|
1607
|
+
`caloriesKcal`, `proteinG`, `carbsG`, and `fatG` in the item.
|
|
1608
|
+
6. Use `forge_get_nutrition_patterns`, `forge_start_nutrition_experiment`, and
|
|
1602
1609
|
`forge_update_nutrition_experiment` when repeated observations should become
|
|
1603
1610
|
an N-of-1 test instead of vague advice.
|
|
1604
1611
|
|
|
@@ -1607,6 +1614,8 @@ Helpful follow-up lanes:
|
|
|
1607
1614
|
- whether the decision is weight trend, protein/fiber sufficiency, sport fuel,
|
|
1608
1615
|
visual look, water retention, gut comfort, cravings, or energy
|
|
1609
1616
|
- whether a meal should be confirmed precisely or logged as a candidate estimate
|
|
1617
|
+
- whether a searched/catalog food can be reused by `foodId` or whether a researched
|
|
1618
|
+
custom food with calories and macros is needed
|
|
1610
1619
|
- which outcome metric should define a nutrition experiment before interpreting it
|
|
1611
1620
|
|
|
1612
1621
|
Route note:
|
|
@@ -1614,7 +1623,9 @@ Route note:
|
|
|
1614
1623
|
- `weight_loss` is a health read model plus dedicated nutrition write workflow.
|
|
1615
1624
|
Use `/api/v1/health/weight-loss` or `forge_get_weight_loss_overview` for the
|
|
1616
1625
|
overview. Do not invent generic batch entities for food logs or body check-ins
|
|
1617
|
-
when the dedicated tools exist.
|
|
1626
|
+
when the dedicated tools exist. Food search reads Forge's local custom/cache
|
|
1627
|
+
database plus public nutrition catalogs. `forge_log_food` rejects custom items
|
|
1628
|
+
without calories, protein, carbohydrate, and fat.
|
|
1618
1629
|
|
|
1619
1630
|
Ready to review when:
|
|
1620
1631
|
|