hevy-mcp 4.1.3 → 5.0.0
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/README.md +9 -8
- package/dist/cli.mjs +2 -2
- package/dist/index.mjs +2 -2
- package/dist/{src-CBiQ6f-q.mjs → src-BUmpaxmH.mjs} +862 -949
- package/package.json +1 -1
- package/server.json +2 -2
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
(function() {
|
|
5
5
|
try {
|
|
6
6
|
var e = "undefined" != typeof window ? window : "undefined" != typeof global ? global : "undefined" != typeof globalThis ? globalThis : "undefined" != typeof self ? self : {};
|
|
7
|
-
e.SENTRY_RELEASE = { id: "hevy-mcp@
|
|
7
|
+
e.SENTRY_RELEASE = { id: "hevy-mcp@5.0.0" };
|
|
8
8
|
var n = new e.Error().stack;
|
|
9
|
-
n && (e._sentryDebugIds = e._sentryDebugIds || {}, e._sentryDebugIds[n] = "
|
|
9
|
+
n && (e._sentryDebugIds = e._sentryDebugIds || {}, e._sentryDebugIds[n] = "8ea46bdf-fa98-4e73-b4d5-a25044e016df", e._sentryDebugIdIdentifier = "sentry-dbid-8ea46bdf-fa98-4e73-b4d5-a25044e016df");
|
|
10
10
|
} catch (e) {}
|
|
11
11
|
})();
|
|
12
12
|
import { createHmac, randomUUID, timingSafeEqual } from "node:crypto";
|
|
@@ -80,11 +80,11 @@ function sanitizeSentryMcpSpan(span) {
|
|
|
80
80
|
* OTel Collector → Honeycomb: performance traces, metrics
|
|
81
81
|
*/
|
|
82
82
|
const name$1 = "hevy-mcp";
|
|
83
|
-
const version$1 = "
|
|
83
|
+
const version$1 = "5.0.0";
|
|
84
84
|
const telemetryEnabled = process.env.HEVY_MCP_TELEMETRY !== "0";
|
|
85
85
|
const collectorToken = "NH9vOela-HYreQxAbJa68cjEmORoEKM57EvneUDVcOo";
|
|
86
86
|
const COLLECTOR_ENDPOINT = "https://otel.chrisdoc.dev/v1";
|
|
87
|
-
const sentryRelease = process.env.SENTRY_RELEASE ?? `hevy-mcp@
|
|
87
|
+
const sentryRelease = process.env.SENTRY_RELEASE ?? `hevy-mcp@5.0.0`;
|
|
88
88
|
const resource = resourceFromAttributes({
|
|
89
89
|
"service.name": name$1,
|
|
90
90
|
"service.version": version$1
|
|
@@ -226,8 +226,8 @@ const repRangeShape = {
|
|
|
226
226
|
function optionalRepRangeSchema(schema) {
|
|
227
227
|
return z.preprocess((value) => value === null ? void 0 : value, schema);
|
|
228
228
|
}
|
|
229
|
-
|
|
230
|
-
optionalRepRangeSchema(z.object(repRangeShape).strict().optional());
|
|
229
|
+
optionalRepRangeSchema(z.object(repRangeShape).optional());
|
|
230
|
+
const zStrictOptionalRepRange = optionalRepRangeSchema(z.object(repRangeShape).strict().optional());
|
|
231
231
|
const setTypeEnum = z.enum([
|
|
232
232
|
"warmup",
|
|
233
233
|
"normal",
|
|
@@ -1468,8 +1468,8 @@ function withPromptObservation(name, observer, handler) {
|
|
|
1468
1468
|
scope = memoizeObservationScope(observer?.start({
|
|
1469
1469
|
name,
|
|
1470
1470
|
kind: "prompt",
|
|
1471
|
-
argumentKeys: Object.keys(args).filter((key) => key === "
|
|
1472
|
-
argumentPresence: args.
|
|
1471
|
+
argumentKeys: Object.keys(args).filter((key) => key === "routine_id"),
|
|
1472
|
+
argumentPresence: args.routine_id ? { routine_id: true } : {},
|
|
1473
1473
|
argumentKeyCountBucket: bucketCount(Object.keys(args).length)
|
|
1474
1474
|
}));
|
|
1475
1475
|
} catch {
|
|
@@ -1524,22 +1524,22 @@ function registerWorkoutPrompts(server, observer) {
|
|
|
1524
1524
|
title: "Create Workout From Routine",
|
|
1525
1525
|
description: "Create a completed workout from an existing routine.",
|
|
1526
1526
|
argsSchema: z.object({
|
|
1527
|
-
|
|
1528
|
-
|
|
1527
|
+
routine_id: z.string().min(1).optional().describe("Routine ID to use as a guide."),
|
|
1528
|
+
start_time: utcSecondTimestamp.optional().describe("Workout start time in UTC as YYYY-MM-DDTHH:mm:ssZ.")
|
|
1529
1529
|
})
|
|
1530
|
-
}, withPromptObservation("create-workout-from-routine", observer, ({
|
|
1530
|
+
}, withPromptObservation("create-workout-from-routine", observer, ({ routine_id, start_time }) => ({ messages: [{
|
|
1531
1531
|
role: "user",
|
|
1532
1532
|
content: {
|
|
1533
1533
|
type: "text",
|
|
1534
|
-
text:
|
|
1535
|
-
`Create a workout from routine ${
|
|
1536
|
-
"First call get-routine with the
|
|
1537
|
-
"Do not copy routine-only
|
|
1538
|
-
"Before calling create-workout, confirm or collect the user's actual completed set data for every set, including applicable
|
|
1539
|
-
"Also collect the required
|
|
1540
|
-
"Never invent completion data. If the actual results or
|
|
1534
|
+
text: routine_id && start_time ? [
|
|
1535
|
+
`Create a workout from routine ${routine_id}, starting at ${start_time}.`,
|
|
1536
|
+
["First call get-routine with the routine_id and map supported plan fields:", "routine title to workout title, plus each exercise_template_id, superset_id, exercise notes, and set type."].join(" "),
|
|
1537
|
+
["Do not copy routine-only rest_seconds or rep_range fields into", "create-workout."].join(" "),
|
|
1538
|
+
["Before calling create-workout, confirm or collect the user's actual completed set data for every set,", "including applicable weight_kg, reps, distance_meters, duration_seconds, rpe, or custom_metric values."].join(" "),
|
|
1539
|
+
["Also collect the required end_time in strict UTC YYYY-MM-DDTHH:mm:ssZ format and confirm", "any other missing required workout fields."].join(" "),
|
|
1540
|
+
["Never invent completion data. If the actual results or end_time are unavailable, ask the user for them", "instead of creating the workout."].join(" "),
|
|
1541
1541
|
"Once confirmed, call create-workout with only fields supported by that tool."
|
|
1542
|
-
].join("\n") : "Provide a
|
|
1542
|
+
].join("\n") : "Provide a routine_id and start_time to generate the full prompt."
|
|
1543
1543
|
}
|
|
1544
1544
|
}] })));
|
|
1545
1545
|
}
|
|
@@ -1845,7 +1845,7 @@ const getV1WorkoutsWorkoutid200Schema = z$1.lazy(() => workoutSchema);
|
|
|
1845
1845
|
z$1.any();
|
|
1846
1846
|
z$1.lazy(() => getV1WorkoutsWorkoutid200Schema);
|
|
1847
1847
|
//#endregion
|
|
1848
|
-
//#region ../core/src/utils/response-
|
|
1848
|
+
//#region ../core/src/utils/response-contracts.ts
|
|
1849
1849
|
function jsonText(data) {
|
|
1850
1850
|
return JSON.stringify(data, null, 2) ?? "null";
|
|
1851
1851
|
}
|
|
@@ -1883,146 +1883,150 @@ function defineJsonResponseContract(present, telemetry) {
|
|
|
1883
1883
|
function respond(contract, data) {
|
|
1884
1884
|
return contract.render(data);
|
|
1885
1885
|
}
|
|
1886
|
-
const
|
|
1886
|
+
const optionalNumber = z.number().optional();
|
|
1887
1887
|
const formattedWorkoutSetSchema = z.object({
|
|
1888
1888
|
index: z.number().optional(),
|
|
1889
1889
|
type: z.string().optional(),
|
|
1890
|
-
|
|
1891
|
-
reps:
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
rpe:
|
|
1895
|
-
|
|
1890
|
+
weight_kg: optionalNumber,
|
|
1891
|
+
reps: optionalNumber,
|
|
1892
|
+
distance_meters: optionalNumber,
|
|
1893
|
+
duration_seconds: optionalNumber,
|
|
1894
|
+
rpe: optionalNumber,
|
|
1895
|
+
custom_metric: optionalNumber
|
|
1896
1896
|
});
|
|
1897
1897
|
const formattedWorkoutExerciseSchema = z.object({
|
|
1898
1898
|
index: z.number().optional(),
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
notes: z.string().
|
|
1902
|
-
|
|
1899
|
+
title: z.string().optional(),
|
|
1900
|
+
exercise_template_id: z.string().optional(),
|
|
1901
|
+
notes: z.string().optional(),
|
|
1902
|
+
supersets_id: optionalNumber,
|
|
1903
1903
|
sets: z.array(formattedWorkoutSetSchema).optional()
|
|
1904
1904
|
});
|
|
1905
1905
|
const formattedWorkoutSchema = z.object({
|
|
1906
1906
|
id: z.string().optional(),
|
|
1907
|
+
routine_id: z.string().optional(),
|
|
1907
1908
|
title: z.string().optional(),
|
|
1908
|
-
description: z.string().
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1909
|
+
description: z.string().optional(),
|
|
1910
|
+
start_time: z.string().optional(),
|
|
1911
|
+
end_time: z.string().optional(),
|
|
1912
|
+
created_at: z.string().optional(),
|
|
1913
|
+
updated_at: z.string().optional(),
|
|
1913
1914
|
duration: z.string(),
|
|
1914
1915
|
exercises: z.array(formattedWorkoutExerciseSchema).optional()
|
|
1915
1916
|
});
|
|
1916
1917
|
const formattedRoutineSetSchema = z.object({
|
|
1917
1918
|
index: z.number().optional(),
|
|
1918
1919
|
type: z.string().optional(),
|
|
1919
|
-
|
|
1920
|
-
reps:
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
start: z.number().
|
|
1926
|
-
end: z.number().
|
|
1927
|
-
}).
|
|
1928
|
-
rpe:
|
|
1920
|
+
weight_kg: optionalNumber,
|
|
1921
|
+
reps: optionalNumber,
|
|
1922
|
+
distance_meters: optionalNumber,
|
|
1923
|
+
duration_seconds: optionalNumber,
|
|
1924
|
+
custom_metric: optionalNumber,
|
|
1925
|
+
rep_range: z.object({
|
|
1926
|
+
start: z.number().optional(),
|
|
1927
|
+
end: z.number().optional()
|
|
1928
|
+
}).optional(),
|
|
1929
|
+
rpe: optionalNumber
|
|
1929
1930
|
});
|
|
1930
1931
|
const formattedRoutineExerciseSchema = z.object({
|
|
1931
|
-
|
|
1932
|
+
title: z.string().optional(),
|
|
1932
1933
|
index: z.number().optional(),
|
|
1933
|
-
|
|
1934
|
-
notes: z.string().
|
|
1935
|
-
|
|
1936
|
-
|
|
1934
|
+
exercise_template_id: z.string().optional(),
|
|
1935
|
+
notes: z.string().optional(),
|
|
1936
|
+
supersets_id: optionalNumber,
|
|
1937
|
+
rest_seconds: z.string().optional(),
|
|
1937
1938
|
sets: z.array(formattedRoutineSetSchema).optional()
|
|
1938
1939
|
});
|
|
1939
1940
|
const formattedRoutineSchema = z.object({
|
|
1940
1941
|
id: z.string().optional(),
|
|
1941
1942
|
title: z.string().optional(),
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1943
|
+
folder_id: z.number().optional(),
|
|
1944
|
+
created_at: z.string().optional(),
|
|
1945
|
+
updated_at: z.string().optional(),
|
|
1945
1946
|
exercises: z.array(formattedRoutineExerciseSchema).optional()
|
|
1946
1947
|
});
|
|
1947
1948
|
const formattedRoutineFolderSchema = z.object({
|
|
1948
1949
|
id: z.number().optional(),
|
|
1949
1950
|
title: z.string().optional(),
|
|
1950
|
-
|
|
1951
|
-
|
|
1951
|
+
created_at: z.string().optional(),
|
|
1952
|
+
updated_at: z.string().optional()
|
|
1952
1953
|
});
|
|
1953
1954
|
const formattedExerciseTemplateSchema = z.object({
|
|
1954
1955
|
id: z.string().optional(),
|
|
1955
1956
|
title: z.string().optional(),
|
|
1956
1957
|
type: z.string().optional(),
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1958
|
+
primary_muscle_group: z.string().optional(),
|
|
1959
|
+
secondary_muscle_groups: z.array(z.string()).optional(),
|
|
1960
|
+
is_custom: z.boolean().optional()
|
|
1960
1961
|
});
|
|
1961
1962
|
const formattedExerciseHistoryEntrySchema = z.object({
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
reps:
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
rpe:
|
|
1972
|
-
|
|
1973
|
-
|
|
1963
|
+
workout_id: z.string().optional(),
|
|
1964
|
+
workout_title: z.string().optional(),
|
|
1965
|
+
workout_start_time: z.string().optional(),
|
|
1966
|
+
workout_end_time: z.string().optional(),
|
|
1967
|
+
exercise_template_id: z.string().optional(),
|
|
1968
|
+
weight_kg: optionalNumber,
|
|
1969
|
+
reps: optionalNumber,
|
|
1970
|
+
distance_meters: optionalNumber,
|
|
1971
|
+
duration_seconds: optionalNumber,
|
|
1972
|
+
rpe: optionalNumber,
|
|
1973
|
+
custom_metric: optionalNumber,
|
|
1974
|
+
set_type: z.string().optional()
|
|
1974
1975
|
});
|
|
1975
1976
|
const formattedBodyMeasurementSchema = z.object({
|
|
1976
1977
|
date: z.string(),
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
abdomen:
|
|
1988
|
-
waist:
|
|
1989
|
-
hips:
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1978
|
+
weight_kg: optionalNumber,
|
|
1979
|
+
lean_mass_kg: optionalNumber,
|
|
1980
|
+
fat_percent: optionalNumber,
|
|
1981
|
+
neck_cm: optionalNumber,
|
|
1982
|
+
shoulder_cm: optionalNumber,
|
|
1983
|
+
chest_cm: optionalNumber,
|
|
1984
|
+
left_bicep_cm: optionalNumber,
|
|
1985
|
+
right_bicep_cm: optionalNumber,
|
|
1986
|
+
left_forearm_cm: optionalNumber,
|
|
1987
|
+
right_forearm_cm: optionalNumber,
|
|
1988
|
+
abdomen: optionalNumber,
|
|
1989
|
+
waist: optionalNumber,
|
|
1990
|
+
hips: optionalNumber,
|
|
1991
|
+
left_thigh_cm: optionalNumber,
|
|
1992
|
+
right_thigh_cm: optionalNumber,
|
|
1993
|
+
left_calf_cm: optionalNumber,
|
|
1994
|
+
right_calf_cm: optionalNumber
|
|
1994
1995
|
});
|
|
1995
|
-
const
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
cacheStatus: z.enum([
|
|
1999
|
-
"hit",
|
|
2000
|
-
"miss",
|
|
2001
|
-
"not-used"
|
|
2002
|
-
]),
|
|
2003
|
-
itemsScanned: z.number().int().nonnegative()
|
|
1996
|
+
const scanSchema = z.object({
|
|
1997
|
+
pages: z.record(z.string(), z.number().int().nonnegative()),
|
|
1998
|
+
items: z.number().int().nonnegative()
|
|
2004
1999
|
});
|
|
2005
2000
|
const trainingSummarySessionSchema = z.object({
|
|
2006
2001
|
id: z.string().optional(),
|
|
2007
2002
|
title: z.string().optional(),
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2003
|
+
start_time: z.string().optional(),
|
|
2004
|
+
end_time: z.string().optional(),
|
|
2005
|
+
duration_seconds: z.number().int().nonnegative().optional(),
|
|
2006
|
+
exercise_count: z.number().int().nonnegative(),
|
|
2007
|
+
set_count: z.number().int().nonnegative()
|
|
2013
2008
|
});
|
|
2014
2009
|
const compactRoutineSchema = z.object({
|
|
2015
2010
|
id: z.string().optional(),
|
|
2016
2011
|
title: z.string().optional(),
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2012
|
+
folder_id: z.number().optional(),
|
|
2013
|
+
updated_at: z.string().optional(),
|
|
2014
|
+
exercise_count: z.number().int().nonnegative(),
|
|
2015
|
+
set_count: z.number().int().nonnegative()
|
|
2016
|
+
});
|
|
2017
|
+
const formattedWorkoutSummarySchema = z.object({
|
|
2018
|
+
id: z.string().optional(),
|
|
2019
|
+
title: z.string().optional(),
|
|
2020
|
+
start_time: z.string().optional(),
|
|
2021
|
+
end_time: z.string().optional(),
|
|
2022
|
+
duration: z.string(),
|
|
2023
|
+
exercise_count: z.number().int().nonnegative(),
|
|
2024
|
+
set_count: z.number().int().nonnegative()
|
|
2021
2025
|
});
|
|
2022
2026
|
const paginationOutputSchema = {
|
|
2023
2027
|
page: z.number().int().positive(),
|
|
2024
|
-
|
|
2025
|
-
|
|
2028
|
+
page_count: z.number().int().nonnegative().optional(),
|
|
2029
|
+
has_next_page: z.boolean().optional()
|
|
2026
2030
|
};
|
|
2027
2031
|
function normalizePaginatedInput(data) {
|
|
2028
2032
|
if (data === void 0) return {
|
|
@@ -2036,11 +2040,11 @@ function normalizePaginatedInput(data) {
|
|
|
2036
2040
|
};
|
|
2037
2041
|
}
|
|
2038
2042
|
const workoutsOutputSchema = {
|
|
2039
|
-
workouts: z.array(
|
|
2043
|
+
workouts: z.array(formattedWorkoutSummarySchema),
|
|
2040
2044
|
...paginationOutputSchema
|
|
2041
2045
|
};
|
|
2042
2046
|
const workoutOutputSchema = { workout: formattedWorkoutSchema.nullable() };
|
|
2043
|
-
const workoutCountOutputSchema = {
|
|
2047
|
+
const workoutCountOutputSchema = { workout_count: z.number().int() };
|
|
2044
2048
|
const formattedUpdatedWorkoutSchema = z.object({
|
|
2045
2049
|
type: z.literal("updated"),
|
|
2046
2050
|
workout: formattedWorkoutSchema
|
|
@@ -2048,70 +2052,70 @@ const formattedUpdatedWorkoutSchema = z.object({
|
|
|
2048
2052
|
const formattedDeletedWorkoutSchema = z.object({
|
|
2049
2053
|
type: z.literal("deleted"),
|
|
2050
2054
|
id: z.string(),
|
|
2051
|
-
|
|
2055
|
+
deleted_at: z.string().optional()
|
|
2052
2056
|
});
|
|
2053
2057
|
const workoutEventsOutputSchema = {
|
|
2054
2058
|
events: z.array(z.union([formattedUpdatedWorkoutSchema, formattedDeletedWorkoutSchema])),
|
|
2055
2059
|
...paginationOutputSchema
|
|
2056
2060
|
};
|
|
2057
2061
|
const routinesOutputSchema = {
|
|
2058
|
-
routines: z.array(
|
|
2062
|
+
routines: z.array(compactRoutineSchema),
|
|
2059
2063
|
...paginationOutputSchema
|
|
2060
2064
|
};
|
|
2061
2065
|
const routineOutputSchema = { routine: formattedRoutineSchema.nullable() };
|
|
2062
2066
|
const exerciseTemplatesOutputSchema = {
|
|
2063
|
-
|
|
2067
|
+
exercise_templates: z.array(formattedExerciseTemplateSchema),
|
|
2064
2068
|
...paginationOutputSchema
|
|
2065
2069
|
};
|
|
2066
|
-
const exerciseTemplateSearchOutputSchema = {
|
|
2067
|
-
const exerciseTemplateOutputSchema = {
|
|
2068
|
-
const exerciseHistoryOutputSchema = {
|
|
2070
|
+
const exerciseTemplateSearchOutputSchema = { exercise_templates: z.array(formattedExerciseTemplateSchema) };
|
|
2071
|
+
const exerciseTemplateOutputSchema = { exercise_template: formattedExerciseTemplateSchema.nullable() };
|
|
2072
|
+
const exerciseHistoryOutputSchema = { exercise_history: z.array(formattedExerciseHistoryEntrySchema) };
|
|
2069
2073
|
const routineFoldersOutputSchema = {
|
|
2070
|
-
|
|
2074
|
+
routine_folders: z.array(formattedRoutineFolderSchema),
|
|
2071
2075
|
...paginationOutputSchema
|
|
2072
2076
|
};
|
|
2073
|
-
const routineFolderOutputSchema = {
|
|
2077
|
+
const routineFolderOutputSchema = { routine_folder: formattedRoutineFolderSchema.nullable() };
|
|
2074
2078
|
const bodyMeasurementsOutputSchema = {
|
|
2075
|
-
|
|
2079
|
+
body_measurements: z.array(formattedBodyMeasurementSchema),
|
|
2076
2080
|
...paginationOutputSchema
|
|
2077
2081
|
};
|
|
2078
|
-
const bodyMeasurementOutputSchema = {
|
|
2082
|
+
const bodyMeasurementOutputSchema = { body_measurement: formattedBodyMeasurementSchema.nullable() };
|
|
2079
2083
|
const userOutputSchema = { user: userInfoSchema.nullable() };
|
|
2080
2084
|
const trainingSummaryOutputSchema = {
|
|
2081
2085
|
period: z.object({
|
|
2082
|
-
|
|
2083
|
-
|
|
2086
|
+
start_date: z.string(),
|
|
2087
|
+
end_date: z.string(),
|
|
2084
2088
|
weeks: z.number().int().positive()
|
|
2085
2089
|
}),
|
|
2086
2090
|
workouts: z.object({
|
|
2087
2091
|
count: z.number().int().nonnegative(),
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
+
total_duration_seconds: z.number().int().nonnegative(),
|
|
2093
|
+
exercise_count: z.number().int().nonnegative(),
|
|
2094
|
+
set_count: z.number().int().nonnegative(),
|
|
2095
|
+
unique_exercise_template_ids: z.array(z.string()),
|
|
2092
2096
|
sessions: z.array(trainingSummarySessionSchema)
|
|
2093
2097
|
}),
|
|
2094
|
-
|
|
2098
|
+
body_measurements: z.object({
|
|
2095
2099
|
count: z.number().int().nonnegative(),
|
|
2096
2100
|
latest: z.object({
|
|
2097
2101
|
date: z.string(),
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
}).
|
|
2102
|
+
weight_kg: optionalNumber,
|
|
2103
|
+
lean_mass_kg: optionalNumber,
|
|
2104
|
+
fat_percent: optionalNumber
|
|
2105
|
+
}).optional(),
|
|
2102
2106
|
earliest: z.object({
|
|
2103
2107
|
date: z.string(),
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
}).
|
|
2108
|
-
|
|
2108
|
+
weight_kg: optionalNumber,
|
|
2109
|
+
lean_mass_kg: optionalNumber,
|
|
2110
|
+
fat_percent: optionalNumber
|
|
2111
|
+
}).optional(),
|
|
2112
|
+
weight_change_kg: optionalNumber
|
|
2109
2113
|
}),
|
|
2110
|
-
|
|
2114
|
+
scan: scanSchema
|
|
2111
2115
|
};
|
|
2112
2116
|
const compactRoutinesOutputSchema = {
|
|
2113
2117
|
routines: z.array(compactRoutineSchema),
|
|
2114
|
-
|
|
2118
|
+
scan: scanSchema
|
|
2115
2119
|
};
|
|
2116
2120
|
function getSupersetId(exercise) {
|
|
2117
2121
|
if (exercise.superset_id !== void 0) return exercise.superset_id;
|
|
@@ -2119,99 +2123,104 @@ function getSupersetId(exercise) {
|
|
|
2119
2123
|
return null;
|
|
2120
2124
|
}
|
|
2121
2125
|
/**
|
|
2122
|
-
*
|
|
2123
|
-
*
|
|
2124
|
-
* @param workout - The workout object from the API
|
|
2125
|
-
* @returns A formatted workout object with standardized properties
|
|
2126
|
+
* Project a workout into the sparse snake_case MCP contract.
|
|
2126
2127
|
*/
|
|
2127
|
-
function
|
|
2128
|
+
function projectWorkout(workout) {
|
|
2128
2129
|
return {
|
|
2129
|
-
id: workout.id,
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2130
|
+
...workout.id == null ? {} : { id: workout.id },
|
|
2131
|
+
...workout.routine_id == null ? {} : { routine_id: workout.routine_id },
|
|
2132
|
+
...workout.title == null ? {} : { title: workout.title },
|
|
2133
|
+
...workout.description == null ? {} : { description: workout.description },
|
|
2134
|
+
...workout.start_time == null ? {} : { start_time: workout.start_time },
|
|
2135
|
+
...workout.end_time == null ? {} : { end_time: workout.end_time },
|
|
2136
|
+
...workout.created_at == null ? {} : { created_at: workout.created_at },
|
|
2137
|
+
...workout.updated_at == null ? {} : { updated_at: workout.updated_at },
|
|
2136
2138
|
duration: calculateDuration(workout.start_time, workout.end_time),
|
|
2137
|
-
exercises: workout.exercises
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
};
|
|
2155
|
-
})
|
|
2139
|
+
...workout.exercises ? { exercises: workout.exercises.map((exercise) => ({
|
|
2140
|
+
...exercise.index === void 0 ? {} : { index: exercise.index },
|
|
2141
|
+
...exercise.title == null ? {} : { title: exercise.title },
|
|
2142
|
+
...exercise.exercise_template_id == null ? {} : { exercise_template_id: exercise.exercise_template_id },
|
|
2143
|
+
...exercise.notes == null ? {} : { notes: exercise.notes },
|
|
2144
|
+
...getSupersetId(exercise) == null ? {} : { supersets_id: getSupersetId(exercise) ?? void 0 },
|
|
2145
|
+
...exercise.sets ? { sets: exercise.sets.map((set) => ({
|
|
2146
|
+
...set.index === void 0 ? {} : { index: set.index },
|
|
2147
|
+
...set.type == null ? {} : { type: set.type },
|
|
2148
|
+
...set.weight_kg == null ? {} : { weight_kg: set.weight_kg },
|
|
2149
|
+
...set.reps == null ? {} : { reps: set.reps },
|
|
2150
|
+
...set.distance_meters == null ? {} : { distance_meters: set.distance_meters },
|
|
2151
|
+
...set.duration_seconds == null ? {} : { duration_seconds: set.duration_seconds },
|
|
2152
|
+
...set.rpe == null ? {} : { rpe: set.rpe },
|
|
2153
|
+
...set.custom_metric == null ? {} : { custom_metric: set.custom_metric }
|
|
2154
|
+
})) } : {}
|
|
2155
|
+
})) } : {}
|
|
2156
2156
|
};
|
|
2157
2157
|
}
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
*
|
|
2161
|
-
* @param routine - The routine object from the API
|
|
2162
|
-
* @returns A formatted routine object with standardized properties
|
|
2163
|
-
*/
|
|
2164
|
-
function formatRoutine(routine) {
|
|
2158
|
+
function summarizeWorkout(workout) {
|
|
2159
|
+
const exercises = workout.exercises ?? [];
|
|
2165
2160
|
return {
|
|
2166
|
-
id:
|
|
2167
|
-
title:
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
name: exercise.title,
|
|
2174
|
-
index: exercise.index,
|
|
2175
|
-
exerciseTemplateId: exercise.exercise_template_id,
|
|
2176
|
-
notes: exercise.notes,
|
|
2177
|
-
supersetId: getSupersetId(exercise),
|
|
2178
|
-
restSeconds: exercise.rest_seconds,
|
|
2179
|
-
sets: exercise.sets?.map((set) => ({
|
|
2180
|
-
index: set.index,
|
|
2181
|
-
type: set.type,
|
|
2182
|
-
weight: set.weight_kg,
|
|
2183
|
-
reps: set.reps,
|
|
2184
|
-
...set.rep_range !== void 0 && { repRange: set.rep_range },
|
|
2185
|
-
distance: set.distance_meters,
|
|
2186
|
-
duration: set.duration_seconds,
|
|
2187
|
-
...set.rpe !== void 0 && { rpe: set.rpe },
|
|
2188
|
-
customMetric: set.custom_metric
|
|
2189
|
-
}))
|
|
2190
|
-
};
|
|
2191
|
-
})
|
|
2161
|
+
...workout.id == null ? {} : { id: workout.id },
|
|
2162
|
+
...workout.title == null ? {} : { title: workout.title },
|
|
2163
|
+
...workout.start_time == null ? {} : { start_time: workout.start_time },
|
|
2164
|
+
...workout.end_time == null ? {} : { end_time: workout.end_time },
|
|
2165
|
+
duration: calculateDuration(workout.start_time, workout.end_time),
|
|
2166
|
+
exercise_count: exercises.length,
|
|
2167
|
+
set_count: exercises.reduce((total, exercise) => total + (exercise.sets?.length ?? 0), 0)
|
|
2192
2168
|
};
|
|
2193
2169
|
}
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2170
|
+
function projectRoutine(routine) {
|
|
2171
|
+
return {
|
|
2172
|
+
...routine.id == null ? {} : { id: routine.id },
|
|
2173
|
+
...routine.title == null ? {} : { title: routine.title },
|
|
2174
|
+
...routine.folder_id == null ? {} : { folder_id: routine.folder_id },
|
|
2175
|
+
...routine.created_at == null ? {} : { created_at: routine.created_at },
|
|
2176
|
+
...routine.updated_at == null ? {} : { updated_at: routine.updated_at },
|
|
2177
|
+
...routine.exercises ? { exercises: routine.exercises.map((exercise) => ({
|
|
2178
|
+
...exercise.title == null ? {} : { title: exercise.title },
|
|
2179
|
+
...exercise.index === void 0 ? {} : { index: exercise.index },
|
|
2180
|
+
...exercise.exercise_template_id == null ? {} : { exercise_template_id: exercise.exercise_template_id },
|
|
2181
|
+
...exercise.notes == null ? {} : { notes: exercise.notes },
|
|
2182
|
+
...getSupersetId(exercise) == null ? {} : { supersets_id: getSupersetId(exercise) ?? void 0 },
|
|
2183
|
+
...exercise.rest_seconds == null ? {} : { rest_seconds: exercise.rest_seconds },
|
|
2184
|
+
...exercise.sets ? { sets: exercise.sets.map((set) => {
|
|
2185
|
+
const repRange = set.rep_range;
|
|
2186
|
+
const normalizedRepRange = repRange && (repRange.start != null || repRange.end != null) ? {
|
|
2187
|
+
...repRange.start == null ? {} : { start: repRange.start },
|
|
2188
|
+
...repRange.end == null ? {} : { end: repRange.end }
|
|
2189
|
+
} : void 0;
|
|
2190
|
+
return {
|
|
2191
|
+
...set.index === void 0 ? {} : { index: set.index },
|
|
2192
|
+
...set.type == null ? {} : { type: set.type },
|
|
2193
|
+
...set.weight_kg == null ? {} : { weight_kg: set.weight_kg },
|
|
2194
|
+
...set.reps == null ? {} : { reps: set.reps },
|
|
2195
|
+
...normalizedRepRange ? { rep_range: normalizedRepRange } : {},
|
|
2196
|
+
...set.distance_meters == null ? {} : { distance_meters: set.distance_meters },
|
|
2197
|
+
...set.duration_seconds == null ? {} : { duration_seconds: set.duration_seconds },
|
|
2198
|
+
...set.rpe == null ? {} : { rpe: set.rpe },
|
|
2199
|
+
...set.custom_metric == null ? {} : { custom_metric: set.custom_metric }
|
|
2200
|
+
};
|
|
2201
|
+
}) } : {}
|
|
2202
|
+
})) } : {}
|
|
2203
|
+
};
|
|
2204
|
+
}
|
|
2205
|
+
function summarizeRoutine(routine) {
|
|
2206
|
+
const exercises = routine.exercises ?? [];
|
|
2207
|
+
return {
|
|
2208
|
+
...routine.id == null ? {} : { id: routine.id },
|
|
2209
|
+
...routine.title == null ? {} : { title: routine.title },
|
|
2210
|
+
...routine.folder_id == null ? {} : { folder_id: routine.folder_id },
|
|
2211
|
+
...routine.updated_at == null ? { updated_at: void 0 } : { updated_at: routine.updated_at },
|
|
2212
|
+
exercise_count: exercises.length,
|
|
2213
|
+
set_count: exercises.reduce((total, exercise) => total + (exercise.sets?.length ?? 0), 0)
|
|
2214
|
+
};
|
|
2215
|
+
}
|
|
2216
|
+
function projectRoutineFolder(folder) {
|
|
2201
2217
|
return {
|
|
2202
2218
|
id: folder.id,
|
|
2203
2219
|
title: folder.title,
|
|
2204
|
-
|
|
2205
|
-
|
|
2220
|
+
created_at: folder.created_at,
|
|
2221
|
+
updated_at: folder.updated_at
|
|
2206
2222
|
};
|
|
2207
2223
|
}
|
|
2208
|
-
/**
|
|
2209
|
-
* Calculate duration between two ISO timestamp strings
|
|
2210
|
-
*
|
|
2211
|
-
* @param startTime - The start time as ISO string or timestamp
|
|
2212
|
-
* @param endTime - The end time as ISO string or timestamp
|
|
2213
|
-
* @returns A formatted duration string (e.g. "1h 30m 45s") or "Unknown duration" if inputs are invalid
|
|
2214
|
-
*/
|
|
2215
2224
|
function calculateDuration(startTime, endTime) {
|
|
2216
2225
|
if (!startTime || !endTime) return "Unknown duration";
|
|
2217
2226
|
try {
|
|
@@ -2226,69 +2235,53 @@ function calculateDuration(startTime, endTime) {
|
|
|
2226
2235
|
return "Unknown duration";
|
|
2227
2236
|
}
|
|
2228
2237
|
}
|
|
2229
|
-
|
|
2230
|
-
* Format an exercise template object for consistent presentation
|
|
2231
|
-
*
|
|
2232
|
-
* @param template - The exercise template object from the API
|
|
2233
|
-
* @returns A formatted exercise template object with standardized properties
|
|
2234
|
-
*/
|
|
2235
|
-
function formatExerciseTemplate(template) {
|
|
2236
|
-
return {
|
|
2237
|
-
id: template.id,
|
|
2238
|
-
title: template.title,
|
|
2239
|
-
type: template.type,
|
|
2240
|
-
primaryMuscleGroup: template.primary_muscle_group,
|
|
2241
|
-
secondaryMuscleGroups: template.secondary_muscle_groups,
|
|
2242
|
-
isCustom: template.is_custom
|
|
2243
|
-
};
|
|
2244
|
-
}
|
|
2245
|
-
function formatExerciseHistoryEntry(entry) {
|
|
2238
|
+
function normalizeExerciseHistoryEntry(entry) {
|
|
2246
2239
|
return {
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
reps: entry.reps,
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
rpe: entry.rpe,
|
|
2257
|
-
|
|
2258
|
-
|
|
2240
|
+
...entry.workout_id == null ? {} : { workout_id: entry.workout_id },
|
|
2241
|
+
...entry.workout_title == null ? {} : { workout_title: entry.workout_title },
|
|
2242
|
+
...entry.workout_start_time == null ? {} : { workout_start_time: entry.workout_start_time },
|
|
2243
|
+
...entry.workout_end_time == null ? {} : { workout_end_time: entry.workout_end_time },
|
|
2244
|
+
...entry.exercise_template_id == null ? {} : { exercise_template_id: entry.exercise_template_id },
|
|
2245
|
+
...entry.weight_kg == null ? {} : { weight_kg: entry.weight_kg },
|
|
2246
|
+
...entry.reps == null ? {} : { reps: entry.reps },
|
|
2247
|
+
...entry.distance_meters == null ? {} : { distance_meters: entry.distance_meters },
|
|
2248
|
+
...entry.duration_seconds == null ? {} : { duration_seconds: entry.duration_seconds },
|
|
2249
|
+
...entry.rpe == null ? {} : { rpe: entry.rpe },
|
|
2250
|
+
...entry.custom_metric == null ? {} : { custom_metric: entry.custom_metric },
|
|
2251
|
+
...entry.set_type == null ? {} : { set_type: entry.set_type }
|
|
2259
2252
|
};
|
|
2260
2253
|
}
|
|
2261
|
-
function
|
|
2254
|
+
function normalizeBodyMeasurement(measurement) {
|
|
2262
2255
|
return {
|
|
2263
2256
|
date: measurement.date,
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
abdomen: measurement.abdomen
|
|
2275
|
-
waist: measurement.waist
|
|
2276
|
-
hips: measurement.hips
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2257
|
+
...measurement.weight_kg == null ? {} : { weight_kg: measurement.weight_kg },
|
|
2258
|
+
...measurement.lean_mass_kg == null ? {} : { lean_mass_kg: measurement.lean_mass_kg },
|
|
2259
|
+
...measurement.fat_percent == null ? {} : { fat_percent: measurement.fat_percent },
|
|
2260
|
+
...measurement.neck_cm == null ? {} : { neck_cm: measurement.neck_cm },
|
|
2261
|
+
...measurement.shoulder_cm == null ? {} : { shoulder_cm: measurement.shoulder_cm },
|
|
2262
|
+
...measurement.chest_cm == null ? {} : { chest_cm: measurement.chest_cm },
|
|
2263
|
+
...measurement.left_bicep_cm == null ? {} : { left_bicep_cm: measurement.left_bicep_cm },
|
|
2264
|
+
...measurement.right_bicep_cm == null ? {} : { right_bicep_cm: measurement.right_bicep_cm },
|
|
2265
|
+
...measurement.left_forearm_cm == null ? {} : { left_forearm_cm: measurement.left_forearm_cm },
|
|
2266
|
+
...measurement.right_forearm_cm == null ? {} : { right_forearm_cm: measurement.right_forearm_cm },
|
|
2267
|
+
...measurement.abdomen == null ? {} : { abdomen: measurement.abdomen },
|
|
2268
|
+
...measurement.waist == null ? {} : { waist: measurement.waist },
|
|
2269
|
+
...measurement.hips == null ? {} : { hips: measurement.hips },
|
|
2270
|
+
...measurement.left_thigh == null ? {} : { left_thigh_cm: measurement.left_thigh },
|
|
2271
|
+
...measurement.right_thigh == null ? {} : { right_thigh_cm: measurement.right_thigh },
|
|
2272
|
+
...measurement.left_calf == null ? {} : { left_calf_cm: measurement.left_calf },
|
|
2273
|
+
...measurement.right_calf == null ? {} : { right_calf_cm: measurement.right_calf }
|
|
2281
2274
|
};
|
|
2282
2275
|
}
|
|
2283
|
-
function
|
|
2276
|
+
function projectWorkoutEvent(event) {
|
|
2284
2277
|
if (event.type === "updated" && "workout" in event) return {
|
|
2285
2278
|
type: "updated",
|
|
2286
|
-
workout:
|
|
2279
|
+
workout: projectWorkout(event.workout)
|
|
2287
2280
|
};
|
|
2288
2281
|
if (event.type === "deleted" && "id" in event) return {
|
|
2289
2282
|
type: "deleted",
|
|
2290
2283
|
id: event.id,
|
|
2291
|
-
|
|
2284
|
+
deleted_at: event.deleted_at
|
|
2292
2285
|
};
|
|
2293
2286
|
throw new Error(`Unsupported workout event type: ${event.type}`);
|
|
2294
2287
|
}
|
|
@@ -2332,10 +2325,10 @@ const workoutsResponse = defineStructuredResponseContract({
|
|
|
2332
2325
|
normalize: (input) => {
|
|
2333
2326
|
const data = normalizePaginatedInput(input);
|
|
2334
2327
|
return {
|
|
2335
|
-
workouts: data.items.map(
|
|
2328
|
+
workouts: data.items.map(summarizeWorkout),
|
|
2336
2329
|
page: data.page,
|
|
2337
|
-
|
|
2338
|
-
|
|
2330
|
+
page_count: data.pageCount,
|
|
2331
|
+
has_next_page: data.pageCount === void 0 ? void 0 : data.page < data.pageCount
|
|
2339
2332
|
};
|
|
2340
2333
|
},
|
|
2341
2334
|
legacyJson: ({ workouts }) => workouts,
|
|
@@ -2347,9 +2340,9 @@ const workoutsResponse = defineStructuredResponseContract({
|
|
|
2347
2340
|
});
|
|
2348
2341
|
const workoutResponse = defineStructuredResponseContract({
|
|
2349
2342
|
outputSchema: workoutOutputSchema,
|
|
2350
|
-
normalize: (data) => ({ workout: data.workout ?
|
|
2343
|
+
normalize: (data) => ({ workout: data.workout ? projectWorkout(data.workout) : null }),
|
|
2351
2344
|
legacyJson: ({ workout }) => workout,
|
|
2352
|
-
text: ({
|
|
2345
|
+
text: ({ workout_id }, { workout }) => workout === null ? `Workout with ID ${workout_id} not found` : void 0,
|
|
2353
2346
|
telemetry: ({ workout, expected404Outcome }) => ({
|
|
2354
2347
|
...workoutResultTelemetry(workout),
|
|
2355
2348
|
expected404Outcome
|
|
@@ -2357,17 +2350,17 @@ const workoutResponse = defineStructuredResponseContract({
|
|
|
2357
2350
|
});
|
|
2358
2351
|
const workoutCountResponse = defineStructuredResponseContract({
|
|
2359
2352
|
outputSchema: workoutCountOutputSchema,
|
|
2360
|
-
normalize: (count) => ({ count }),
|
|
2353
|
+
normalize: (count) => ({ workout_count: count }),
|
|
2361
2354
|
legacyJson: (output) => output,
|
|
2362
2355
|
telemetry: (count) => ({ itemCountBucket: bucketCount(count) })
|
|
2363
2356
|
});
|
|
2364
2357
|
const workoutEventsResponse = defineStructuredResponseContract({
|
|
2365
2358
|
outputSchema: workoutEventsOutputSchema,
|
|
2366
2359
|
normalize: (data) => ({
|
|
2367
|
-
events: data.events?.map(
|
|
2360
|
+
events: data.events?.map(projectWorkoutEvent) ?? [],
|
|
2368
2361
|
page: data.page,
|
|
2369
|
-
|
|
2370
|
-
|
|
2362
|
+
page_count: data.pageCount,
|
|
2363
|
+
has_next_page: data.pageCount === void 0 ? void 0 : data.page < data.pageCount
|
|
2371
2364
|
}),
|
|
2372
2365
|
legacyJson: ({ events }) => events,
|
|
2373
2366
|
text: ({ since }, { events }) => events.length === 0 ? `No workout events found for the specified parameters since ${since}` : void 0,
|
|
@@ -2381,10 +2374,10 @@ const routinesResponse = defineStructuredResponseContract({
|
|
|
2381
2374
|
normalize: (input) => {
|
|
2382
2375
|
const data = normalizePaginatedInput(input);
|
|
2383
2376
|
return {
|
|
2384
|
-
routines: data.items.map(
|
|
2377
|
+
routines: data.items.map(summarizeRoutine),
|
|
2385
2378
|
page: data.page,
|
|
2386
|
-
|
|
2387
|
-
|
|
2379
|
+
page_count: data.pageCount,
|
|
2380
|
+
has_next_page: data.pageCount === void 0 ? void 0 : data.page < data.pageCount
|
|
2388
2381
|
};
|
|
2389
2382
|
},
|
|
2390
2383
|
legacyJson: ({ routines }) => routines,
|
|
@@ -2396,9 +2389,9 @@ const routinesResponse = defineStructuredResponseContract({
|
|
|
2396
2389
|
});
|
|
2397
2390
|
const routineResponse = defineStructuredResponseContract({
|
|
2398
2391
|
outputSchema: routineOutputSchema,
|
|
2399
|
-
normalize: (data) => ({ routine: data.routine ?
|
|
2392
|
+
normalize: (data) => ({ routine: data.routine ? projectRoutine(data.routine) : null }),
|
|
2400
2393
|
legacyJson: ({ routine }) => routine,
|
|
2401
|
-
text: ({
|
|
2394
|
+
text: ({ routine_id }, { routine }) => routine === null ? `Routine with ID ${routine_id} not found` : void 0,
|
|
2402
2395
|
telemetry: ({ routine, expected404Outcome }) => ({
|
|
2403
2396
|
...routineResultTelemetry(routine),
|
|
2404
2397
|
expected404Outcome
|
|
@@ -2409,14 +2402,14 @@ const exerciseTemplatesResponse = defineStructuredResponseContract({
|
|
|
2409
2402
|
normalize: (input) => {
|
|
2410
2403
|
const data = normalizePaginatedInput(input);
|
|
2411
2404
|
return {
|
|
2412
|
-
|
|
2405
|
+
exercise_templates: data.items,
|
|
2413
2406
|
page: data.page,
|
|
2414
|
-
|
|
2415
|
-
|
|
2407
|
+
page_count: data.pageCount,
|
|
2408
|
+
has_next_page: data.pageCount === void 0 ? void 0 : data.page < data.pageCount
|
|
2416
2409
|
};
|
|
2417
2410
|
},
|
|
2418
|
-
legacyJson: ({
|
|
2419
|
-
text: (_data, {
|
|
2411
|
+
legacyJson: ({ exercise_templates }) => exercise_templates,
|
|
2412
|
+
text: (_data, { exercise_templates }) => exercise_templates.length === 0 ? "No exercise templates found for the specified parameters" : void 0,
|
|
2420
2413
|
telemetry: (templates) => ({
|
|
2421
2414
|
itemCountBucket: bucketCount(normalizePaginatedInput(templates).items.length),
|
|
2422
2415
|
expected404Outcome: normalizePaginatedInput(templates).expected404Outcome
|
|
@@ -2424,26 +2417,26 @@ const exerciseTemplatesResponse = defineStructuredResponseContract({
|
|
|
2424
2417
|
});
|
|
2425
2418
|
const exerciseTemplateResponse = defineStructuredResponseContract({
|
|
2426
2419
|
outputSchema: exerciseTemplateOutputSchema,
|
|
2427
|
-
normalize: (data) => ({
|
|
2428
|
-
legacyJson: ({
|
|
2429
|
-
text: ({
|
|
2430
|
-
telemetry: ({
|
|
2431
|
-
itemCountBucket: bucketCount(
|
|
2420
|
+
normalize: (data) => ({ exercise_template: data.exercise_template ?? null }),
|
|
2421
|
+
legacyJson: ({ exercise_template }) => exercise_template,
|
|
2422
|
+
text: ({ exercise_template_id }, { exercise_template }) => exercise_template === null ? `Exercise template with ID ${exercise_template_id} not found` : void 0,
|
|
2423
|
+
telemetry: ({ exercise_template, expected404Outcome }) => ({
|
|
2424
|
+
itemCountBucket: bucketCount(exercise_template ? 1 : 0),
|
|
2432
2425
|
expected404Outcome
|
|
2433
2426
|
})
|
|
2434
2427
|
});
|
|
2435
2428
|
const exerciseHistoryResponse = defineStructuredResponseContract({
|
|
2436
2429
|
outputSchema: exerciseHistoryOutputSchema,
|
|
2437
|
-
normalize: (data) => ({
|
|
2438
|
-
legacyJson: ({
|
|
2439
|
-
text: ({
|
|
2440
|
-
telemetry: (data) => ({ itemCountBucket: bucketCount(data.
|
|
2430
|
+
normalize: (data) => ({ exercise_history: data.exercise_history?.map(normalizeExerciseHistoryEntry) ?? [] }),
|
|
2431
|
+
legacyJson: ({ exercise_history }) => exercise_history,
|
|
2432
|
+
text: ({ exercise_template_id }, { exercise_history }) => exercise_history.length === 0 ? `No exercise history found for template ${exercise_template_id}` : void 0,
|
|
2433
|
+
telemetry: (data) => ({ itemCountBucket: bucketCount(data.exercise_history?.length ?? 0) })
|
|
2441
2434
|
});
|
|
2442
2435
|
const searchExerciseTemplatesResponse = defineStructuredResponseContract({
|
|
2443
2436
|
outputSchema: exerciseTemplateSearchOutputSchema,
|
|
2444
|
-
normalize: (data) => ({
|
|
2445
|
-
legacyJson: ({
|
|
2446
|
-
text: ({ query,
|
|
2437
|
+
normalize: (data) => ({ exercise_templates: data.results }),
|
|
2438
|
+
legacyJson: ({ exercise_templates }) => exercise_templates,
|
|
2439
|
+
text: ({ query, primary_muscle_group }, { exercise_templates }) => exercise_templates.length === 0 ? `No exercise templates found matching "${query}"${primary_muscle_group ? ` with primary muscle group "${primary_muscle_group}"` : ""}` : void 0,
|
|
2447
2440
|
telemetry: (data) => ({ itemCountBucket: bucketCount(data.results.length) })
|
|
2448
2441
|
});
|
|
2449
2442
|
const routineFoldersResponse = defineStructuredResponseContract({
|
|
@@ -2451,14 +2444,14 @@ const routineFoldersResponse = defineStructuredResponseContract({
|
|
|
2451
2444
|
normalize: (input) => {
|
|
2452
2445
|
const data = normalizePaginatedInput(input);
|
|
2453
2446
|
return {
|
|
2454
|
-
|
|
2447
|
+
routine_folders: data.items.map(projectRoutineFolder),
|
|
2455
2448
|
page: data.page,
|
|
2456
|
-
|
|
2457
|
-
|
|
2449
|
+
page_count: data.pageCount,
|
|
2450
|
+
has_next_page: data.pageCount === void 0 ? void 0 : data.page < data.pageCount
|
|
2458
2451
|
};
|
|
2459
2452
|
},
|
|
2460
|
-
legacyJson: ({
|
|
2461
|
-
text: (_data, {
|
|
2453
|
+
legacyJson: ({ routine_folders }) => routine_folders,
|
|
2454
|
+
text: (_data, { routine_folders }) => routine_folders.length === 0 ? "No routine folders found for the specified parameters" : void 0,
|
|
2462
2455
|
telemetry: (folders) => ({
|
|
2463
2456
|
itemCountBucket: bucketCount(normalizePaginatedInput(folders).items.length),
|
|
2464
2457
|
expected404Outcome: normalizePaginatedInput(folders).expected404Outcome
|
|
@@ -2466,11 +2459,11 @@ const routineFoldersResponse = defineStructuredResponseContract({
|
|
|
2466
2459
|
});
|
|
2467
2460
|
const routineFolderResponse = defineStructuredResponseContract({
|
|
2468
2461
|
outputSchema: routineFolderOutputSchema,
|
|
2469
|
-
normalize: (data) => ({
|
|
2470
|
-
legacyJson: ({
|
|
2471
|
-
text: ({
|
|
2472
|
-
telemetry: ({
|
|
2473
|
-
itemCountBucket: bucketCount(
|
|
2462
|
+
normalize: (data) => ({ routine_folder: data.routine_folder ? projectRoutineFolder(data.routine_folder) : null }),
|
|
2463
|
+
legacyJson: ({ routine_folder }) => routine_folder,
|
|
2464
|
+
text: ({ folder_id }, { routine_folder }) => routine_folder === null ? `Routine folder with ID ${folder_id} not found` : void 0,
|
|
2465
|
+
telemetry: ({ routine_folder, expected404Outcome }) => ({
|
|
2466
|
+
itemCountBucket: bucketCount(routine_folder ? 1 : 0),
|
|
2474
2467
|
expected404Outcome
|
|
2475
2468
|
})
|
|
2476
2469
|
});
|
|
@@ -2479,14 +2472,14 @@ const bodyMeasurementsResponse = defineStructuredResponseContract({
|
|
|
2479
2472
|
normalize: (input) => {
|
|
2480
2473
|
const data = normalizePaginatedInput(input);
|
|
2481
2474
|
return {
|
|
2482
|
-
|
|
2475
|
+
body_measurements: data.items.map(normalizeBodyMeasurement),
|
|
2483
2476
|
page: data.page,
|
|
2484
|
-
|
|
2485
|
-
|
|
2477
|
+
page_count: data.pageCount,
|
|
2478
|
+
has_next_page: data.pageCount === void 0 ? void 0 : data.page < data.pageCount
|
|
2486
2479
|
};
|
|
2487
2480
|
},
|
|
2488
|
-
legacyJson: ({
|
|
2489
|
-
text: (_data, {
|
|
2481
|
+
legacyJson: ({ body_measurements }) => body_measurements,
|
|
2482
|
+
text: (_data, { body_measurements }) => body_measurements.length === 0 ? "No body measurements found for the specified parameters" : void 0,
|
|
2490
2483
|
telemetry: (measurements) => ({
|
|
2491
2484
|
itemCountBucket: bucketCount(normalizePaginatedInput(measurements).items.length),
|
|
2492
2485
|
expected404Outcome: normalizePaginatedInput(measurements).expected404Outcome
|
|
@@ -2494,11 +2487,11 @@ const bodyMeasurementsResponse = defineStructuredResponseContract({
|
|
|
2494
2487
|
});
|
|
2495
2488
|
const bodyMeasurementResponse = defineStructuredResponseContract({
|
|
2496
2489
|
outputSchema: bodyMeasurementOutputSchema,
|
|
2497
|
-
normalize: (data) => ({
|
|
2498
|
-
legacyJson: ({
|
|
2499
|
-
text: ({ date }, {
|
|
2500
|
-
telemetry: ({
|
|
2501
|
-
itemCountBucket: bucketCount(
|
|
2490
|
+
normalize: (data) => ({ body_measurement: data.body_measurement ? normalizeBodyMeasurement(data.body_measurement) : null }),
|
|
2491
|
+
legacyJson: ({ body_measurement }) => body_measurement,
|
|
2492
|
+
text: ({ date }, { body_measurement }) => body_measurement === null ? `No body measurement found for date ${date}` : void 0,
|
|
2493
|
+
telemetry: ({ body_measurement, expected404Outcome }) => ({
|
|
2494
|
+
itemCountBucket: bucketCount(body_measurement ? 1 : 0),
|
|
2502
2495
|
expected404Outcome
|
|
2503
2496
|
})
|
|
2504
2497
|
});
|
|
@@ -2511,17 +2504,46 @@ const userResponse = defineStructuredResponseContract({
|
|
|
2511
2504
|
});
|
|
2512
2505
|
const trainingSummaryResponse = defineStructuredResponseContract({
|
|
2513
2506
|
outputSchema: trainingSummaryOutputSchema,
|
|
2514
|
-
normalize: (data) =>
|
|
2507
|
+
normalize: (data) => {
|
|
2508
|
+
const { workflow, ...result } = data;
|
|
2509
|
+
const { latest, earliest, weight_change_kg, ...body_measurements } = result.body_measurements;
|
|
2510
|
+
return {
|
|
2511
|
+
...result,
|
|
2512
|
+
body_measurements: {
|
|
2513
|
+
...body_measurements,
|
|
2514
|
+
...latest ? { latest } : {},
|
|
2515
|
+
...earliest ? { earliest } : {},
|
|
2516
|
+
...weight_change_kg == null ? {} : { weight_change_kg }
|
|
2517
|
+
},
|
|
2518
|
+
scan: {
|
|
2519
|
+
pages: workflow.pagination,
|
|
2520
|
+
items: workflow.itemsScanned
|
|
2521
|
+
}
|
|
2522
|
+
};
|
|
2523
|
+
},
|
|
2515
2524
|
legacyJson: (output) => output,
|
|
2516
|
-
text: (data) => data.workouts.count === 0 && data.
|
|
2525
|
+
text: (data) => data.workouts.count === 0 && data.body_measurements.count === 0 ? "No workouts or body measurements found for the specified period" : void 0,
|
|
2517
2526
|
telemetry: (data) => ({
|
|
2518
|
-
itemCountBucket: bucketCount(data.workouts.count + data.
|
|
2527
|
+
itemCountBucket: bucketCount(data.workouts.count + data.body_measurements.count),
|
|
2519
2528
|
workflow: workflowResultTelemetry(data.workflow)
|
|
2520
2529
|
})
|
|
2521
2530
|
});
|
|
2522
2531
|
const compactRoutinesResponse = defineStructuredResponseContract({
|
|
2523
2532
|
outputSchema: compactRoutinesOutputSchema,
|
|
2524
|
-
normalize: (data) =>
|
|
2533
|
+
normalize: (data) => {
|
|
2534
|
+
const { workflow, ...result } = data;
|
|
2535
|
+
return {
|
|
2536
|
+
...result,
|
|
2537
|
+
routines: result.routines.map((routine) => ({
|
|
2538
|
+
...routine,
|
|
2539
|
+
...routine.folder_id == null ? { folder_id: void 0 } : {}
|
|
2540
|
+
})),
|
|
2541
|
+
scan: {
|
|
2542
|
+
pages: workflow.pagination,
|
|
2543
|
+
items: workflow.itemsScanned
|
|
2544
|
+
}
|
|
2545
|
+
};
|
|
2546
|
+
},
|
|
2525
2547
|
legacyJson: ({ routines }) => routines,
|
|
2526
2548
|
text: (_data, { routines }) => routines.length === 0 ? "No routines found matching the query" : void 0,
|
|
2527
2549
|
telemetry: (data) => ({
|
|
@@ -2529,22 +2551,22 @@ const compactRoutinesResponse = defineStructuredResponseContract({
|
|
|
2529
2551
|
workflow: workflowResultTelemetry(data.workflow)
|
|
2530
2552
|
})
|
|
2531
2553
|
});
|
|
2532
|
-
const createWorkoutResponse = defineJsonResponseContract((workout) => workout ? { json:
|
|
2533
|
-
const updateWorkoutResponse = defineJsonResponseContract((data) => data.workout ? { json:
|
|
2554
|
+
const createWorkoutResponse = defineJsonResponseContract((workout) => workout ? { json: projectWorkout(workout) } : { text: "Failed to create workout: Server returned no data" }, (workout) => workoutResultTelemetry(workout));
|
|
2555
|
+
const updateWorkoutResponse = defineJsonResponseContract((data) => data.workout ? { json: projectWorkout(data.workout) } : { text: `Failed to update workout with ID ${data.workout_id}` }, (data) => workoutResultTelemetry(data.workout));
|
|
2534
2556
|
const repRangeDisplayWarningText = "Note: Hevy's public API stores rep ranges (rep_range), but the Hevy apps may not display them because they rely on an internal-only exercise field (input_modifier). See https://github.com/chrisdoc/hevy-mcp/issues/261 for details/workarounds.";
|
|
2535
2557
|
const createRoutineResponse = defineJsonResponseContract((data) => data.routine ? {
|
|
2536
|
-
json:
|
|
2558
|
+
json: projectRoutine(data.routine),
|
|
2537
2559
|
additionalText: data.usesRepRanges ? [repRangeDisplayWarningText] : []
|
|
2538
2560
|
} : { text: "Failed to create routine: Server returned no data" }, (data) => routineResultTelemetry(data.routine));
|
|
2539
2561
|
const updateRoutineResponse = defineJsonResponseContract((data) => data.routine ? {
|
|
2540
|
-
json:
|
|
2562
|
+
json: projectRoutine(data.routine),
|
|
2541
2563
|
additionalText: data.usesRepRanges ? [repRangeDisplayWarningText] : []
|
|
2542
|
-
} : { text: `Failed to update routine with ID ${data.
|
|
2564
|
+
} : { text: `Failed to update routine with ID ${data.routine_id}` }, (data) => routineResultTelemetry(data.routine));
|
|
2543
2565
|
const createExerciseTemplateResponse = defineJsonResponseContract((response) => ({ json: {
|
|
2544
2566
|
id: response?.id,
|
|
2545
2567
|
message: "Exercise template created successfully"
|
|
2546
2568
|
} }));
|
|
2547
|
-
const createRoutineFolderResponse = defineJsonResponseContract((folder) => folder ? { json:
|
|
2569
|
+
const createRoutineFolderResponse = defineJsonResponseContract((folder) => folder ? { json: projectRoutineFolder(folder) } : { text: "Failed to create routine folder: Server returned no data" }, (folder) => ({ itemCountBucket: bucketCount(folder ? 1 : 0) }));
|
|
2548
2570
|
const createBodyMeasurementResponse = defineJsonResponseContract((date) => ({ text: `Body measurement for ${date} created successfully.` }), () => ({ itemCountBucket: "1" }));
|
|
2549
2571
|
const updateBodyMeasurementResponse = defineJsonResponseContract((date) => ({ text: `Body measurement for ${date} updated successfully.` }), () => ({ itemCountBucket: "1" }));
|
|
2550
2572
|
//#endregion
|
|
@@ -2581,33 +2603,103 @@ function registerHevyResources(server, runtime) {
|
|
|
2581
2603
|
description: "Total number of workouts in the Hevy account",
|
|
2582
2604
|
mimeType: JSON_MIME_TYPE
|
|
2583
2605
|
}, async (uri) => {
|
|
2584
|
-
return createJsonResourceResult(uri, {
|
|
2606
|
+
return createJsonResourceResult(uri, { workout_count: (await runtime.getClient().getWorkoutCount())?.workout_count ?? 0 });
|
|
2585
2607
|
});
|
|
2586
2608
|
server.registerResource("exercise-templates", "hevy://exercise-templates", {
|
|
2587
2609
|
description: "Full formatted Hevy exercise template catalog",
|
|
2588
2610
|
mimeType: JSON_MIME_TYPE
|
|
2589
2611
|
}, async (uri) => {
|
|
2590
|
-
return createJsonResourceResult(uri,
|
|
2612
|
+
return createJsonResourceResult(uri, await runtime.catalog.get());
|
|
2591
2613
|
});
|
|
2592
2614
|
server.registerResource("routine-folders", "hevy://routine-folders", {
|
|
2593
2615
|
description: "Full formatted list of Hevy routine folders",
|
|
2594
2616
|
mimeType: JSON_MIME_TYPE
|
|
2595
2617
|
}, async (uri) => {
|
|
2596
|
-
return createJsonResourceResult(uri, (await fetchAllRoutineFolders(runtime)).map(
|
|
2618
|
+
return createJsonResourceResult(uri, (await fetchAllRoutineFolders(runtime)).map(projectRoutineFolder));
|
|
2597
2619
|
});
|
|
2598
2620
|
}
|
|
2599
2621
|
//#endregion
|
|
2600
2622
|
//#region ../core/src/server-metadata.ts
|
|
2601
2623
|
const SERVER_NAME = "hevy-mcp";
|
|
2602
|
-
const SERVER_VERSION = "
|
|
2624
|
+
const SERVER_VERSION = "5.0.0";
|
|
2603
2625
|
const SERVER_INSTRUCTIONS = [
|
|
2604
2626
|
"Hevy MCP connects clients to the authenticated user's Hevy workout-tracking data, including workouts, routines, exercise templates, routine folders, body measurements, and profile information. HEVY_API_KEY must contain a valid Hevy API key for local stdio use.",
|
|
2605
2627
|
"Safety: all get-* and search-* tools are read-only. create-* and update-* tools mutate Hevy data. Creates are additive and non-idempotent, so repeating one can create duplicates. Updates can overwrite existing data. Delete operations are not available.",
|
|
2606
2628
|
"Workflow: search exercise templates first, then use the returned template IDs when creating workouts or routines. To create a completed workout from a routine, fetch the routine as a plan, then obtain the actual completed sets and end time from the user; never invent completion data.",
|
|
2607
|
-
"Pagination: start at page 1 and fetch only the pages needed. Most list tools allow
|
|
2629
|
+
"Pagination: start at page 1 and fetch only the pages needed. Most list tools allow page_size up to 10; get-exercise-templates allows page_size up to 100.",
|
|
2608
2630
|
"Rate limits and retries: minimize repeated calls. If Hevy returns HTTP 429, follow its retry guidance. Transient read requests retry automatically, but write requests do not; confirm uncertain write outcomes before trying again."
|
|
2609
2631
|
].join("\n\n");
|
|
2610
2632
|
//#endregion
|
|
2633
|
+
//#region ../core/src/utils/compact-json-schema.ts
|
|
2634
|
+
function isRecord$1(value) {
|
|
2635
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
2636
|
+
}
|
|
2637
|
+
function isNullSchema(value) {
|
|
2638
|
+
return isRecord$1(value) && value.type === "null" && Object.keys(value).length === 1;
|
|
2639
|
+
}
|
|
2640
|
+
function compactNullableSchema(value) {
|
|
2641
|
+
if (Array.isArray(value)) return value.map(compactNullableSchema);
|
|
2642
|
+
if (!isRecord$1(value)) return value;
|
|
2643
|
+
const anyOf = value.anyOf;
|
|
2644
|
+
if (Array.isArray(anyOf) && anyOf.length === 2) {
|
|
2645
|
+
const nullableBranch = anyOf.find(isNullSchema);
|
|
2646
|
+
const valueBranch = anyOf.find((branch) => !isNullSchema(branch));
|
|
2647
|
+
if (nullableBranch !== void 0 && isRecord$1(valueBranch) && typeof valueBranch.type === "string") {
|
|
2648
|
+
const { anyOf: _anyOf, ...metadata } = value;
|
|
2649
|
+
return compactNullableSchema({
|
|
2650
|
+
...valueBranch,
|
|
2651
|
+
...metadata,
|
|
2652
|
+
type: [valueBranch.type, "null"]
|
|
2653
|
+
});
|
|
2654
|
+
}
|
|
2655
|
+
}
|
|
2656
|
+
return Object.fromEntries(Object.entries(value).map(([key, nested]) => [key, compactNullableSchema(nested)]));
|
|
2657
|
+
}
|
|
2658
|
+
function omitOutputObjectRestrictions(value) {
|
|
2659
|
+
if (Array.isArray(value)) return value.map(omitOutputObjectRestrictions);
|
|
2660
|
+
if (!isRecord$1(value)) return value;
|
|
2661
|
+
return Object.fromEntries(Object.entries(value).filter(([key, nested]) => key !== "additionalProperties" || nested !== false).map(([key, nested]) => [key, omitOutputObjectRestrictions(nested)]));
|
|
2662
|
+
}
|
|
2663
|
+
function convertSchema(schema, io) {
|
|
2664
|
+
const converted = z.toJSONSchema(schema, {
|
|
2665
|
+
target: "draft-2020-12",
|
|
2666
|
+
io
|
|
2667
|
+
});
|
|
2668
|
+
if (!isRecord$1(converted)) throw new Error("Zod emitted a non-object root schema");
|
|
2669
|
+
const compacted = compactNullableSchema(converted);
|
|
2670
|
+
if (!isRecord$1(compacted)) throw new Error("Nullable schema compaction changed the root schema");
|
|
2671
|
+
const output = io === "output" ? omitOutputObjectRestrictions(compacted) : compacted;
|
|
2672
|
+
if (!isRecord$1(output)) throw new Error("Output schema compaction changed the root schema");
|
|
2673
|
+
delete output.$schema;
|
|
2674
|
+
return output;
|
|
2675
|
+
}
|
|
2676
|
+
/**
|
|
2677
|
+
* Keep Zod validation while advertising a compact JSON Schema.
|
|
2678
|
+
*
|
|
2679
|
+
* Zod emits nullable values as `anyOf`; draft 2020-12 also permits a type
|
|
2680
|
+
* array. The MCP wire envelope does not need a repeated `$schema` marker, and
|
|
2681
|
+
* structured output is validated by the original Zod schema before sending.
|
|
2682
|
+
*/
|
|
2683
|
+
function compactJsonSchema(schema) {
|
|
2684
|
+
let inputSchema;
|
|
2685
|
+
let outputSchema;
|
|
2686
|
+
Object.defineProperty(schema["~standard"], "jsonSchema", {
|
|
2687
|
+
configurable: true,
|
|
2688
|
+
value: {
|
|
2689
|
+
input: () => {
|
|
2690
|
+
inputSchema ??= convertSchema(schema, "input");
|
|
2691
|
+
return inputSchema;
|
|
2692
|
+
},
|
|
2693
|
+
output: () => {
|
|
2694
|
+
outputSchema ??= convertSchema(schema, "output");
|
|
2695
|
+
return outputSchema;
|
|
2696
|
+
}
|
|
2697
|
+
},
|
|
2698
|
+
writable: true
|
|
2699
|
+
});
|
|
2700
|
+
return schema;
|
|
2701
|
+
}
|
|
2702
|
+
//#endregion
|
|
2611
2703
|
//#region ../core/src/utils/tool-helpers.ts
|
|
2612
2704
|
/**
|
|
2613
2705
|
* Type-safe utilities for MCP tool handlers
|
|
@@ -2645,7 +2737,7 @@ function requireClient(client) {
|
|
|
2645
2737
|
* ```
|
|
2646
2738
|
*/
|
|
2647
2739
|
function createTypedToolHandler(schema, handler) {
|
|
2648
|
-
const zodSchema = z.
|
|
2740
|
+
const zodSchema = z.strictObject(schema);
|
|
2649
2741
|
return async (args) => {
|
|
2650
2742
|
return handler(zodSchema.parse(args));
|
|
2651
2743
|
};
|
|
@@ -2659,11 +2751,12 @@ function registerToolDefinition(server, runtime, definition) {
|
|
|
2659
2751
|
kind: definition.kind,
|
|
2660
2752
|
operation: definition.operation
|
|
2661
2753
|
});
|
|
2754
|
+
const inputSchema = compactJsonSchema(z.strictObject(definition.inputSchema));
|
|
2662
2755
|
const config = {
|
|
2663
2756
|
description: definition.description,
|
|
2664
|
-
inputSchema
|
|
2757
|
+
inputSchema,
|
|
2665
2758
|
annotations: definition.annotations,
|
|
2666
|
-
...definition.kind === "read" ? { outputSchema: z.object(definition.outputSchema) } : {}
|
|
2759
|
+
...definition.kind === "read" ? { outputSchema: compactJsonSchema(z.object(definition.outputSchema)) } : {}
|
|
2667
2760
|
};
|
|
2668
2761
|
server.registerTool(definition.name, config, callback);
|
|
2669
2762
|
}
|
|
@@ -2701,16 +2794,6 @@ function updateAnnotations(title) {
|
|
|
2701
2794
|
};
|
|
2702
2795
|
}
|
|
2703
2796
|
//#endregion
|
|
2704
|
-
//#region ../core/src/utils/tool-descriptions.ts
|
|
2705
|
-
function describeTool({ summary, aliases, useCase, importantNotes }) {
|
|
2706
|
-
return [
|
|
2707
|
-
summary,
|
|
2708
|
-
`Aliases: ${aliases.join(", ")}.`,
|
|
2709
|
-
`<use_case>${useCase}</use_case>`,
|
|
2710
|
-
`<important_notes>${importantNotes}</important_notes>`
|
|
2711
|
-
].join(" ");
|
|
2712
|
-
}
|
|
2713
|
-
//#endregion
|
|
2714
2797
|
//#region ../core/src/utils/json-parser.ts
|
|
2715
2798
|
/**
|
|
2716
2799
|
* Preprocessor to handle MCP clients that send JSON-stringified arrays
|
|
@@ -2732,143 +2815,156 @@ function parseJsonArray(val) {
|
|
|
2732
2815
|
}
|
|
2733
2816
|
//#endregion
|
|
2734
2817
|
//#region ../core/src/tools/input-schemas.ts
|
|
2735
|
-
/** Build the page and
|
|
2818
|
+
/** Build the page and page_size fields shared by paginated tools. */
|
|
2736
2819
|
function paginationShape({ defaultPageSize, maxPageSize, integerPage = true }) {
|
|
2737
2820
|
const pageNumber = z.coerce.number().gte(1);
|
|
2738
2821
|
return {
|
|
2739
2822
|
page: integerPage ? pageNumber.int() : pageNumber,
|
|
2740
|
-
|
|
2823
|
+
page_size: z.coerce.number().int().gte(1).lte(maxPageSize).default(defaultPageSize)
|
|
2741
2824
|
};
|
|
2742
2825
|
}
|
|
2743
2826
|
const nonEmptyId = z.string().min(1);
|
|
2744
|
-
const
|
|
2827
|
+
const exerciseTemplatePayloadShape = {
|
|
2745
2828
|
title: z.string().min(1),
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2829
|
+
exercise_type: exerciseTypeEnum,
|
|
2830
|
+
equipment_category: equipmentCategoryEnum,
|
|
2831
|
+
muscle_group: muscleGroupEnum,
|
|
2832
|
+
other_muscles: z.array(muscleGroupEnum).default([])
|
|
2750
2833
|
};
|
|
2751
|
-
const
|
|
2834
|
+
const exerciseTemplateInputShape = z.strictObject({ exercise: z.strictObject(exerciseTemplatePayloadShape) }).shape;
|
|
2835
|
+
const routineFolderPayloadShape = { title: z.string().min(1) };
|
|
2836
|
+
const routineFolderInputShape = z.strictObject({ routine_folder: z.strictObject(routineFolderPayloadShape) }).shape;
|
|
2752
2837
|
const CALENDAR_DATE_MESSAGE = "Date must be in YYYY-MM-DD format";
|
|
2753
2838
|
const calendarDate = z.string().regex(/^\d{4}-\d{2}-\d{2}$/, CALENDAR_DATE_MESSAGE).refine((value) => {
|
|
2754
2839
|
const parsed = /* @__PURE__ */ new Date(`${value}T00:00:00.000Z`);
|
|
2755
2840
|
return !Number.isNaN(parsed.getTime()) && parsed.toISOString().startsWith(value);
|
|
2756
2841
|
}, CALENDAR_DATE_MESSAGE);
|
|
2842
|
+
const rpeEnum = z.union([
|
|
2843
|
+
z.literal(6),
|
|
2844
|
+
z.literal(7),
|
|
2845
|
+
z.literal(7.5),
|
|
2846
|
+
z.literal(8),
|
|
2847
|
+
z.literal(8.5),
|
|
2848
|
+
z.literal(9),
|
|
2849
|
+
z.literal(9.5),
|
|
2850
|
+
z.literal(10)
|
|
2851
|
+
]);
|
|
2757
2852
|
const workoutSetShape = {
|
|
2758
2853
|
type: setTypeEnum,
|
|
2759
|
-
|
|
2760
|
-
weightKg: z.coerce.number().optional().nullable(),
|
|
2854
|
+
weight_kg: z.coerce.number().optional().nullable(),
|
|
2761
2855
|
reps: z.coerce.number().int().optional().nullable(),
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
rpe: z.coerce.number().optional().nullable(),
|
|
2767
|
-
customMetric: z.coerce.number().optional().nullable()
|
|
2856
|
+
distance_meters: z.coerce.number().int().optional().nullable(),
|
|
2857
|
+
duration_seconds: z.coerce.number().int().optional().nullable(),
|
|
2858
|
+
rpe: rpeEnum.optional().nullable(),
|
|
2859
|
+
custom_metric: z.coerce.number().optional().nullable()
|
|
2768
2860
|
};
|
|
2861
|
+
const workoutSetSchema = z.strictObject(workoutSetShape);
|
|
2769
2862
|
const workoutExerciseShape = {
|
|
2770
|
-
|
|
2771
|
-
|
|
2863
|
+
exercise_template_id: nonEmptyId,
|
|
2864
|
+
superset_id: z.coerce.number().nullable().optional(),
|
|
2772
2865
|
notes: z.string().optional().nullable(),
|
|
2773
|
-
sets: z.array(
|
|
2866
|
+
sets: z.array(workoutSetSchema)
|
|
2774
2867
|
};
|
|
2775
|
-
const
|
|
2868
|
+
const workoutExerciseSchema = z.strictObject(workoutExerciseShape);
|
|
2869
|
+
const workoutExercisesSchema = z.preprocess(parseJsonArray, z.array(workoutExerciseSchema));
|
|
2870
|
+
const replaceWorkoutPayloadShape = {
|
|
2776
2871
|
title: z.string().min(1),
|
|
2777
2872
|
description: z.string().optional().nullable(),
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
exercises:
|
|
2873
|
+
start_time: utcSecondTimestamp,
|
|
2874
|
+
end_time: utcSecondTimestamp,
|
|
2875
|
+
is_private: z.boolean().default(false),
|
|
2876
|
+
exercises: workoutExercisesSchema
|
|
2782
2877
|
};
|
|
2878
|
+
const replaceWorkoutPayloadSchema = z.strictObject(replaceWorkoutPayloadShape);
|
|
2879
|
+
const workoutInputShape = z.strictObject({ workout: replaceWorkoutPayloadSchema }).shape;
|
|
2880
|
+
z.strictObject({
|
|
2881
|
+
workout_id: nonEmptyId,
|
|
2882
|
+
workout: replaceWorkoutPayloadSchema
|
|
2883
|
+
}).shape;
|
|
2884
|
+
const workoutMetadataPatchSchema = z.strictObject({
|
|
2885
|
+
title: z.string().min(1).optional(),
|
|
2886
|
+
description: z.string().nullable().optional(),
|
|
2887
|
+
start_time: utcSecondTimestamp.optional(),
|
|
2888
|
+
end_time: utcSecondTimestamp.optional(),
|
|
2889
|
+
is_private: z.boolean().optional()
|
|
2890
|
+
}).refine((patch) => Object.values(patch).some((value) => value !== void 0), "Include at least one workout metadata field").meta({ minProperties: 1 });
|
|
2891
|
+
const updateWorkoutInputShape = z.strictObject({
|
|
2892
|
+
workout_id: nonEmptyId,
|
|
2893
|
+
workout: workoutMetadataPatchSchema
|
|
2894
|
+
}).shape;
|
|
2895
|
+
const replaceWorkoutExercisesInputShape = z.strictObject({
|
|
2896
|
+
workout_id: nonEmptyId,
|
|
2897
|
+
workout: z.strictObject({ exercises: workoutExercisesSchema })
|
|
2898
|
+
}).shape;
|
|
2783
2899
|
const routineSetShape = {
|
|
2784
2900
|
type: setTypeEnum,
|
|
2785
|
-
|
|
2786
|
-
weightKg: z.coerce.number().optional(),
|
|
2901
|
+
weight_kg: z.coerce.number().optional(),
|
|
2787
2902
|
reps: zNullableInt,
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
customMetric: z.coerce.number().optional(),
|
|
2793
|
-
repRange: zOptionalRepRange
|
|
2903
|
+
distance_meters: z.coerce.number().int().optional(),
|
|
2904
|
+
duration_seconds: z.coerce.number().int().optional(),
|
|
2905
|
+
rep_range: zStrictOptionalRepRange,
|
|
2906
|
+
custom_metric: z.coerce.number().optional()
|
|
2794
2907
|
};
|
|
2908
|
+
const routineSetSchema = z.strictObject(routineSetShape);
|
|
2795
2909
|
const routineExerciseShape = {
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2910
|
+
exercise_template_id: nonEmptyId,
|
|
2911
|
+
superset_id: z.coerce.number().nullable().optional(),
|
|
2912
|
+
rest_seconds: z.coerce.number().int().min(0).optional(),
|
|
2799
2913
|
notes: z.string().optional(),
|
|
2800
|
-
sets: z.array(
|
|
2914
|
+
sets: z.array(routineSetSchema)
|
|
2801
2915
|
};
|
|
2916
|
+
const routineExerciseSchema = z.strictObject(routineExerciseShape);
|
|
2917
|
+
const routineExercisesSchema = z.preprocess(parseJsonArray, z.array(routineExerciseSchema));
|
|
2802
2918
|
const routinePayloadShape = {
|
|
2803
2919
|
title: z.string().min(1),
|
|
2804
|
-
|
|
2920
|
+
folder_id: z.coerce.number().nullable().optional(),
|
|
2805
2921
|
notes: z.string().optional(),
|
|
2806
|
-
exercises:
|
|
2922
|
+
exercises: routineExercisesSchema
|
|
2807
2923
|
};
|
|
2808
|
-
const
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
chestCm: zNullableNumber.describe("Chest circumference in centimeters"),
|
|
2815
|
-
leftBicepCm: zNullableNumber.describe("Left bicep circumference in centimeters"),
|
|
2816
|
-
rightBicepCm: zNullableNumber.describe("Right bicep circumference in centimeters"),
|
|
2817
|
-
leftForearmCm: zNullableNumber.describe("Left forearm circumference in centimeters"),
|
|
2818
|
-
rightForearmCm: zNullableNumber.describe("Right forearm circumference in centimeters"),
|
|
2819
|
-
abdomen: zNullableNumber.describe("Abdomen circumference in centimeters"),
|
|
2820
|
-
waist: zNullableNumber.describe("Waist circumference in centimeters"),
|
|
2821
|
-
hips: zNullableNumber.describe("Hips circumference in centimeters"),
|
|
2822
|
-
leftThigh: zNullableNumber.describe("Left thigh circumference in centimeters"),
|
|
2823
|
-
rightThigh: zNullableNumber.describe("Right thigh circumference in centimeters"),
|
|
2824
|
-
leftCalf: zNullableNumber.describe("Left calf circumference in centimeters"),
|
|
2825
|
-
rightCalf: zNullableNumber.describe("Right calf circumference in centimeters")
|
|
2924
|
+
const routinePayloadSchema = z.strictObject(routinePayloadShape);
|
|
2925
|
+
const createRoutineInputShape = z.strictObject({ routine: routinePayloadSchema }).shape;
|
|
2926
|
+
const routineUpdatePayloadShape = {
|
|
2927
|
+
title: z.string().min(1),
|
|
2928
|
+
notes: z.string().optional(),
|
|
2929
|
+
exercises: routineExercisesSchema
|
|
2826
2930
|
};
|
|
2827
|
-
const
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2931
|
+
const routineUpdatePayloadSchema = z.strictObject(routineUpdatePayloadShape);
|
|
2932
|
+
const updateRoutineInputShape = z.strictObject({
|
|
2933
|
+
routine_id: nonEmptyId,
|
|
2934
|
+
routine: routineUpdatePayloadSchema
|
|
2935
|
+
}).shape;
|
|
2936
|
+
const bodyMeasurementFieldsSchema = {
|
|
2937
|
+
weight_kg: zNullableNumber,
|
|
2938
|
+
lean_mass_kg: zNullableNumber,
|
|
2939
|
+
fat_percent: zNullableNumber,
|
|
2940
|
+
neck_cm: zNullableNumber,
|
|
2941
|
+
shoulder_cm: zNullableNumber,
|
|
2942
|
+
chest_cm: zNullableNumber,
|
|
2943
|
+
left_bicep_cm: zNullableNumber,
|
|
2944
|
+
right_bicep_cm: zNullableNumber,
|
|
2945
|
+
left_forearm_cm: zNullableNumber,
|
|
2946
|
+
right_forearm_cm: zNullableNumber,
|
|
2947
|
+
abdomen: zNullableNumber.describe("Circumference in centimeters."),
|
|
2948
|
+
waist: zNullableNumber.describe("Circumference in centimeters."),
|
|
2949
|
+
hips: zNullableNumber.describe("Circumference in centimeters."),
|
|
2950
|
+
left_thigh: zNullableNumber,
|
|
2951
|
+
right_thigh: zNullableNumber,
|
|
2952
|
+
left_calf: zNullableNumber,
|
|
2953
|
+
right_calf: zNullableNumber
|
|
2845
2954
|
};
|
|
2955
|
+
z.strictObject(bodyMeasurementFieldsSchema);
|
|
2956
|
+
const createBodyMeasurementInputSchema = z.strictObject({
|
|
2957
|
+
date: calendarDate,
|
|
2958
|
+
...bodyMeasurementFieldsSchema
|
|
2959
|
+
});
|
|
2960
|
+
const updateBodyMeasurementInputSchema = z.strictObject({
|
|
2961
|
+
date: calendarDate,
|
|
2962
|
+
...bodyMeasurementFieldsSchema
|
|
2963
|
+
});
|
|
2964
|
+
const createBodyMeasurementInputShape = createBodyMeasurementInputSchema.shape;
|
|
2965
|
+
const updateBodyMeasurementInputShape = updateBodyMeasurementInputSchema.shape;
|
|
2846
2966
|
//#endregion
|
|
2847
|
-
//#region ../core/src/tools/
|
|
2848
|
-
/** Map the public camelCase workout input to the API's snake_case payload. */
|
|
2849
|
-
function buildWorkoutPayload(input) {
|
|
2850
|
-
return {
|
|
2851
|
-
title: input.title,
|
|
2852
|
-
description: input.description ?? null,
|
|
2853
|
-
start_time: input.startTime,
|
|
2854
|
-
end_time: input.endTime,
|
|
2855
|
-
is_private: input.isPrivate,
|
|
2856
|
-
exercises: input.exercises.map((exercise) => ({
|
|
2857
|
-
exercise_template_id: exercise.exerciseTemplateId,
|
|
2858
|
-
superset_id: exercise.supersetId ?? null,
|
|
2859
|
-
notes: exercise.notes ?? null,
|
|
2860
|
-
sets: exercise.sets.map((set) => ({
|
|
2861
|
-
type: set.type,
|
|
2862
|
-
weight_kg: set.weight ?? set.weightKg ?? null,
|
|
2863
|
-
reps: set.reps ?? null,
|
|
2864
|
-
distance_meters: set.distance ?? set.distanceMeters ?? null,
|
|
2865
|
-
duration_seconds: set.duration ?? set.durationSeconds ?? null,
|
|
2866
|
-
rpe: set.rpe ?? null,
|
|
2867
|
-
custom_metric: set.customMetric ?? null
|
|
2868
|
-
}))
|
|
2869
|
-
}))
|
|
2870
|
-
};
|
|
2871
|
-
}
|
|
2967
|
+
//#region ../core/src/tools/mutation-semantics.ts
|
|
2872
2968
|
function buildRepRange(repRange) {
|
|
2873
2969
|
if (!repRange) return null;
|
|
2874
2970
|
const start = repRange.start ?? void 0;
|
|
@@ -2888,14 +2984,14 @@ function getFixedRepsFromRepRange(repRange) {
|
|
|
2888
2984
|
}
|
|
2889
2985
|
function buildRoutineSets(sets, mode) {
|
|
2890
2986
|
return sets.map((set) => {
|
|
2891
|
-
const repRange = buildRepRange(set.
|
|
2987
|
+
const repRange = buildRepRange(set.rep_range);
|
|
2892
2988
|
const reps = typeof set.reps === "number" ? set.reps : getFixedRepsFromRepRange(repRange);
|
|
2893
2989
|
const common = {
|
|
2894
|
-
weight_kg: set.
|
|
2990
|
+
weight_kg: set.weight_kg ?? null,
|
|
2895
2991
|
reps: reps ?? null,
|
|
2896
|
-
distance_meters: set.
|
|
2897
|
-
duration_seconds: set.
|
|
2898
|
-
custom_metric: set.
|
|
2992
|
+
distance_meters: set.distance_meters ?? null,
|
|
2993
|
+
duration_seconds: set.duration_seconds ?? null,
|
|
2994
|
+
custom_metric: set.custom_metric ?? null
|
|
2899
2995
|
};
|
|
2900
2996
|
if (mode === "create") return {
|
|
2901
2997
|
...common,
|
|
@@ -2915,9 +3011,9 @@ function buildRoutinePayload(input, mode) {
|
|
|
2915
3011
|
const sets = buildRoutineSets(exercise.sets, mode);
|
|
2916
3012
|
if (sets.some((set) => set.rep_range != null && getFixedRepsFromRepRange(set.rep_range) === null)) usesRepRanges = true;
|
|
2917
3013
|
return {
|
|
2918
|
-
exercise_template_id: exercise.
|
|
2919
|
-
superset_id: exercise.
|
|
2920
|
-
rest_seconds: exercise.
|
|
3014
|
+
exercise_template_id: exercise.exercise_template_id,
|
|
3015
|
+
superset_id: exercise.superset_id ?? null,
|
|
3016
|
+
rest_seconds: exercise.rest_seconds ?? null,
|
|
2921
3017
|
notes: exercise.notes ?? null,
|
|
2922
3018
|
sets
|
|
2923
3019
|
};
|
|
@@ -2925,7 +3021,7 @@ function buildRoutinePayload(input, mode) {
|
|
|
2925
3021
|
if (mode === "create") return {
|
|
2926
3022
|
payload: {
|
|
2927
3023
|
title: input.title,
|
|
2928
|
-
folder_id: input.
|
|
3024
|
+
folder_id: input.folder_id ?? null,
|
|
2929
3025
|
notes: input.notes ?? "",
|
|
2930
3026
|
exercises
|
|
2931
3027
|
},
|
|
@@ -2940,27 +3036,71 @@ function buildRoutinePayload(input, mode) {
|
|
|
2940
3036
|
usesRepRanges
|
|
2941
3037
|
};
|
|
2942
3038
|
}
|
|
3039
|
+
const workoutUpdateMetadataSchema = z.object({
|
|
3040
|
+
title: z.string().min(1),
|
|
3041
|
+
start_time: utcSecondTimestamp,
|
|
3042
|
+
end_time: utcSecondTimestamp
|
|
3043
|
+
});
|
|
3044
|
+
/**
|
|
3045
|
+
* Map fetched API exercises to the update shape without validating legacy data.
|
|
3046
|
+
* Caller-supplied replacement exercises are still validated by their input schema.
|
|
3047
|
+
*/
|
|
3048
|
+
function preserveWorkoutExercises(current) {
|
|
3049
|
+
return current.exercises?.map((exercise) => ({
|
|
3050
|
+
exercise_template_id: exercise.exercise_template_id,
|
|
3051
|
+
superset_id: exercise.supersets_id ?? null,
|
|
3052
|
+
notes: exercise.notes ?? null,
|
|
3053
|
+
sets: exercise.sets?.map((set) => ({
|
|
3054
|
+
...set.type === void 0 ? {} : { type: set.type },
|
|
3055
|
+
weight_kg: set.weight_kg ?? null,
|
|
3056
|
+
reps: set.reps ?? null,
|
|
3057
|
+
distance_meters: set.distance_meters ?? null,
|
|
3058
|
+
duration_seconds: set.duration_seconds ?? null,
|
|
3059
|
+
rpe: set.rpe,
|
|
3060
|
+
custom_metric: set.custom_metric ?? null
|
|
3061
|
+
})) ?? []
|
|
3062
|
+
})) ?? [];
|
|
3063
|
+
}
|
|
3064
|
+
function buildWorkoutUpdatePayload(current, patch, replacementExercises) {
|
|
3065
|
+
return {
|
|
3066
|
+
...workoutUpdateMetadataSchema.parse({
|
|
3067
|
+
title: patch.title !== void 0 ? patch.title : current.title,
|
|
3068
|
+
start_time: patch.start_time !== void 0 ? patch.start_time : current.start_time,
|
|
3069
|
+
end_time: patch.end_time !== void 0 ? patch.end_time : current.end_time
|
|
3070
|
+
}),
|
|
3071
|
+
description: patch.description !== void 0 ? patch.description : current.description ?? null,
|
|
3072
|
+
exercises: replacementExercises === void 0 ? preserveWorkoutExercises(current) : replacementExercises,
|
|
3073
|
+
...patch.is_private !== void 0 ? { is_private: patch.is_private } : {}
|
|
3074
|
+
};
|
|
3075
|
+
}
|
|
3076
|
+
const measurementKeys = [
|
|
3077
|
+
"weight_kg",
|
|
3078
|
+
"lean_mass_kg",
|
|
3079
|
+
"fat_percent",
|
|
3080
|
+
"neck_cm",
|
|
3081
|
+
"shoulder_cm",
|
|
3082
|
+
"chest_cm",
|
|
3083
|
+
"left_bicep_cm",
|
|
3084
|
+
"right_bicep_cm",
|
|
3085
|
+
"left_forearm_cm",
|
|
3086
|
+
"right_forearm_cm",
|
|
3087
|
+
"abdomen",
|
|
3088
|
+
"waist",
|
|
3089
|
+
"hips",
|
|
3090
|
+
"left_thigh",
|
|
3091
|
+
"right_thigh",
|
|
3092
|
+
"left_calf",
|
|
3093
|
+
"right_calf"
|
|
3094
|
+
];
|
|
2943
3095
|
/** Omit nullish measurement values because the API rejects explicit nulls. */
|
|
2944
3096
|
function buildMeasurementPayload(fields) {
|
|
2945
3097
|
const payload = {};
|
|
2946
|
-
for (const
|
|
2947
|
-
const value = fields[
|
|
2948
|
-
if (value != null) payload[
|
|
3098
|
+
for (const key of measurementKeys) {
|
|
3099
|
+
const value = fields[key];
|
|
3100
|
+
if (value != null) payload[key] = value;
|
|
2949
3101
|
}
|
|
2950
3102
|
return payload;
|
|
2951
3103
|
}
|
|
2952
|
-
function buildExerciseTemplateRequest(input) {
|
|
2953
|
-
return { exercise: {
|
|
2954
|
-
title: input.title,
|
|
2955
|
-
exercise_type: input.exerciseType,
|
|
2956
|
-
equipment_category: input.equipmentCategory,
|
|
2957
|
-
muscle_group: input.muscleGroup,
|
|
2958
|
-
other_muscles: input.otherMuscles
|
|
2959
|
-
} };
|
|
2960
|
-
}
|
|
2961
|
-
function buildRoutineFolderRequest(input) {
|
|
2962
|
-
return { routine_folder: { title: input.name } };
|
|
2963
|
-
}
|
|
2964
3104
|
//#endregion
|
|
2965
3105
|
//#region ../core/src/utils/hevy-error-policy.ts
|
|
2966
3106
|
const READ_RESOURCE_ENDPOINTS = /* @__PURE__ */ new Set([
|
|
@@ -2992,41 +3132,26 @@ const getBodyMeasurementsSchema = { ...paginationShape({
|
|
|
2992
3132
|
defaultPageSize: 10,
|
|
2993
3133
|
maxPageSize: 10
|
|
2994
3134
|
}) };
|
|
2995
|
-
const getBodyMeasurementSchema = { date: calendarDate
|
|
2996
|
-
const createBodyMeasurementSchema =
|
|
2997
|
-
|
|
2998
|
-
...bodyMeasurementFieldsSchema
|
|
2999
|
-
};
|
|
3000
|
-
const updateBodyMeasurementSchema = {
|
|
3001
|
-
date: calendarDate.describe("The date of the body measurement to update (YYYY-MM-DD). Must already exist — returns 404 otherwise."),
|
|
3002
|
-
...bodyMeasurementFieldsSchema
|
|
3003
|
-
};
|
|
3135
|
+
const getBodyMeasurementSchema = { date: calendarDate };
|
|
3136
|
+
const createBodyMeasurementSchema = createBodyMeasurementInputShape;
|
|
3137
|
+
const updateBodyMeasurementSchema = updateBodyMeasurementInputShape;
|
|
3004
3138
|
const bodyMeasurementToolDefinitions = [
|
|
3005
3139
|
{
|
|
3006
3140
|
name: "get-body-measurements",
|
|
3007
3141
|
feature: "measurements",
|
|
3008
3142
|
operation: "list",
|
|
3009
|
-
description:
|
|
3010
|
-
summary: "Read-only. Lists dated body measurements for the account.",
|
|
3011
|
-
aliases: [
|
|
3012
|
-
"body stats history",
|
|
3013
|
-
"list weigh-ins",
|
|
3014
|
-
"measurement log"
|
|
3015
|
-
],
|
|
3016
|
-
useCase: "Use to browse measurement history; use get-body-measurement for one exact date.",
|
|
3017
|
-
importantNotes: "Results are paginated; page starts at 1 and pageSize is limited to 10."
|
|
3018
|
-
}),
|
|
3143
|
+
description: "Read-only. Lists dated body measurements; results are paginated. Use get-body-measurement for one date.",
|
|
3019
3144
|
inputSchema: getBodyMeasurementsSchema,
|
|
3020
3145
|
outputSchema: bodyMeasurementsResponse.outputSchema,
|
|
3021
3146
|
annotations: readOnlyAnnotations("Get Body Measurements"),
|
|
3022
3147
|
kind: "read",
|
|
3023
3148
|
responseContract: bodyMeasurementsResponse,
|
|
3024
3149
|
execute: async (runtime, args) => {
|
|
3025
|
-
const { page,
|
|
3150
|
+
const { page, page_size } = args;
|
|
3026
3151
|
try {
|
|
3027
3152
|
const data = await runtime.getClient().getBodyMeasurements({
|
|
3028
3153
|
page,
|
|
3029
|
-
pageSize
|
|
3154
|
+
pageSize: page_size
|
|
3030
3155
|
});
|
|
3031
3156
|
return {
|
|
3032
3157
|
items: data?.body_measurements ?? [],
|
|
@@ -3047,16 +3172,7 @@ const bodyMeasurementToolDefinitions = [
|
|
|
3047
3172
|
name: "get-body-measurement",
|
|
3048
3173
|
feature: "measurements",
|
|
3049
3174
|
operation: "get",
|
|
3050
|
-
description:
|
|
3051
|
-
summary: "Read-only. Retrieves the body measurement entry for one date.",
|
|
3052
|
-
aliases: [
|
|
3053
|
-
"get weigh-in",
|
|
3054
|
-
"show body stats",
|
|
3055
|
-
"measurement by date"
|
|
3056
|
-
],
|
|
3057
|
-
useCase: "Use when the exact measurement date is known; use get-body-measurements to browse dates.",
|
|
3058
|
-
importantNotes: "date must use YYYY-MM-DD; at most one entry exists per date."
|
|
3059
|
-
}),
|
|
3175
|
+
description: "Read-only. Gets the body measurement for one YYYY-MM-DD date. Use get-body-measurements to browse dates.",
|
|
3060
3176
|
inputSchema: getBodyMeasurementSchema,
|
|
3061
3177
|
outputSchema: bodyMeasurementResponse.outputSchema,
|
|
3062
3178
|
annotations: readOnlyAnnotations("Get Body Measurement"),
|
|
@@ -3066,12 +3182,12 @@ const bodyMeasurementToolDefinitions = [
|
|
|
3066
3182
|
const { date } = args;
|
|
3067
3183
|
try {
|
|
3068
3184
|
return {
|
|
3069
|
-
|
|
3185
|
+
body_measurement: await runtime.getClient().getBodyMeasurement(date),
|
|
3070
3186
|
date
|
|
3071
3187
|
};
|
|
3072
3188
|
} catch (error) {
|
|
3073
3189
|
if (isExpectedReadNotFound(error)) return {
|
|
3074
|
-
|
|
3190
|
+
body_measurement: null,
|
|
3075
3191
|
date,
|
|
3076
3192
|
expected404Outcome: "not_found"
|
|
3077
3193
|
};
|
|
@@ -3083,16 +3199,7 @@ const bodyMeasurementToolDefinitions = [
|
|
|
3083
3199
|
name: "create-body-measurement",
|
|
3084
3200
|
feature: "measurements",
|
|
3085
3201
|
operation: "create",
|
|
3086
|
-
description:
|
|
3087
|
-
summary: "Writes to the Hevy account by creating a dated body measurement.",
|
|
3088
|
-
aliases: [
|
|
3089
|
-
"log weigh-in",
|
|
3090
|
-
"add body stats",
|
|
3091
|
-
"record measurements"
|
|
3092
|
-
],
|
|
3093
|
-
useCase: "Use for a date without an entry; use update-body-measurement when that date already exists.",
|
|
3094
|
-
importantNotes: "date must use YYYY-MM-DD and be unique. Null fields are omitted and cannot clear values; an existing date returns 409."
|
|
3095
|
-
}),
|
|
3202
|
+
description: "Writes a body measurement for a new YYYY-MM-DD date. Existing dates conflict; retries are not idempotent.",
|
|
3096
3203
|
inputSchema: createBodyMeasurementSchema,
|
|
3097
3204
|
annotations: createAnnotations("Create Body Measurement"),
|
|
3098
3205
|
kind: "write",
|
|
@@ -3110,16 +3217,7 @@ const bodyMeasurementToolDefinitions = [
|
|
|
3110
3217
|
name: "update-body-measurement",
|
|
3111
3218
|
feature: "measurements",
|
|
3112
3219
|
operation: "update",
|
|
3113
|
-
description:
|
|
3114
|
-
summary: "Mutates the Hevy account by updating a body measurement for a date.",
|
|
3115
|
-
aliases: [
|
|
3116
|
-
"edit weigh-in",
|
|
3117
|
-
"correct body stats",
|
|
3118
|
-
"change measurements"
|
|
3119
|
-
],
|
|
3120
|
-
useCase: "Use to change fields on an existing date; use create-body-measurement for a new date.",
|
|
3121
|
-
importantNotes: "date must use YYYY-MM-DD and already exist. Provide at least one numeric field; nulls are omitted and cannot clear stored values."
|
|
3122
|
-
}),
|
|
3220
|
+
description: "Mutates numeric fields on an existing YYYY-MM-DD measurement. Omitted fields remain unchanged; values cannot be cleared.",
|
|
3123
3221
|
inputSchema: updateBodyMeasurementSchema,
|
|
3124
3222
|
annotations: updateAnnotations("Update Body Measurement"),
|
|
3125
3223
|
kind: "write",
|
|
@@ -3127,7 +3225,7 @@ const bodyMeasurementToolDefinitions = [
|
|
|
3127
3225
|
execute: async (runtime, args) => {
|
|
3128
3226
|
const { date, ...fields } = args;
|
|
3129
3227
|
const payload = buildMeasurementPayload(fields);
|
|
3130
|
-
if (Object.keys(payload).length === 0) throw new Error("No measurement fields provided. Include at least one numeric measurement field (e.g.
|
|
3228
|
+
if (Object.keys(payload).length === 0) throw new Error("No measurement fields provided. Include at least one numeric measurement field (e.g. weight_kg) to update.");
|
|
3131
3229
|
await runtime.getClient().updateBodyMeasurement(date, payload);
|
|
3132
3230
|
return date;
|
|
3133
3231
|
}
|
|
@@ -3139,34 +3237,25 @@ const getRoutineFoldersSchema = paginationShape({
|
|
|
3139
3237
|
defaultPageSize: 5,
|
|
3140
3238
|
maxPageSize: 10
|
|
3141
3239
|
});
|
|
3142
|
-
const getRoutineFolderSchema = {
|
|
3240
|
+
const getRoutineFolderSchema = { folder_id: nonEmptyId };
|
|
3143
3241
|
const createRoutineFolderSchema = routineFolderInputShape;
|
|
3144
3242
|
const folderToolDefinitions = [
|
|
3145
3243
|
{
|
|
3146
3244
|
name: "get-routine-folders",
|
|
3147
3245
|
feature: "folders",
|
|
3148
3246
|
operation: "list",
|
|
3149
|
-
description:
|
|
3150
|
-
summary: "Read-only. Lists default and custom routine folders.",
|
|
3151
|
-
aliases: [
|
|
3152
|
-
"list folders",
|
|
3153
|
-
"browse routine groups",
|
|
3154
|
-
"show plan folders"
|
|
3155
|
-
],
|
|
3156
|
-
useCase: "Use to browse folder organization or discover folder IDs for routine creation.",
|
|
3157
|
-
importantNotes: "Results are paginated; page starts at 1 and pageSize is limited to 10."
|
|
3158
|
-
}),
|
|
3247
|
+
description: "Read-only. Lists routine folders and IDs; results are paginated.",
|
|
3159
3248
|
inputSchema: getRoutineFoldersSchema,
|
|
3160
3249
|
outputSchema: routineFoldersResponse.outputSchema,
|
|
3161
3250
|
annotations: readOnlyAnnotations("Get Routine Folders"),
|
|
3162
3251
|
kind: "read",
|
|
3163
3252
|
responseContract: routineFoldersResponse,
|
|
3164
3253
|
execute: async (runtime, args) => {
|
|
3165
|
-
const { page,
|
|
3254
|
+
const { page, page_size } = args;
|
|
3166
3255
|
try {
|
|
3167
3256
|
const data = await runtime.getClient().getRoutineFolders({
|
|
3168
3257
|
page,
|
|
3169
|
-
pageSize
|
|
3258
|
+
pageSize: page_size
|
|
3170
3259
|
});
|
|
3171
3260
|
return {
|
|
3172
3261
|
items: data?.routine_folders ?? [],
|
|
@@ -3187,32 +3276,23 @@ const folderToolDefinitions = [
|
|
|
3187
3276
|
name: "get-routine-folder",
|
|
3188
3277
|
feature: "folders",
|
|
3189
3278
|
operation: "get",
|
|
3190
|
-
description:
|
|
3191
|
-
summary: "Read-only. Retrieves one routine folder's metadata by ID.",
|
|
3192
|
-
aliases: [
|
|
3193
|
-
"show folder",
|
|
3194
|
-
"folder details",
|
|
3195
|
-
"routine folder metadata"
|
|
3196
|
-
],
|
|
3197
|
-
useCase: "Use for one known folder; use get-routine-folders to browse or discover folder IDs.",
|
|
3198
|
-
importantNotes: "Requires a folderId from get-routine-folders or a prior create response."
|
|
3199
|
-
}),
|
|
3279
|
+
description: "Read-only. Gets one routine folder by folder_id. Use get-routine-folders to discover IDs.",
|
|
3200
3280
|
inputSchema: getRoutineFolderSchema,
|
|
3201
3281
|
outputSchema: routineFolderResponse.outputSchema,
|
|
3202
3282
|
annotations: readOnlyAnnotations("Get Routine Folder"),
|
|
3203
3283
|
kind: "read",
|
|
3204
3284
|
responseContract: routineFolderResponse,
|
|
3205
3285
|
execute: async (runtime, args) => {
|
|
3206
|
-
const {
|
|
3286
|
+
const { folder_id } = args;
|
|
3207
3287
|
try {
|
|
3208
3288
|
return {
|
|
3209
|
-
|
|
3210
|
-
|
|
3289
|
+
routine_folder: await runtime.getClient().getRoutineFolder(folder_id),
|
|
3290
|
+
folder_id
|
|
3211
3291
|
};
|
|
3212
3292
|
} catch (error) {
|
|
3213
3293
|
if (isExpectedReadNotFound(error)) return {
|
|
3214
|
-
|
|
3215
|
-
|
|
3294
|
+
routine_folder: null,
|
|
3295
|
+
folder_id,
|
|
3216
3296
|
expected404Outcome: "not_found"
|
|
3217
3297
|
};
|
|
3218
3298
|
throw error;
|
|
@@ -3223,169 +3303,109 @@ const folderToolDefinitions = [
|
|
|
3223
3303
|
name: "create-routine-folder",
|
|
3224
3304
|
feature: "folders",
|
|
3225
3305
|
operation: "create",
|
|
3226
|
-
description:
|
|
3227
|
-
summary: "Writes to the Hevy account by creating a new routine folder.",
|
|
3228
|
-
aliases: [
|
|
3229
|
-
"add folder",
|
|
3230
|
-
"organize routines",
|
|
3231
|
-
"create plan group"
|
|
3232
|
-
],
|
|
3233
|
-
useCase: "Use to create an organizational destination before assigning new routines to a folderId.",
|
|
3234
|
-
importantNotes: "Requires a non-empty name. Retrying or reusing a name can create duplicate folders."
|
|
3235
|
-
}),
|
|
3306
|
+
description: "Writes a routine folder. Retries or reused titles can create duplicates.",
|
|
3236
3307
|
inputSchema: createRoutineFolderSchema,
|
|
3237
3308
|
annotations: createAnnotations("Create Routine Folder"),
|
|
3238
3309
|
kind: "write",
|
|
3239
3310
|
responseContract: createRoutineFolderResponse,
|
|
3240
3311
|
execute: async (runtime, args) => {
|
|
3241
|
-
return runtime.getClient().createRoutineFolder(
|
|
3312
|
+
return runtime.getClient().createRoutineFolder(args);
|
|
3242
3313
|
}
|
|
3243
3314
|
}
|
|
3244
3315
|
];
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
page,
|
|
3279
|
-
pageCount: data?.page_count
|
|
3280
|
-
};
|
|
3281
|
-
} catch (error) {
|
|
3282
|
-
if (isExpectedListPageNotFound(error, page)) return {
|
|
3283
|
-
items: [],
|
|
3284
|
-
page,
|
|
3285
|
-
expected404Outcome: "end_of_list"
|
|
3286
|
-
};
|
|
3287
|
-
throw error;
|
|
3316
|
+
const routineToolDefinitions = [
|
|
3317
|
+
{
|
|
3318
|
+
name: "get-routines",
|
|
3319
|
+
feature: "routines",
|
|
3320
|
+
operation: "list",
|
|
3321
|
+
description: "Read-only. Lists compact routine summaries. Use get-routine for exercises and sets; results are paginated.",
|
|
3322
|
+
inputSchema: paginationShape({
|
|
3323
|
+
defaultPageSize: 5,
|
|
3324
|
+
maxPageSize: 10
|
|
3325
|
+
}),
|
|
3326
|
+
kind: "read",
|
|
3327
|
+
outputSchema: routinesResponse.outputSchema,
|
|
3328
|
+
annotations: readOnlyAnnotations("Get Routines"),
|
|
3329
|
+
responseContract: routinesResponse,
|
|
3330
|
+
execute: async (runtime, { page, page_size }) => {
|
|
3331
|
+
try {
|
|
3332
|
+
const data = await runtime.getClient().getRoutines({
|
|
3333
|
+
page,
|
|
3334
|
+
pageSize: page_size
|
|
3335
|
+
});
|
|
3336
|
+
return {
|
|
3337
|
+
items: data?.routines ?? [],
|
|
3338
|
+
page,
|
|
3339
|
+
pageCount: data?.page_count
|
|
3340
|
+
};
|
|
3341
|
+
} catch (error) {
|
|
3342
|
+
if (isExpectedListPageNotFound(error, page)) return {
|
|
3343
|
+
items: [],
|
|
3344
|
+
page,
|
|
3345
|
+
expected404Outcome: "end_of_list"
|
|
3346
|
+
};
|
|
3347
|
+
throw error;
|
|
3348
|
+
}
|
|
3288
3349
|
}
|
|
3289
|
-
}
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
3350
|
+
},
|
|
3351
|
+
{
|
|
3352
|
+
name: "get-routine",
|
|
3353
|
+
feature: "routines",
|
|
3354
|
+
operation: "get",
|
|
3355
|
+
description: "Read-only. Gets one routine with exercises and sets by routine_id. Use search-routines to discover IDs.",
|
|
3356
|
+
inputSchema: { routine_id: nonEmptyId },
|
|
3357
|
+
kind: "read",
|
|
3358
|
+
outputSchema: routineResponse.outputSchema,
|
|
3359
|
+
annotations: readOnlyAnnotations("Get Routine"),
|
|
3360
|
+
responseContract: routineResponse,
|
|
3361
|
+
execute: async (runtime, { routine_id }) => {
|
|
3362
|
+
try {
|
|
3363
|
+
return {
|
|
3364
|
+
routine: (await runtime.getClient().getRoutineById(String(routine_id)))?.routine,
|
|
3365
|
+
routine_id
|
|
3366
|
+
};
|
|
3367
|
+
} catch (error) {
|
|
3368
|
+
if (isExpectedReadNotFound(error)) return {
|
|
3369
|
+
routine: null,
|
|
3370
|
+
routine_id,
|
|
3371
|
+
expected404Outcome: "not_found"
|
|
3372
|
+
};
|
|
3373
|
+
throw error;
|
|
3374
|
+
}
|
|
3375
|
+
}
|
|
3376
|
+
},
|
|
3377
|
+
{
|
|
3378
|
+
name: "create-routine",
|
|
3379
|
+
feature: "routines",
|
|
3380
|
+
operation: "create",
|
|
3381
|
+
description: "Writes a reusable routine; use create-workout for completed sessions. Retries can create duplicates.",
|
|
3382
|
+
inputSchema: createRoutineInputShape,
|
|
3383
|
+
kind: "write",
|
|
3384
|
+
annotations: createAnnotations("Create Routine"),
|
|
3385
|
+
responseContract: createRoutineResponse,
|
|
3386
|
+
execute: async (runtime, args) => {
|
|
3387
|
+
const { payload, usesRepRanges } = buildRoutinePayload(args.routine, "create");
|
|
3313
3388
|
return {
|
|
3314
|
-
routine:
|
|
3315
|
-
|
|
3316
|
-
};
|
|
3317
|
-
} catch (error) {
|
|
3318
|
-
if (isExpectedReadNotFound(error)) return {
|
|
3319
|
-
routine: null,
|
|
3320
|
-
routineId,
|
|
3321
|
-
expected404Outcome: "not_found"
|
|
3389
|
+
routine: await runtime.getClient().createRoutine({ routine: payload }),
|
|
3390
|
+
usesRepRanges
|
|
3322
3391
|
};
|
|
3323
|
-
throw error;
|
|
3324
3392
|
}
|
|
3325
|
-
}
|
|
3326
|
-
};
|
|
3327
|
-
const routineExercisesSchema = z.preprocess(parseJsonArray, z.array(z.object(routineExerciseShape)));
|
|
3328
|
-
const createRoutineSchema = routinePayloadShape;
|
|
3329
|
-
const createRoutineDefinition = {
|
|
3330
|
-
name: "create-routine",
|
|
3331
|
-
feature: "routines",
|
|
3332
|
-
operation: "create",
|
|
3333
|
-
description: describeTool({
|
|
3334
|
-
summary: "Writes to the Hevy account by creating a new workout routine.",
|
|
3335
|
-
aliases: [
|
|
3336
|
-
"add routine",
|
|
3337
|
-
"build workout plan",
|
|
3338
|
-
"save training template"
|
|
3339
|
-
],
|
|
3340
|
-
useCase: "Use to create a reusable plan; use create-workout to log a completed session.",
|
|
3341
|
-
importantNotes: "Requires exercise template IDs; folderId is optional. Retrying can create duplicates, and non-fixed rep ranges may not display in Hevy apps."
|
|
3342
|
-
}),
|
|
3343
|
-
inputSchema: createRoutineSchema,
|
|
3344
|
-
kind: "write",
|
|
3345
|
-
annotations: createAnnotations("Create Routine"),
|
|
3346
|
-
responseContract: createRoutineResponse,
|
|
3347
|
-
execute: async (runtime, args) => {
|
|
3348
|
-
const { payload, usesRepRanges } = buildRoutinePayload(args, "create");
|
|
3349
|
-
return {
|
|
3350
|
-
routine: await runtime.getClient().createRoutine({ routine: payload }),
|
|
3351
|
-
usesRepRanges
|
|
3352
|
-
};
|
|
3353
|
-
}
|
|
3354
|
-
};
|
|
3355
|
-
const updateRoutineSchema = {
|
|
3356
|
-
routineId: nonEmptyId,
|
|
3357
|
-
title: z.string().min(1),
|
|
3358
|
-
notes: z.string().optional(),
|
|
3359
|
-
exercises: routineExercisesSchema
|
|
3360
|
-
};
|
|
3361
|
-
const routineToolDefinitions = [
|
|
3362
|
-
getRoutinesDefinition,
|
|
3363
|
-
getRoutineDefinition,
|
|
3364
|
-
createRoutineDefinition,
|
|
3393
|
+
},
|
|
3365
3394
|
{
|
|
3366
3395
|
name: "update-routine",
|
|
3367
3396
|
feature: "routines",
|
|
3368
3397
|
operation: "update",
|
|
3369
|
-
description:
|
|
3370
|
-
|
|
3371
|
-
aliases: [
|
|
3372
|
-
"edit routine",
|
|
3373
|
-
"revise workout plan",
|
|
3374
|
-
"replace routine exercises"
|
|
3375
|
-
],
|
|
3376
|
-
useCase: "Use to change a known routine; use create-routine for a separate new plan.",
|
|
3377
|
-
importantNotes: "Requires routineId and the complete title and exercises payload; omitted exercises are removed. Non-fixed rep ranges may not display in Hevy apps."
|
|
3378
|
-
}),
|
|
3379
|
-
inputSchema: updateRoutineSchema,
|
|
3398
|
+
description: "Mutates a routine by replacing its title and exercises. Omitted exercises are removed.",
|
|
3399
|
+
inputSchema: updateRoutineInputShape,
|
|
3380
3400
|
kind: "write",
|
|
3381
3401
|
annotations: updateAnnotations("Update Routine"),
|
|
3382
3402
|
responseContract: updateRoutineResponse,
|
|
3383
3403
|
execute: async (runtime, args) => {
|
|
3384
|
-
const {
|
|
3385
|
-
const { payload, usesRepRanges } = buildRoutinePayload(args, "update");
|
|
3404
|
+
const { routine_id } = args;
|
|
3405
|
+
const { payload, usesRepRanges } = buildRoutinePayload(args.routine, "update");
|
|
3386
3406
|
return {
|
|
3387
|
-
routine: await runtime.getClient().updateRoutine(
|
|
3388
|
-
|
|
3407
|
+
routine: await runtime.getClient().updateRoutine(routine_id, { routine: payload }),
|
|
3408
|
+
routine_id,
|
|
3389
3409
|
usesRepRanges
|
|
3390
3410
|
};
|
|
3391
3411
|
}
|
|
@@ -3397,17 +3417,19 @@ const getExerciseTemplatesSchema = paginationShape({
|
|
|
3397
3417
|
defaultPageSize: 5,
|
|
3398
3418
|
maxPageSize: 100
|
|
3399
3419
|
});
|
|
3400
|
-
const getExerciseTemplateSchema = {
|
|
3420
|
+
const getExerciseTemplateSchema = { exercise_template_id: nonEmptyId };
|
|
3421
|
+
const isoDateTimeBase = z.iso.datetime({ offset: true });
|
|
3422
|
+
const isoDateTimeWithOffset = z.string().refine((value) => isoDateTimeBase.safeParse(value).success, "Must be an ISO 8601 timestamp with an offset").meta({ format: "date-time" });
|
|
3401
3423
|
const getExerciseHistorySchema = {
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
|
|
3424
|
+
exercise_template_id: nonEmptyId,
|
|
3425
|
+
start_date: isoDateTimeWithOffset.optional(),
|
|
3426
|
+
end_date: isoDateTimeWithOffset.optional()
|
|
3405
3427
|
};
|
|
3406
3428
|
const createExerciseTemplateSchema = exerciseTemplateInputShape;
|
|
3407
3429
|
const searchExerciseTemplatesSchema = {
|
|
3408
|
-
query: z.string().min(1)
|
|
3409
|
-
|
|
3410
|
-
refresh: z.boolean().optional().default(false)
|
|
3430
|
+
query: z.string().min(1),
|
|
3431
|
+
primary_muscle_group: muscleGroupEnum.optional(),
|
|
3432
|
+
refresh: z.boolean().optional().default(false)
|
|
3411
3433
|
};
|
|
3412
3434
|
/** Ordered exercise-template tools for composition by the shared server. */
|
|
3413
3435
|
const templateToolDefinitions = [
|
|
@@ -3415,27 +3437,18 @@ const templateToolDefinitions = [
|
|
|
3415
3437
|
name: "get-exercise-templates",
|
|
3416
3438
|
feature: "templates",
|
|
3417
3439
|
operation: "list",
|
|
3418
|
-
description:
|
|
3419
|
-
summary: "Read-only. Lists default and custom exercise templates with equipment and muscle metadata.",
|
|
3420
|
-
aliases: [
|
|
3421
|
-
"browse exercises",
|
|
3422
|
-
"list exercise catalog",
|
|
3423
|
-
"show movements"
|
|
3424
|
-
],
|
|
3425
|
-
useCase: "Use for page-by-page catalog browsing; use search-exercise-templates for a name lookup across the full catalog.",
|
|
3426
|
-
importantNotes: "Results are paginated; page starts at 1 and pageSize is limited to 100."
|
|
3427
|
-
}),
|
|
3440
|
+
description: "Read-only. Pages through exercise templates. Use search-exercise-templates when a title is known.",
|
|
3428
3441
|
inputSchema: getExerciseTemplatesSchema,
|
|
3429
3442
|
outputSchema: exerciseTemplatesResponse.outputSchema,
|
|
3430
3443
|
annotations: readOnlyAnnotations("Get Exercise Templates"),
|
|
3431
3444
|
kind: "read",
|
|
3432
3445
|
responseContract: exerciseTemplatesResponse,
|
|
3433
3446
|
execute: async (runtime, args) => {
|
|
3434
|
-
const { page,
|
|
3447
|
+
const { page, page_size } = args;
|
|
3435
3448
|
try {
|
|
3436
3449
|
const data = await runtime.getClient().getExerciseTemplates({
|
|
3437
3450
|
page,
|
|
3438
|
-
pageSize
|
|
3451
|
+
pageSize: page_size
|
|
3439
3452
|
});
|
|
3440
3453
|
return {
|
|
3441
3454
|
items: data?.exercise_templates ?? [],
|
|
@@ -3456,32 +3469,23 @@ const templateToolDefinitions = [
|
|
|
3456
3469
|
name: "get-exercise-template",
|
|
3457
3470
|
feature: "templates",
|
|
3458
3471
|
operation: "get",
|
|
3459
|
-
description:
|
|
3460
|
-
summary: "Read-only. Retrieves complete metadata for one exercise template by ID.",
|
|
3461
|
-
aliases: [
|
|
3462
|
-
"show exercise details",
|
|
3463
|
-
"fetch movement",
|
|
3464
|
-
"exercise template info"
|
|
3465
|
-
],
|
|
3466
|
-
useCase: "Use after locating an exact template; use search-exercise-templates when only a name is known.",
|
|
3467
|
-
importantNotes: "Requires an exerciseTemplateId from a template list, search, routine, or workout."
|
|
3468
|
-
}),
|
|
3472
|
+
description: "Read-only. Gets one exercise template by exercise_template_id. Use search-exercise-templates to discover IDs.",
|
|
3469
3473
|
inputSchema: getExerciseTemplateSchema,
|
|
3470
3474
|
outputSchema: exerciseTemplateResponse.outputSchema,
|
|
3471
3475
|
annotations: readOnlyAnnotations("Get Exercise Template"),
|
|
3472
3476
|
kind: "read",
|
|
3473
3477
|
responseContract: exerciseTemplateResponse,
|
|
3474
3478
|
execute: async (runtime, args) => {
|
|
3475
|
-
const {
|
|
3479
|
+
const { exercise_template_id } = args;
|
|
3476
3480
|
try {
|
|
3477
3481
|
return {
|
|
3478
|
-
|
|
3479
|
-
|
|
3482
|
+
exercise_template: await runtime.getClient().getExerciseTemplate(exercise_template_id),
|
|
3483
|
+
exercise_template_id
|
|
3480
3484
|
};
|
|
3481
3485
|
} catch (error) {
|
|
3482
3486
|
if (isExpectedReadNotFound(error)) return {
|
|
3483
|
-
|
|
3484
|
-
|
|
3487
|
+
exercise_template: null,
|
|
3488
|
+
exercise_template_id,
|
|
3485
3489
|
expected404Outcome: "not_found"
|
|
3486
3490
|
};
|
|
3487
3491
|
throw error;
|
|
@@ -3492,29 +3496,20 @@ const templateToolDefinitions = [
|
|
|
3492
3496
|
name: "get-exercise-history",
|
|
3493
3497
|
feature: "templates",
|
|
3494
3498
|
operation: "get",
|
|
3495
|
-
description:
|
|
3496
|
-
summary: "Read-only. Retrieves past performed sets for one exercise template.",
|
|
3497
|
-
aliases: [
|
|
3498
|
-
"exercise progress",
|
|
3499
|
-
"past sets",
|
|
3500
|
-
"movement history"
|
|
3501
|
-
],
|
|
3502
|
-
useCase: "Use to analyze performance for one movement; use get-workouts for complete sessions.",
|
|
3503
|
-
importantNotes: "Requires an exerciseTemplateId. Optional startDate and endDate must be ISO 8601 datetimes with an offset."
|
|
3504
|
-
}),
|
|
3499
|
+
description: "Read-only. Returns performed sets for one exercise-template ID, optionally bounded by ISO 8601 timestamps.",
|
|
3505
3500
|
inputSchema: getExerciseHistorySchema,
|
|
3506
3501
|
outputSchema: exerciseHistoryResponse.outputSchema,
|
|
3507
3502
|
annotations: readOnlyAnnotations("Get Exercise History"),
|
|
3508
3503
|
kind: "read",
|
|
3509
3504
|
responseContract: exerciseHistoryResponse,
|
|
3510
3505
|
execute: async (runtime, args) => {
|
|
3511
|
-
const {
|
|
3506
|
+
const { exercise_template_id, start_date, end_date } = args;
|
|
3512
3507
|
return {
|
|
3513
|
-
|
|
3514
|
-
...
|
|
3515
|
-
...
|
|
3508
|
+
exercise_history: (await runtime.getClient().getExerciseHistory(exercise_template_id, {
|
|
3509
|
+
...start_date ? { start_date } : {},
|
|
3510
|
+
...end_date ? { end_date } : {}
|
|
3516
3511
|
}))?.exercise_history,
|
|
3517
|
-
|
|
3512
|
+
exercise_template_id
|
|
3518
3513
|
};
|
|
3519
3514
|
}
|
|
3520
3515
|
},
|
|
@@ -3522,38 +3517,20 @@ const templateToolDefinitions = [
|
|
|
3522
3517
|
name: "create-exercise-template",
|
|
3523
3518
|
feature: "templates",
|
|
3524
3519
|
operation: "create",
|
|
3525
|
-
description:
|
|
3526
|
-
summary: "Writes to the Hevy account by creating a custom exercise template.",
|
|
3527
|
-
aliases: [
|
|
3528
|
-
"add custom exercise",
|
|
3529
|
-
"create movement",
|
|
3530
|
-
"define exercise"
|
|
3531
|
-
],
|
|
3532
|
-
useCase: "Use only when the needed movement is absent; search-exercise-templates should check existing templates first.",
|
|
3533
|
-
importantNotes: "Requires title, exercise type, equipment category, and primary muscle group. Retrying or reusing a title can create duplicates."
|
|
3534
|
-
}),
|
|
3520
|
+
description: "Writes a custom exercise template. Search first; retries or reused titles can create duplicates.",
|
|
3535
3521
|
inputSchema: createExerciseTemplateSchema,
|
|
3536
3522
|
annotations: createAnnotations("Create Exercise Template"),
|
|
3537
3523
|
kind: "write",
|
|
3538
3524
|
responseContract: createExerciseTemplateResponse,
|
|
3539
3525
|
execute: async (runtime, args) => {
|
|
3540
|
-
return runtime.getClient().createExerciseTemplate(
|
|
3526
|
+
return runtime.getClient().createExerciseTemplate(args);
|
|
3541
3527
|
}
|
|
3542
3528
|
},
|
|
3543
3529
|
{
|
|
3544
3530
|
name: "search-exercise-templates",
|
|
3545
3531
|
feature: "templates",
|
|
3546
3532
|
operation: "search",
|
|
3547
|
-
description:
|
|
3548
|
-
summary: "Read-only for the Hevy account. Searches the full exercise template catalog by title substring.",
|
|
3549
|
-
aliases: [
|
|
3550
|
-
"find exercise",
|
|
3551
|
-
"look up movement",
|
|
3552
|
-
"search exercise IDs"
|
|
3553
|
-
],
|
|
3554
|
-
useCase: "Use when a name or partial name is known, especially to discover IDs for workouts and routines; use get-exercise-templates for page browsing.",
|
|
3555
|
-
importantNotes: "Matching is case-insensitive. The catalog is cached locally for 5 minutes; refresh:true re-fetches all pages and changes only local cache state."
|
|
3556
|
-
}),
|
|
3533
|
+
description: "Read-only. Searches template titles case-insensitively and returns IDs. refresh reloads the five-minute catalog cache.",
|
|
3557
3534
|
inputSchema: searchExerciseTemplatesSchema,
|
|
3558
3535
|
outputSchema: searchExerciseTemplatesResponse.outputSchema,
|
|
3559
3536
|
annotations: readOnlyAnnotations("Search Exercise Templates"),
|
|
@@ -3561,7 +3538,7 @@ const templateToolDefinitions = [
|
|
|
3561
3538
|
responseContract: searchExerciseTemplatesResponse,
|
|
3562
3539
|
execute: async (runtime, args) => {
|
|
3563
3540
|
runtime.getClient();
|
|
3564
|
-
const { query,
|
|
3541
|
+
const { query, primary_muscle_group, refresh } = args;
|
|
3565
3542
|
const templates = await runtime.catalog.get({
|
|
3566
3543
|
refresh,
|
|
3567
3544
|
onRefreshed: (refreshedCatalog, reason) => {
|
|
@@ -3582,11 +3559,11 @@ const templateToolDefinitions = [
|
|
|
3582
3559
|
});
|
|
3583
3560
|
const queryLower = query.toLowerCase();
|
|
3584
3561
|
let results = templates.filter((t) => (t.title ?? "").toLowerCase().includes(queryLower));
|
|
3585
|
-
if (
|
|
3562
|
+
if (primary_muscle_group !== void 0) results = results.filter((t) => t.primary_muscle_group === primary_muscle_group);
|
|
3586
3563
|
return {
|
|
3587
3564
|
results,
|
|
3588
3565
|
query,
|
|
3589
|
-
|
|
3566
|
+
primary_muscle_group
|
|
3590
3567
|
};
|
|
3591
3568
|
}
|
|
3592
3569
|
}
|
|
@@ -3595,16 +3572,7 @@ const userToolDefinitions = [{
|
|
|
3595
3572
|
name: "get-user-info",
|
|
3596
3573
|
feature: "profile",
|
|
3597
3574
|
operation: "get",
|
|
3598
|
-
description:
|
|
3599
|
-
summary: "Read-only. Returns the authenticated account's user ID, display name, and public profile URL.",
|
|
3600
|
-
aliases: [
|
|
3601
|
-
"who am I",
|
|
3602
|
-
"account info",
|
|
3603
|
-
"verify Hevy user"
|
|
3604
|
-
],
|
|
3605
|
-
useCase: "Use to confirm which Hevy account is connected before reading or writing account data.",
|
|
3606
|
-
importantNotes: "Accepts no inputs and reports only the account associated with the configured credentials."
|
|
3607
|
-
}),
|
|
3575
|
+
description: "Read-only. Returns the authenticated Hevy user ID, display name, and public profile URL.",
|
|
3608
3576
|
inputSchema: {},
|
|
3609
3577
|
kind: "read",
|
|
3610
3578
|
outputSchema: userResponse.outputSchema,
|
|
@@ -3617,8 +3585,8 @@ const userToolDefinitions = [{
|
|
|
3617
3585
|
//#endregion
|
|
3618
3586
|
//#region ../core/src/tools/routine-discovery.ts
|
|
3619
3587
|
const routineDiscoverySchema = {
|
|
3620
|
-
query: z.string().min(1).optional()
|
|
3621
|
-
limit: z.coerce.number().int().min(1).max(100).default(20)
|
|
3588
|
+
query: z.string().min(1).optional(),
|
|
3589
|
+
limit: z.coerce.number().int().min(1).max(100).default(20)
|
|
3622
3590
|
};
|
|
3623
3591
|
async function discoverRoutines(runtime, { query, limit }) {
|
|
3624
3592
|
const normalizedQuery = query?.toLocaleLowerCase();
|
|
@@ -3645,14 +3613,7 @@ async function discoverRoutines(runtime, { query, limit }) {
|
|
|
3645
3613
|
page += 1;
|
|
3646
3614
|
}
|
|
3647
3615
|
return {
|
|
3648
|
-
routines: routines.slice(0, limit).map(
|
|
3649
|
-
...routine.id ? { id: routine.id } : {},
|
|
3650
|
-
...routine.title ? { title: routine.title } : {},
|
|
3651
|
-
folderId: routine.folder_id ?? null,
|
|
3652
|
-
...routine.updated_at ? { updatedAt: routine.updated_at } : {},
|
|
3653
|
-
exerciseCount: routine.exercises?.length ?? 0,
|
|
3654
|
-
setCount: routine.exercises?.reduce((total, exercise) => total + (exercise.sets?.length ?? 0), 0) ?? 0
|
|
3655
|
-
})),
|
|
3616
|
+
routines: routines.slice(0, limit).map(summarizeRoutine),
|
|
3656
3617
|
workflow: {
|
|
3657
3618
|
name: "routine-discovery",
|
|
3658
3619
|
pagination: { routines: pages },
|
|
@@ -3665,16 +3626,7 @@ const routineDiscoveryToolDefinitions = [{
|
|
|
3665
3626
|
name: "search-routines",
|
|
3666
3627
|
feature: "workflows",
|
|
3667
3628
|
operation: "search",
|
|
3668
|
-
description:
|
|
3669
|
-
summary: "Read-only. Discovers routines by title and returns compact metadata without full set payloads.",
|
|
3670
|
-
aliases: [
|
|
3671
|
-
"find routine",
|
|
3672
|
-
"browse routine names",
|
|
3673
|
-
"compact routine list"
|
|
3674
|
-
],
|
|
3675
|
-
useCase: "Use to find a routine ID or shortlist plans before calling get-routine for full exercise details.",
|
|
3676
|
-
importantNotes: "Search scans routine pages at pageSize 10 and returns only IDs, titles, folder metadata, and exercise/set counts."
|
|
3677
|
-
}),
|
|
3629
|
+
description: "Read-only. Searches routine titles and returns compact IDs and counts. Use get-routine for full exercises and sets.",
|
|
3678
3630
|
inputSchema: routineDiscoverySchema,
|
|
3679
3631
|
outputSchema: compactRoutinesResponse.outputSchema,
|
|
3680
3632
|
annotations: readOnlyAnnotations("Search Routines"),
|
|
@@ -3684,7 +3636,7 @@ const routineDiscoveryToolDefinitions = [{
|
|
|
3684
3636
|
}];
|
|
3685
3637
|
//#endregion
|
|
3686
3638
|
//#region ../core/src/tools/workflows.ts
|
|
3687
|
-
const trainingSummarySchema = { weeks: z.coerce.number().int().min(1).max(12).default(4)
|
|
3639
|
+
const trainingSummarySchema = { weeks: z.coerce.number().int().min(1).max(12).default(4) };
|
|
3688
3640
|
const MILLISECONDS_PER_DAY = 1440 * 60 * 1e3;
|
|
3689
3641
|
function parseUtcDate(value) {
|
|
3690
3642
|
const normalized = value.length === 10 ? `${value}T00:00:00.000Z` : value;
|
|
@@ -3738,28 +3690,29 @@ function getPeriod(weeks) {
|
|
|
3738
3690
|
};
|
|
3739
3691
|
}
|
|
3740
3692
|
function durationSeconds(workout) {
|
|
3741
|
-
if (!workout.start_time || !workout.end_time) return
|
|
3693
|
+
if (!workout.start_time || !workout.end_time) return void 0;
|
|
3742
3694
|
const duration = new Date(workout.end_time).getTime() - new Date(workout.start_time).getTime();
|
|
3743
|
-
return Number.isFinite(duration) && duration >= 0 ? Math.floor(duration / 1e3) :
|
|
3695
|
+
return Number.isFinite(duration) && duration >= 0 ? Math.floor(duration / 1e3) : void 0;
|
|
3744
3696
|
}
|
|
3745
3697
|
function compactSession(workout) {
|
|
3746
3698
|
const exercises = workout.exercises ?? [];
|
|
3699
|
+
const elapsedSeconds = durationSeconds(workout);
|
|
3747
3700
|
return {
|
|
3748
3701
|
...workout.id ? { id: workout.id } : {},
|
|
3749
3702
|
...workout.title ? { title: workout.title } : {},
|
|
3750
|
-
...workout.start_time ? {
|
|
3751
|
-
...workout.end_time ? {
|
|
3752
|
-
|
|
3753
|
-
|
|
3754
|
-
|
|
3703
|
+
...workout.start_time ? { start_time: workout.start_time } : {},
|
|
3704
|
+
...workout.end_time ? { end_time: workout.end_time } : {},
|
|
3705
|
+
...elapsedSeconds === void 0 ? {} : { duration_seconds: elapsedSeconds },
|
|
3706
|
+
exercise_count: exercises.length,
|
|
3707
|
+
set_count: exercises.reduce((total, exercise) => total + (exercise.sets?.length ?? 0), 0)
|
|
3755
3708
|
};
|
|
3756
3709
|
}
|
|
3757
3710
|
function compactMeasurement(measurement) {
|
|
3758
3711
|
return {
|
|
3759
3712
|
date: measurement.date,
|
|
3760
|
-
|
|
3761
|
-
|
|
3762
|
-
|
|
3713
|
+
...measurement.weight_kg == null ? {} : { weight_kg: measurement.weight_kg },
|
|
3714
|
+
...measurement.lean_mass_kg == null ? {} : { lean_mass_kg: measurement.lean_mass_kg },
|
|
3715
|
+
...measurement.fat_percent == null ? {} : { fat_percent: measurement.fat_percent }
|
|
3763
3716
|
};
|
|
3764
3717
|
}
|
|
3765
3718
|
async function getTrainingSummary(runtime, weeks) {
|
|
@@ -3787,37 +3740,38 @@ async function getTrainingSummary(runtime, weeks) {
|
|
|
3787
3740
|
}, pageSize, period.startDate, period.endDate, (measurement) => measurement.date)]);
|
|
3788
3741
|
const workouts = workoutPages.items;
|
|
3789
3742
|
const sessions = workouts.map(compactSession);
|
|
3790
|
-
const
|
|
3743
|
+
const unique_exercise_template_ids = [...new Set(workouts.flatMap((workout) => (workout.exercises ?? []).map((exercise) => exercise.exercise_template_id).filter((id) => Boolean(id))))];
|
|
3791
3744
|
const measurements = [...measurementPages.items].sort((a, b) => a.date.localeCompare(b.date));
|
|
3792
3745
|
const earliestMeasurement = measurements[0];
|
|
3793
3746
|
const latestMeasurement = measurements.at(-1);
|
|
3794
|
-
const earliest = earliestMeasurement ? compactMeasurement(earliestMeasurement) :
|
|
3795
|
-
const latest = latestMeasurement ? compactMeasurement(latestMeasurement) :
|
|
3796
|
-
const
|
|
3747
|
+
const earliest = earliestMeasurement ? compactMeasurement(earliestMeasurement) : void 0;
|
|
3748
|
+
const latest = latestMeasurement ? compactMeasurement(latestMeasurement) : void 0;
|
|
3749
|
+
const weight_change_kg = latest?.weight_kg !== void 0 && earliest?.weight_kg !== void 0 ? latest.weight_kg - earliest.weight_kg : void 0;
|
|
3797
3750
|
return {
|
|
3798
3751
|
period: {
|
|
3799
|
-
|
|
3752
|
+
start_date: period.startDate,
|
|
3753
|
+
end_date: period.endDate,
|
|
3800
3754
|
weeks
|
|
3801
3755
|
},
|
|
3802
3756
|
workouts: {
|
|
3803
3757
|
count: workouts.length,
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
-
|
|
3807
|
-
|
|
3758
|
+
total_duration_seconds: sessions.reduce((total, session) => total + (session.duration_seconds ?? 0), 0),
|
|
3759
|
+
exercise_count: sessions.reduce((total, session) => total + session.exercise_count, 0),
|
|
3760
|
+
set_count: sessions.reduce((total, session) => total + session.set_count, 0),
|
|
3761
|
+
unique_exercise_template_ids,
|
|
3808
3762
|
sessions
|
|
3809
3763
|
},
|
|
3810
|
-
|
|
3764
|
+
body_measurements: {
|
|
3811
3765
|
count: measurements.length,
|
|
3812
|
-
latest,
|
|
3813
|
-
earliest,
|
|
3814
|
-
|
|
3766
|
+
...latest ? { latest } : {},
|
|
3767
|
+
...earliest ? { earliest } : {},
|
|
3768
|
+
...weight_change_kg === void 0 ? {} : { weight_change_kg }
|
|
3815
3769
|
},
|
|
3816
3770
|
workflow: {
|
|
3817
3771
|
name: "training-summary",
|
|
3818
3772
|
pagination: {
|
|
3819
3773
|
workouts: workoutPages.pages,
|
|
3820
|
-
|
|
3774
|
+
body_measurements: measurementPages.pages
|
|
3821
3775
|
},
|
|
3822
3776
|
cacheStatus: "not-used",
|
|
3823
3777
|
itemsScanned: workoutPages.itemsScanned + measurementPages.itemsScanned
|
|
@@ -3828,16 +3782,7 @@ const workflowToolDefinitions = [{
|
|
|
3828
3782
|
name: "get-training-summary",
|
|
3829
3783
|
feature: "workflows",
|
|
3830
3784
|
operation: "get",
|
|
3831
|
-
description:
|
|
3832
|
-
summary: "Read-only. Summarizes recent workout activity and body-measurement trends in one call.",
|
|
3833
|
-
aliases: [
|
|
3834
|
-
"training progress",
|
|
3835
|
-
"progress summary",
|
|
3836
|
-
"recent training overview"
|
|
3837
|
-
],
|
|
3838
|
-
useCase: "Use for a bounded progress review instead of separately counting and paging through workouts and body measurements.",
|
|
3839
|
-
importantNotes: "The summary covers the most recent 1-12 weeks, returns compact session evidence, and reports the pages and items scanned."
|
|
3840
|
-
}),
|
|
3785
|
+
description: "Read-only. Summarizes workouts and body-measurement trends for the last 1–12 weeks, including compact session and scan evidence.",
|
|
3841
3786
|
inputSchema: trainingSummarySchema,
|
|
3842
3787
|
outputSchema: trainingSummaryResponse.outputSchema,
|
|
3843
3788
|
annotations: readOnlyAnnotations("Get Training Summary"),
|
|
@@ -3852,7 +3797,7 @@ const getWorkoutsSchema = paginationShape({
|
|
|
3852
3797
|
maxPageSize: 10,
|
|
3853
3798
|
integerPage: false
|
|
3854
3799
|
});
|
|
3855
|
-
const getWorkoutSchema = {
|
|
3800
|
+
const getWorkoutSchema = { workout_id: nonEmptyId };
|
|
3856
3801
|
const getWorkoutEventsSchema = {
|
|
3857
3802
|
...paginationShape({
|
|
3858
3803
|
defaultPageSize: 5,
|
|
@@ -3860,11 +3805,9 @@ const getWorkoutEventsSchema = {
|
|
|
3860
3805
|
}),
|
|
3861
3806
|
since: z.string().default("1970-01-01T00:00:00Z")
|
|
3862
3807
|
};
|
|
3863
|
-
const createWorkoutSchema =
|
|
3864
|
-
const updateWorkoutSchema =
|
|
3865
|
-
|
|
3866
|
-
...workoutPayloadShape
|
|
3867
|
-
};
|
|
3808
|
+
const createWorkoutSchema = workoutInputShape;
|
|
3809
|
+
const updateWorkoutSchema = updateWorkoutInputShape;
|
|
3810
|
+
const replaceWorkoutExercisesSchema = replaceWorkoutExercisesInputShape;
|
|
3868
3811
|
//#endregion
|
|
3869
3812
|
//#region ../core/src/tools/register.ts
|
|
3870
3813
|
const hevyToolDefinitions = [
|
|
@@ -3873,16 +3816,7 @@ const hevyToolDefinitions = [
|
|
|
3873
3816
|
name: "get-workouts",
|
|
3874
3817
|
feature: "workouts",
|
|
3875
3818
|
operation: "list",
|
|
3876
|
-
description:
|
|
3877
|
-
summary: "Read-only. Lists workouts from newest to oldest with exercise and timing details.",
|
|
3878
|
-
aliases: [
|
|
3879
|
-
"list workout history",
|
|
3880
|
-
"show recent workouts",
|
|
3881
|
-
"browse logs"
|
|
3882
|
-
],
|
|
3883
|
-
useCase: "Use to browse or page through workout history; use get-workout when a workout ID is already known.",
|
|
3884
|
-
importantNotes: "Results are paginated; page starts at 1 and pageSize is limited to 10."
|
|
3885
|
-
}),
|
|
3819
|
+
description: "Read-only. Lists compact workout summaries newest first. Use get-workout for exercises and sets; results are paginated.",
|
|
3886
3820
|
inputSchema: getWorkoutsSchema,
|
|
3887
3821
|
outputSchema: workoutsResponse.outputSchema,
|
|
3888
3822
|
annotations: readOnlyAnnotations("Get Workouts"),
|
|
@@ -3892,7 +3826,7 @@ const hevyToolDefinitions = [
|
|
|
3892
3826
|
try {
|
|
3893
3827
|
const data = await runtime.getClient().getWorkouts({
|
|
3894
3828
|
page: args.page,
|
|
3895
|
-
pageSize: args.
|
|
3829
|
+
pageSize: args.page_size
|
|
3896
3830
|
});
|
|
3897
3831
|
return {
|
|
3898
3832
|
items: data?.workouts ?? [],
|
|
@@ -3913,16 +3847,7 @@ const hevyToolDefinitions = [
|
|
|
3913
3847
|
name: "get-workout",
|
|
3914
3848
|
feature: "workouts",
|
|
3915
3849
|
operation: "get",
|
|
3916
|
-
description:
|
|
3917
|
-
summary: "Read-only. Retrieves complete details for one workout by its ID.",
|
|
3918
|
-
aliases: [
|
|
3919
|
-
"show workout",
|
|
3920
|
-
"fetch workout details",
|
|
3921
|
-
"open workout log"
|
|
3922
|
-
],
|
|
3923
|
-
useCase: "Use after get-workouts identifies the exact workout; do not use for browsing multiple workouts.",
|
|
3924
|
-
importantNotes: "Requires a workoutId discovered from a workout list, event, or prior create response."
|
|
3925
|
-
}),
|
|
3850
|
+
description: "Read-only. Gets one workout with exercises and sets by workout_id. Use get-workouts to discover IDs.",
|
|
3926
3851
|
inputSchema: getWorkoutSchema,
|
|
3927
3852
|
outputSchema: workoutResponse.outputSchema,
|
|
3928
3853
|
annotations: readOnlyAnnotations("Get Workout"),
|
|
@@ -3931,13 +3856,13 @@ const hevyToolDefinitions = [
|
|
|
3931
3856
|
execute: async (runtime, args) => {
|
|
3932
3857
|
try {
|
|
3933
3858
|
return {
|
|
3934
|
-
workout: await runtime.getClient().getWorkout(args.
|
|
3935
|
-
|
|
3859
|
+
workout: await runtime.getClient().getWorkout(args.workout_id),
|
|
3860
|
+
workout_id: args.workout_id
|
|
3936
3861
|
};
|
|
3937
3862
|
} catch (error) {
|
|
3938
3863
|
if (isExpectedReadNotFound(error)) return {
|
|
3939
3864
|
workout: null,
|
|
3940
|
-
|
|
3865
|
+
workout_id: args.workout_id,
|
|
3941
3866
|
expected404Outcome: "not_found"
|
|
3942
3867
|
};
|
|
3943
3868
|
throw error;
|
|
@@ -3948,16 +3873,7 @@ const hevyToolDefinitions = [
|
|
|
3948
3873
|
name: "get-workout-count",
|
|
3949
3874
|
feature: "workouts",
|
|
3950
3875
|
operation: "count",
|
|
3951
|
-
description:
|
|
3952
|
-
summary: "Read-only. Returns the total workout count for the account.",
|
|
3953
|
-
aliases: [
|
|
3954
|
-
"count workouts",
|
|
3955
|
-
"how many workouts",
|
|
3956
|
-
"workout total"
|
|
3957
|
-
],
|
|
3958
|
-
useCase: "Use for totals, statistics, or estimating pages; use get-workouts for actual workout records.",
|
|
3959
|
-
importantNotes: "Returns only a count and accepts no paging or date filters."
|
|
3960
|
-
}),
|
|
3876
|
+
description: "Read-only. Returns the total workout count; it does not return records or accept date filters.",
|
|
3961
3877
|
inputSchema: {},
|
|
3962
3878
|
outputSchema: workoutCountResponse.outputSchema,
|
|
3963
3879
|
annotations: readOnlyAnnotations("Get Workout Count"),
|
|
@@ -3971,16 +3887,7 @@ const hevyToolDefinitions = [
|
|
|
3971
3887
|
name: "get-workout-events",
|
|
3972
3888
|
feature: "workouts",
|
|
3973
3889
|
operation: "sync",
|
|
3974
|
-
description:
|
|
3975
|
-
summary: "Read-only. Lists workout update and delete events since a timestamp, newest first.",
|
|
3976
|
-
aliases: [
|
|
3977
|
-
"sync workout changes",
|
|
3978
|
-
"workout change feed",
|
|
3979
|
-
"deleted workouts"
|
|
3980
|
-
],
|
|
3981
|
-
useCase: "Use to incrementally synchronize a local workout cache; use get-workouts for the current workout list.",
|
|
3982
|
-
importantNotes: "since must be a timestamp string; events are paginated with pageSize at most 10, and the default since value reads from 1970."
|
|
3983
|
-
}),
|
|
3890
|
+
description: "Read-only. Lists workout update and deletion events since a timestamp for incremental sync; results are paginated.",
|
|
3984
3891
|
inputSchema: getWorkoutEventsSchema,
|
|
3985
3892
|
outputSchema: workoutEventsResponse.outputSchema,
|
|
3986
3893
|
annotations: readOnlyAnnotations("Get Workout Events"),
|
|
@@ -3990,7 +3897,7 @@ const hevyToolDefinitions = [
|
|
|
3990
3897
|
try {
|
|
3991
3898
|
const data = await runtime.getClient().getWorkoutEvents({
|
|
3992
3899
|
page: args.page,
|
|
3993
|
-
pageSize: args.
|
|
3900
|
+
pageSize: args.page_size,
|
|
3994
3901
|
since: args.since
|
|
3995
3902
|
});
|
|
3996
3903
|
return {
|
|
@@ -4014,46 +3921,48 @@ const hevyToolDefinitions = [
|
|
|
4014
3921
|
name: "create-workout",
|
|
4015
3922
|
feature: "workouts",
|
|
4016
3923
|
operation: "create",
|
|
4017
|
-
description:
|
|
4018
|
-
summary: "Writes to the Hevy account by creating a new workout.",
|
|
4019
|
-
aliases: [
|
|
4020
|
-
"log workout",
|
|
4021
|
-
"add workout",
|
|
4022
|
-
"record training session"
|
|
4023
|
-
],
|
|
4024
|
-
useCase: "Use to add a completed workout; use update-workout only when modifying an existing workout ID.",
|
|
4025
|
-
importantNotes: "Requires UTC startTime/endTime in YYYY-MM-DDTHH:mm:ssZ form and exercise template IDs. Retrying can create duplicates."
|
|
4026
|
-
}),
|
|
3924
|
+
description: "Writes a completed workout. Requires exercise-template IDs and UTC times. Retries can create duplicates.",
|
|
4027
3925
|
inputSchema: createWorkoutSchema,
|
|
4028
3926
|
annotations: createAnnotations("Create Workout"),
|
|
4029
3927
|
kind: "write",
|
|
4030
3928
|
responseContract: createWorkoutResponse,
|
|
4031
3929
|
execute: async (runtime, args) => {
|
|
4032
|
-
return await runtime.getClient().createWorkout({ workout:
|
|
3930
|
+
return await runtime.getClient().createWorkout({ workout: args.workout });
|
|
4033
3931
|
}
|
|
4034
3932
|
},
|
|
4035
3933
|
{
|
|
4036
3934
|
name: "update-workout",
|
|
4037
3935
|
feature: "workouts",
|
|
4038
3936
|
operation: "update",
|
|
4039
|
-
description:
|
|
4040
|
-
summary: "Mutates the Hevy account by replacing an existing workout.",
|
|
4041
|
-
aliases: [
|
|
4042
|
-
"edit workout",
|
|
4043
|
-
"correct workout log",
|
|
4044
|
-
"replace workout details"
|
|
4045
|
-
],
|
|
4046
|
-
useCase: "Use to revise a known workout; use create-workout for a new training session.",
|
|
4047
|
-
importantNotes: "Requires workoutId plus the complete title, times, privacy, exercises, and sets payload; omitted optional values may be cleared or defaulted."
|
|
4048
|
-
}),
|
|
3937
|
+
description: "Mutates workout metadata by ID. Omitted fields and all exercises remain unchanged.",
|
|
4049
3938
|
inputSchema: updateWorkoutSchema,
|
|
4050
3939
|
annotations: updateAnnotations("Update Workout"),
|
|
4051
3940
|
kind: "write",
|
|
4052
3941
|
responseContract: updateWorkoutResponse,
|
|
4053
3942
|
execute: async (runtime, args) => {
|
|
3943
|
+
const client = runtime.getClient();
|
|
3944
|
+
const payload = buildWorkoutUpdatePayload(await client.getWorkout(args.workout_id), args.workout);
|
|
4054
3945
|
return {
|
|
4055
|
-
workout: await
|
|
4056
|
-
|
|
3946
|
+
workout: await client.updateWorkout(args.workout_id, { workout: payload }),
|
|
3947
|
+
workout_id: args.workout_id
|
|
3948
|
+
};
|
|
3949
|
+
}
|
|
3950
|
+
},
|
|
3951
|
+
{
|
|
3952
|
+
name: "replace-workout-exercises",
|
|
3953
|
+
feature: "workouts",
|
|
3954
|
+
operation: "update",
|
|
3955
|
+
description: "Mutates a workout by replacing all exercises and sets. Workout metadata remains unchanged.",
|
|
3956
|
+
inputSchema: replaceWorkoutExercisesSchema,
|
|
3957
|
+
annotations: updateAnnotations("Replace Workout Exercises"),
|
|
3958
|
+
kind: "write",
|
|
3959
|
+
responseContract: updateWorkoutResponse,
|
|
3960
|
+
execute: async (runtime, args) => {
|
|
3961
|
+
const client = runtime.getClient();
|
|
3962
|
+
const payload = buildWorkoutUpdatePayload(await client.getWorkout(args.workout_id), {}, args.workout.exercises);
|
|
3963
|
+
return {
|
|
3964
|
+
workout: await client.updateWorkout(args.workout_id, { workout: payload }),
|
|
3965
|
+
workout_id: args.workout_id
|
|
4057
3966
|
};
|
|
4058
3967
|
}
|
|
4059
3968
|
}
|
|
@@ -4132,55 +4041,59 @@ function withErrorHandling(fn, context, onError) {
|
|
|
4132
4041
|
}
|
|
4133
4042
|
//#endregion
|
|
4134
4043
|
//#region ../core/src/tools/tool-runtime.ts
|
|
4135
|
-
const STRUCTURAL_ARGUMENT_KEYS =
|
|
4136
|
-
|
|
4137
|
-
|
|
4138
|
-
|
|
4139
|
-
|
|
4140
|
-
|
|
4141
|
-
|
|
4142
|
-
|
|
4143
|
-
|
|
4144
|
-
|
|
4145
|
-
|
|
4146
|
-
|
|
4147
|
-
|
|
4148
|
-
|
|
4149
|
-
|
|
4150
|
-
|
|
4151
|
-
|
|
4152
|
-
|
|
4153
|
-
|
|
4154
|
-
const PRESENCE_ARGUMENT_KEYS =
|
|
4155
|
-
|
|
4156
|
-
|
|
4157
|
-
|
|
4158
|
-
|
|
4159
|
-
|
|
4160
|
-
|
|
4161
|
-
|
|
4162
|
-
|
|
4163
|
-
|
|
4164
|
-
|
|
4165
|
-
|
|
4166
|
-
|
|
4167
|
-
const NUMERIC_ARGUMENT_KEYS =
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
|
|
4172
|
-
|
|
4173
|
-
const BOOLEAN_ARGUMENT_KEYS =
|
|
4044
|
+
const STRUCTURAL_ARGUMENT_KEYS = {
|
|
4045
|
+
page: true,
|
|
4046
|
+
page_size: true,
|
|
4047
|
+
since: true,
|
|
4048
|
+
workout_id: true,
|
|
4049
|
+
routine_id: true,
|
|
4050
|
+
folder_id: true,
|
|
4051
|
+
exercise_template_id: true,
|
|
4052
|
+
date: true,
|
|
4053
|
+
start_date: true,
|
|
4054
|
+
end_date: true,
|
|
4055
|
+
updated_since: true,
|
|
4056
|
+
include_custom: true,
|
|
4057
|
+
limit: true,
|
|
4058
|
+
offset: true,
|
|
4059
|
+
refresh: true,
|
|
4060
|
+
query: true,
|
|
4061
|
+
primary_muscle_group: true
|
|
4062
|
+
};
|
|
4063
|
+
const PRESENCE_ARGUMENT_KEYS = {
|
|
4064
|
+
since: true,
|
|
4065
|
+
workout_id: true,
|
|
4066
|
+
routine_id: true,
|
|
4067
|
+
folder_id: true,
|
|
4068
|
+
exercise_template_id: true,
|
|
4069
|
+
date: true,
|
|
4070
|
+
start_date: true,
|
|
4071
|
+
end_date: true,
|
|
4072
|
+
updated_since: true,
|
|
4073
|
+
query: true,
|
|
4074
|
+
primary_muscle_group: true
|
|
4075
|
+
};
|
|
4076
|
+
const NUMERIC_ARGUMENT_KEYS = {
|
|
4077
|
+
page: true,
|
|
4078
|
+
page_size: true,
|
|
4079
|
+
limit: true,
|
|
4080
|
+
offset: true
|
|
4081
|
+
};
|
|
4082
|
+
const BOOLEAN_ARGUMENT_KEYS = {
|
|
4083
|
+
include_custom: true,
|
|
4084
|
+
refresh: true
|
|
4085
|
+
};
|
|
4086
|
+
const structuralArgumentKeys = Object.keys(STRUCTURAL_ARGUMENT_KEYS);
|
|
4174
4087
|
function createSafeInvocation(name, args, taxonomy) {
|
|
4175
|
-
const argumentKeys =
|
|
4088
|
+
const argumentKeys = structuralArgumentKeys.filter((key) => key in args);
|
|
4176
4089
|
const argumentPresence = {};
|
|
4177
4090
|
const numericArgumentBuckets = {};
|
|
4178
4091
|
const booleanArguments = {};
|
|
4179
4092
|
for (const key of argumentKeys) {
|
|
4180
4093
|
const value = args[key];
|
|
4181
|
-
if (
|
|
4182
|
-
if (
|
|
4183
|
-
if (
|
|
4094
|
+
if (key in PRESENCE_ARGUMENT_KEYS && value !== null && value !== void 0) argumentPresence[key] = true;
|
|
4095
|
+
if (key in NUMERIC_ARGUMENT_KEYS && typeof value === "number") numericArgumentBuckets[key] = bucketCount(value);
|
|
4096
|
+
if (key in BOOLEAN_ARGUMENT_KEYS && typeof value === "boolean") booleanArguments[key] = value;
|
|
4184
4097
|
}
|
|
4185
4098
|
return {
|
|
4186
4099
|
name,
|