Talk to your Speediance (Gym Monster) smart cable machine from any agent. Read completed workouts and push custom programs that appear on the machine ready to run — no app navigation mid-session.
Unofficial — reverse-engineered from the Android app. Personal use, your own account only. Built on the MIT-licensed
UnofficialSpeedianceWorkoutManager(hbui3) andspeediance-influx(gavinmcfall).Tested on Gym Monster v1 (
SPEEDIANCE_DEVICE_TYPE=1). GM2 is untested.
speediance-cli is a single static binary — no Python or other runtime needed.
Install it one of two ways:
bash
# A) Download a release binary for your OS/arch, extract, put it on your PATH:
# https://github.com/stozo04/speediance-cli/releases
# B) Or build/install with Go (1.24+):
go install github.com/stozo04/speediance-cli/cmd/speediance-cli@latest
Then authenticate:
bash
speediance-cli login # authenticates and caches a session token (run `config path` to see where)
Set as environment variables — the CLI reads them automatically:
VariableRequiredDefaultNotesSPEEDIANCE_EMAIL✓—Account emailSPEEDIANCE_PASSWORD✓—Account passwordSPEEDIANCE_REGION—Global``Global or EU``SPEEDIANCE_DEVICE_TYPE—1``1 = Gym Monster v1
Alternatively, write a config.json in the working directory (gitignored by the repo):
json
{
"email": "you@example.com",
"password": "yourpassword",
"region": "Global"
}
You can also put these variables in a gitignored .env file in the working directory — the
SPEEDIANCE_* keys are read from it automatically (exported environment variables still take
precedence). Only those keys are read; the file is parsed into a map and any other keys are
ignored, so a stray .env can never inject unrelated variables into the process environment.
bash
speediance-cli today --json # every session today, fully resolved (any type)
speediance-cli today --date 2026-06-17 --json # …or a specific day (today | yesterday | YYYY-MM-DD)
speediance-cli workouts --days 7 --json # recent sessions (summaries, for picking)
speediance-cli session <training_id> --json # full, verbatim detail for one session
today is the one-shot, agent-friendly entry point. When the client just says
"I did a workout," call today — you do not need to know whether it was a
program, free weights, or a rowing/ski session. The tool finds the day's session(s)
and returns each one fully resolved, as an array of the same {training_id, kind, info, detail} shape that session emits. ( kind is "program", "free", or
"".)
Sample workouts --json output (a digest for picking; kind lets you filter):
json
[
{
"training_id": 123456,
"title": "Upper Body",
"date": "2025-06-15",
"duration_secs": 2700,
"calories": 320,
"volume": 4200.0,
"type": "Strength",
"kind": "program"
}
]
session <id> --json is autonomous and a faithful, complete passthrough. Given
only an id it figures out what the session was — a program/Coach session, free
weights, or a rowing/ski free session — and emits the verbatim Speediance payloads
under a uniform shape. The field names, nesting, and values are exactly what
Speediance returned ( leftWatts, forceControlScore, weights, leftBreakTimes,
totalDistance, …); the CLI does not rename, reshape, compute, or fill gaps.
kind tells you which namespace answered, so a type-agnostic consumer always reads
the same two fields:
kind``info``detail``"program"``cttTrainingInfo payload (incl. completionRate)cttTrainingInfoDetail — per-exercise, per-rep arrays"free"``freeTraining payload (totals: totalCapacity, totalEnergy, totalDistance for rowing/ski; name for guided sessions)freeTrainingDetail — [] for a freestyle Free Lift, populated for a guided session (e.g. Aerobic Rowing → per-interval finishedReps with distance/ pace/ spm + per-stroke traces)""``null``null (no session found in either namespace)
kind:"free"is the free namespace, not "freestyle". It spans both a freestyle Free Lift (noinfo.name,detail: [], aggregates only) and a guided free-namespace session (hasinfo.name, often a populateddetail) — guided cardio like Aerobic Rowing carries the full per-interval breakdown. Distinguish viainfo.name+ whetherdetailhas rows; don't assumefree⇒ empty.
A program session:
json
{
"training_id": 940759,
"kind": "program",
"info": {
"completionRate": 100.0
// … the verbatim GET /app/trainingInfo/cttTrainingInfo/<id> data payload
},
"detail": [
{
"actionLibraryName": "Standing Dual-Handle Hammer Curl",
"maxWeight": 15.0, "maxWeightCount": 5,
"score": 16, "completionScore": 5, "forceControlScore": 4,
"bilateralBalanceScore": 4, "amplitudeStableScore": 3, "actionRating": 3,
"finishedReps": [
{
"finishedCount": 14, "targetCount": 14, "capacity": 330.0, "leftRight": 0,
"trainingInfoDetail": {
"weights": [15,15,15,15,15,10,10,10,10,10,10,10,10,10],
"leftWeights": [15,15,15,15,15,10,10,10,10,10,10,10,10,10],
"rightWeights": [15,15,15,15,15,10,10,10,10,10,10,10,10,10],
"leftWatts": [41.65,51.84, "…"],
"rightWatts": [26.28,55.05, "…"],
"leftAmplitudes": [0.46,0.68, "…"]
}
}
]
}
]
}
Notes for consumers:
session <id> probes the program namespace and falls back to free;
today picks each session's probe order from the day's record list (which
carries the authoritative type — free-first for non-program types), with the
same fallback. The result shape is identical either way.weight is never invented. There is no synthesized per-set weight. For a
program, the real per-rep weights are in trainingInfoDetail.weights[] (already
per attachment, so a single-handle average is just their mean); a mid-set drop
(e.g. 15×5 → 10×9) is therefore visible. Average or summarize as you see fit.info aggregates, detail: []). A guided free-namespace session
does more: Aerobic Rowing fills detail with per-interval rows
( distance/ pace/ spm/ time) and per-stroke rope-length traces. Always read
detail rather than assuming kind:"free" is empty.weights); nothing is back-filled.distance / time, not the
per-interval pace field (which is an instantaneous sample). The CLI never
"corrects" a value; that interpretation is yours.info is object | null; detail is array | null. These are
the verbatim endpoint payloads (never normalized), so treat both null and
[] as "no rows" — e.g. if not detail. In practice detail is a populated
array for kind:"program", [] for a freestyle Free Lift, populated for a
guided free-namespace session (see above), and null only for kind:"".--telemetry.A
trainingIdcan identify different sessions in the program vs. free namespaces. Auto-detection prefers the program match; pass--programor--freetosessionto force a namespace when an id is ambiguous.
bash
speediance-cli library --search "chest" --json # filter by name or muscle
speediance-cli library # save full catalog to library.json
Returns [{id, name, muscle, tab}]. The id is required for plan JSON.
Every run saves the full catalog to --out (default library.json) — --search
filters only the stdout view, it does not narrow the saved file.
A committed library.json snapshot ships with the repo (Gym Monster v1) for offline
browsing — regenerate with speediance-cli library to get the freshest catalog or a
different device's exercises.
Author a plan JSON, then push it — the program appears on the machine immediately:
bash
speediance-cli push plan.json --dry-run # preview payload, no network write
speediance-cli push plan.json # create it on the account
Plan JSON format:
json
{
"name": "Pull Day",
"exercises": [
{
"id": 434,
"title": "Seated Dual-Handle Lat Pulldown",
"sets": [
{"reps": 12, "weight": 20, "mode": 1, "rest": 75},
{"reps": 10, "weight": 22, "mode": 1, "rest": 75},
{"reps": 8, "weight": 25, "mode": 1, "rest": 90}
]
},
{
"id": 291,
"title": "Seated Row",
"sets": [
{"reps": 12, "weight": 18, "mode": 1, "rest": 60}
]
}
]
}
FieldTypeNotesidintFrom speediance-cli library — IDs differ per account/deviceweightfloatKilogramsmodeint1=Standard, 2=Eccentric, 3=Isokinetic, 4=Constant, 5=SpotterrestintSeconds between sets
The CLI owns no log format. To keep a record of a session, pull it with workouts --json
and session <id> --json, then write it wherever you keep data (a Markdown sheet, a
database, a notebook). The tool reads and emits structured data; the caller decides the
layout.
CommandWhat it does--json``loginAuthenticate and cache a session token—workouts [--days N]List recent completed sessions✓today [--date D]Every session on a day, auto-resolved to type-correct detail (the one-shot entry point)✓session <training_id> [--free|--program]Full, verbatim detail for one session; auto-detects program/free/rowing✓library [--search X] [--out FILE]Dump or search exercise catalog✓push <plan.json> [--dry-run]Create a training program on the account✓config show|set|pathManage config.json✓ ( show)versionBuild metadata (also --version)✓completion <shell>Shell completion (bash/zsh/fish/powershell)—
--json; all human-readable hints go to stderr.0 success, 2 authentication failure, non-zero for other errors.config.json, .token.json, .env are gitignored — never commit them.%LocalAppData%\speediance\token.json on Windows, ~/.cache/speediance/token.json
on Linux, ~/Library/Caches/speediance/token.json on macOS) and refreshed automatically on
expiry — not in the working directory (so it can't be swept into a commit) and not in the
roaming config dir (so a live credential isn't synced across machines). Override the location with
SPEEDIANCE_TOKEN_CACHE or the token_cache_path config key; run speediance-cli config path to
see where it resolved. A token left in a legacy .token.json by an older version is moved to the
per-user location on first run.--dry-run before push when authoring new programs to
confirm exercise IDs resolved correctly.internal/api — that's the single place to patch.