Add a new built-in exercise to Liftosaur end-to-end — image (gymvisual.com → S3), catalog registration, description page, and the LLM/MCP exercise list. Use when adding one or more built-in exercises to the app catalog.
Add a new built-in exercise to Liftosaur end-to-end — image (gymvisual.com → S3), catalog registration, description page, and the LLM/MCP exercise list. Use when adding one or more built-in exercises to the app catalog.
disable-model-invocation
true
argument-hint
[Exercise Name] [equipment]
Add a Built-in Exercise to Liftosaur
Add the exercise(s): $ARGUMENTS
A built-in exercise touches six places. Do them in order. Worked example throughout:
Nordic Curl, id nordicCurl, equipment bodyweight, image key nordiccurl_bodyweight.
Naming conventions (get these right first)
IExerciseId — camelCase, e.g. nordicCurl. It's a free v.string() (src/types.ts), so there is no enum/picklist to update. Pick an id that is NOT exactly 8 lowercase letters (avoids the theoretical collision space with random custom-exercise ids).
Display name — Title Case, e.g. Nordic Curl.
Equipment — one of the equipments in src/types.ts (bodyweight, barbell, dumbbell, cable, , , , , , , ).
leverageMachine
band
kettlebell
smith
ezbar
trapbar
medicineball
Image / file key — lowercased id + "_" + equipment, e.g. nordiccurl_bodyweight. Used for the S3 image filename, the exerciseImage.ts sets, and the exercises/*.md filename.
Step 1 — Image (gymvisual.com → S3)
Illustrations come from purchased gymvisual.com assets, cropped and uploaded to the images S3 bucket by src/imageExtractorGymVisual.ts.
If the exercise reuses an existing exercise's image (e.g. a slight variant): skip S3 entirely and instead set reuseImageFrom: { id, equipment } on the exercise entry in Step 2. Then skip to Step 2.
Otherwise, get a new image (manual, human-in-the-loop — costs money):
Find the image fast. gymvisual names exercises differently than we do, so don't browse by hand — search its catalog across a few synonym terms:
npx ts-node ./scripts/gymvisual_search.ts "nordic curl" "hamstring curl" "glute ham raise"
(Run ts-node directly, not npm run r — npm strips quotes and splits multi-word terms.) It prints candidate exercises grouped across formats, illustrations first with preview image URLs. Only the illustrations category yields the transparent multi-pose PNG the extractor needs; exercises that exist only as gif/video are flagged as not-extractable. Pick the right illustrations/<id>-<slug>.html product, eyeballing the preview: image. --all lists gif/video-only matches; --max=N widens the scan per term.
Ask the user to buy + download that illustration PNG on gymvisual.com. gymvisual illustrations are a multi-pose sheet.
Have the user tell you the pose index (boxIndex, 0-based, counted left-to-right) to use as the single-pose thumbnail.
Arrange a source directory: <src>/<id>_<equipment>_<boxIndex>/ containing the downloaded file whose name matches /_medium/ (e.g. nordiccurl_bodyweight_2/xxx_medium.png).
Prereqs: ImageMagick convert on PATH, and AWS credentials with write access to the images bucket.
It crops the single pose to 400x600 (→ exercises/single/small/) and the combined sheet to 800x600 (→ exercises/full/large/), uploads both to S3, then prints "Available small images" and "Available large images" arrays.
Copy those printed arrays into src/models/exerciseImage.ts, replacing the availableSmallImages and availableLargeImages set contents (entries are lowercased id_equipment). At minimum the new nordiccurl_bodyweight must appear in both sets — ExerciseImageUtils_exists gates whether the app shows an image.
Step 2 — Register the exercise (src/models/exercise.ts)
Add to allExercisesList (keep it alphabetically near its neighbors):
targetMuscles / synergistMuscles must be exact literals from the availableMuscles list in src/types.ts (typed as IMuscle) — inventing a name is a type error. The allowed values:
Filename is lowercased: exercises/nordiccurl_bodyweight.md. Format:
---
video: "<youtube-id>"
description: "One-sentence summary of the exercise and what it trains."
---## Starting Position1. ...
## Movement1. ...
## Key Points- ...
<!-- howto -->
### Set Up Your Position
Plain-prose step.
### Execute the Movement
Plain-prose step.
video — the YouTube video id only (the abc123XYZ part, not the full URL). Find a real, good demonstration video; do not fabricate an id.
Everything before <!-- howto --> is the form guide; everything after is parsed into ### Heading + prose steps (see scripts/build-exercises.ts).
Match the tone/quality of existing files (exercises/pushup_bodyweight.md, exercises/pistolsquat_bodyweight.md) and the writing guidance in .claude/commands/describe-technique.md.
Then regenerate the compiled descriptions (never hand-edit the generated file):
npm run build:exercises
This rewrites src/models/exerciseDescriptions.ts, keyed by nordiccurl_bodyweight.
Step 4 — LLM/MCP exercise list (llms/exercises.md)
Add the display name to llms/exercises.md (this manually-maintained list is compiled to src/generated/exercises.ts by scripts/build-markdown.js and surfaced to the LLM/MCP via lambda/mcp/reference.ts). Match the existing formatting, including per-equipment variants (Name, Equipment) if the exercise ships with more than its default equipment. The regeneration runs as part of the normal build.
Step 5 — Verify
npx tsc --noEmit — no type errors.
Exercise appears in the picker (search its name).
Its detail/how-to page renders the image, description, video, and how-to steps.
If you added several exercises, re-run the exercise unit tests: npx mocha --require ts-node/register test/exercise.test.ts.
Name-collision safety (already handled in code — do not re-implement)
Users often create custom exercises to fill catalog gaps. When a new built-in shares a name with a user's custom exercise, resolution prefers the custom one, so existing programs/history never silently re-point to the new built-in. This is enforced in Exercise_findIdByName / Exercise_findByNameAndEquipment (src/models/exercise.ts, custom-index checked before nameToIdMapping) and covered by test/exercise.test.ts ("custom exercise wins over built-in on name collision"). You don't need to do anything per-exercise — just be aware that picking a display name that matches an existing custom exercise is safe by design.