@trainheroic-unofficial/cli 0.1.0 → 0.2.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/dist/cli.mjs CHANGED
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
- import { mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import { cp, mkdir, readFile, writeFile } from "node:fs/promises";
3
+ import { dirname, join } from "node:path";
3
4
  import process from "node:process";
4
5
  import { parseArgs } from "node:util";
5
6
  import { z } from "zod";
6
7
  import { homedir } from "node:os";
7
- import { dirname, join } from "node:path";
8
8
  //#region ../dto/src/common.ts
9
9
  /** An entity id as it arrives over the wire — a number or a numeric string. */
10
10
  const idSchema = z.union([z.string(), z.number()]);
@@ -971,6 +971,9 @@ const HELP = `trainheroic — command-line tool for the TrainHeroic coaching API
971
971
 
972
972
  Credentials come from TRAINHEROIC_EMAIL and TRAINHEROIC_PASSWORD. Output is JSON.
973
973
 
974
+ Setup:
975
+ install-skill copy the Claude Code skill to ~/.claude/skills/trainheroic-unofficial/
976
+
974
977
  Reads:
975
978
  whoami | head-coach | athletes | programs | teams | notifications | analytics
976
979
  program <id> | team <id> | team-codes <id>
@@ -1233,6 +1236,18 @@ async function cmdMessage(client, rest) {
1233
1236
  default: return fail("usage: trainheroic message <list|read|draft|send|delete>");
1234
1237
  }
1235
1238
  }
1239
+ async function cmdInstallSkill() {
1240
+ const home = process.env.HOME ?? process.env.USERPROFILE;
1241
+ if (!home) fail("cannot determine home directory.");
1242
+ const skillSrc = join(import.meta.dirname, "../skill/trainheroic-unofficial");
1243
+ const skillDest = join(home, ".claude/skills/trainheroic-unofficial");
1244
+ await mkdir(join(home, ".claude/skills"), { recursive: true });
1245
+ await cp(skillSrc, skillDest, {
1246
+ recursive: true,
1247
+ force: true
1248
+ });
1249
+ out({ installed: skillDest });
1250
+ }
1236
1251
  async function dispatch(client, group, rest) {
1237
1252
  switch (group) {
1238
1253
  case "whoami": return out(await get(client, "/user/simple"));
@@ -1258,6 +1273,10 @@ async function main() {
1258
1273
  process.stdout.write(HELP);
1259
1274
  return;
1260
1275
  }
1276
+ if (group === "install-skill") {
1277
+ await cmdInstallSkill();
1278
+ return;
1279
+ }
1261
1280
  const email = process.env.TRAINHEROIC_EMAIL;
1262
1281
  const password = process.env.TRAINHEROIC_PASSWORD;
1263
1282
  if (!email || !password) fail("set TRAINHEROIC_EMAIL and TRAINHEROIC_PASSWORD in the environment.");
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@trainheroic-unofficial/cli",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
7
- "url": "https://github.com/alandotcom/trainheroic-skill.git",
7
+ "url": "https://github.com/alandotcom/trainheroic-unofficial.git",
8
8
  "directory": "packages/cli"
9
9
  },
10
10
  "description": "Command-line tool for the TrainHeroic coaching API.",
@@ -16,12 +16,13 @@
16
16
  "access": "public"
17
17
  },
18
18
  "files": [
19
- "dist"
19
+ "dist",
20
+ "skill"
20
21
  ],
21
22
  "dependencies": {
22
23
  "zod": "^4.4.3",
23
- "@trainheroic-unofficial/dto": "0.1.0",
24
- "@trainheroic-unofficial/js": "0.1.0"
24
+ "@trainheroic-unofficial/dto": "0.2.0",
25
+ "@trainheroic-unofficial/js": "0.2.0"
25
26
  },
26
27
  "devDependencies": {
27
28
  "@types/node": "^26.0.0",
@@ -0,0 +1,248 @@
1
+ ---
2
+ name: trainheroic-unofficial
3
+ description: Call the TrainHeroic coach/athlete REST API to manage athletes, teams, programs, sessions, exercises, and analytics. Use when the user wants to authenticate against TrainHeroic, automate coaching tasks, build workouts or session templates, create teams/athletes/custom exercises, resolve exercise IDs, or pull training, readiness, and analytics data.
4
+ metadata:
5
+ author: alandotcom
6
+ version: "2.0.0"
7
+ ---
8
+
9
+ # TrainHeroic API
10
+
11
+ Authenticated access to the TrainHeroic coaching API through the `trainheroic`
12
+ command-line tool (`@trainheroic-unofficial/cli`), which wraps the `@trainheroic-unofficial/js`
13
+ SDK: a spec-driven workout builder, an on-disk exercise cache, and gated messaging.
14
+
15
+ - Coach base URL: `https://api.trainheroic.com`
16
+ - Login endpoint: `https://apis.trainheroic.com/auth`
17
+
18
+ ## Setup
19
+
20
+ ### CLI
21
+
22
+ Before making API calls, verify the CLI is available:
23
+
24
+ ```bash
25
+ which trainheroic
26
+ ```
27
+
28
+ If not found, install it (no credentials required):
29
+
30
+ ```bash
31
+ npm install -g @trainheroic-unofficial/cli
32
+ trainheroic install-skill # also refreshes skill files in ~/.claude/skills/
33
+ ```
34
+
35
+ To update to the latest version:
36
+
37
+ ```bash
38
+ npm update -g @trainheroic-unofficial/cli
39
+ trainheroic install-skill # pick up any updated skill files
40
+ ```
41
+
42
+ Set `TH` to the binary name for the commands below:
43
+
44
+ ```bash
45
+ TH="trainheroic"
46
+ ```
47
+
48
+ ### Credentials
49
+
50
+ Credentials come from the environment (if unset, ask the user — do not guess):
51
+
52
+ ```bash
53
+ export TRAINHEROIC_EMAIL="coach@example.com"
54
+ export TRAINHEROIC_PASSWORD="..."
55
+ ```
56
+
57
+ The CLI logs in, caches the session at `~/.trainheroic/session.json` (mode 0600),
58
+ reuses it across invocations, and re-authenticates automatically on a 401/403. The
59
+ exercise library is cached at `~/.trainheroic/library.json`.
60
+
61
+ Start a session with `$TH whoami`: it confirms auth and returns the coach's `id`,
62
+ `org_id`, and roles that later calls reference. Run `$TH help` for the full command list.
63
+
64
+ ## Making requests
65
+
66
+ `$TH request <METHOD> <path> [json-body]` — path is everything after the base URL. It
67
+ prints `{status, ok, data}` and exits non-zero on failure. Bodies can be an inline JSON
68
+ argument, `--file <path>`, or piped on stdin.
69
+
70
+ ```bash
71
+ $TH request GET /v5/athletes
72
+ $TH request POST /v5/teams/4677619/teamCodes '{"type": 2}'
73
+ $TH request PUT /v5/athletes/archive '{"athleteIds": [123]}'
74
+ $TH request DELETE /v5/teamCodes/874586
75
+ cat block.json | $TH request POST /2.0/coach/calendar/saveProgramWorkoutSets
76
+ ```
77
+
78
+ A few endpoints live on the login host (`apis.trainheroic.com`); reach them with
79
+ `--base apis` (the default base is the coach host). Named shortcuts cover the common
80
+ reads: `whoami`, `head-coach`, `athletes`, `programs`, `teams`, `notifications`,
81
+ `analytics`, `program <id>`, `team <id>`, `team-codes <id>`.
82
+
83
+ Load `references/api-reference.md` when you need an endpoint's exact request or
84
+ response shape, or an area not covered here.
85
+
86
+ ## What you can do
87
+
88
+ | Area | How |
89
+ | ------------------- | ------------------------------------------------------------------------------------------------------------------------- |
90
+ | Athletes | `$TH athletes`; invite/archive/restore via `$TH request ...` |
91
+ | Teams | `$TH teams`, `$TH team <id>`, `$TH team-codes <id>`; create via `$TH request POST /1.0/coach/team/createWithTitleAndCode` |
92
+ | Programs | `$TH programs`, `$TH program <id>` |
93
+ | Exercises | `$TH exercise resolve\|search\|get\|sync\|create\|forget\|stats` (below) |
94
+ | Sessions / workouts | `$TH workout build\|read\|publish\|remove` (below) |
95
+ | Messaging | `$TH message list\|read\|draft\|send\|delete` (below) |
96
+ | Analytics | `$TH analytics`, then `$TH request POST /v5/analytics/*` |
97
+
98
+ ## Resolving exercises
99
+
100
+ Do **not** dump `GET /v5/exerciseLibrary/all` (~2,400 rows) to find an ID. Use the
101
+ cache, which mirrors the library into `~/.trainheroic/library.json` and serves lookups
102
+ locally:
103
+
104
+ ```bash
105
+ $TH exercise resolve "Back Squat" # exact/best match -> id (+ unit labels)
106
+ $TH exercise search "incline press" # ranked candidates
107
+ $TH exercise sync --force # refresh the mirror (7-day TTL otherwise)
108
+ ```
109
+
110
+ `resolve` returns `match: null` plus a candidate list when a name is ambiguous; pick the
111
+ right ID from there. The mirror auto-refreshes on a 7-day TTL and on a miss.
112
+
113
+ After creating a custom exercise, go through the cache so the mirror stays current; after
114
+ deleting one via the API, drop it from the mirror:
115
+
116
+ ```bash
117
+ $TH exercise create '{"title":"Sandbag Clean","param_1_type":3,"param_2_type":1}'
118
+ $TH exercise forget 7721170 --yes # cache-only, run after an API delete
119
+ ```
120
+
121
+ `$TH exercise stats` shows the cached row count.
122
+
123
+ ## Building a workout
124
+
125
+ **Confirm the program before you build, and confirm before you publish.** Writing a
126
+ session to a real coaching calendar is athlete-facing and hard to undo. When the request
127
+ is at all ambiguous — which team/program/calendar, which date, the exact exercises,
128
+ sets/reps/load, distance units, or how a scheme should be structured — **ask clarifying
129
+ follow-up questions first; do not guess.** Restate the program you intend to build and get
130
+ explicit confirmation before publishing. Build a draft (the default), show the read-back
131
+ for review, and only publish once the user confirms.
132
+
133
+ `$TH workout build` runs the whole sequence (program → session → blocks → exercises),
134
+ fills every field that otherwise makes the exercise step return HTTP 500, encodes
135
+ prescriptions correctly, and prints unit advisories plus a read-back. Feed it a JSON spec
136
+ (inline, `--file`, or stdin):
137
+
138
+ ```bash
139
+ cat > day.json <<'JSON'
140
+ { "blocks": [
141
+ { "title": "Primary Press", "exercises": [
142
+ { "id": 1162, "title": "Bench Press", "reps": [10,10,8,8], "rpe": 8 } ] },
143
+ { "title": "Accessory", "instruction": "Superset", "exercises": [
144
+ { "id": 903, "title": "Dips", "sets": 3, "reps": 12, "rpe": 8 },
145
+ { "id": 6535, "title": "Tricep Pushdown", "reps": [15,15,15], "rpe": 8 } ] }
146
+ ] }
147
+ JSON
148
+
149
+ $TH workout build --program 4980851 --date 2026-6-22 --file day.json # draft
150
+ $TH workout publish --pw <id> --yes # publish after review
151
+ ```
152
+
153
+ The build defaults to a **draft** (unpublished). Add `--publish --yes` to publish in one
154
+ step, or publish later with `$TH workout publish --pw <id> --yes`. Read a built session
155
+ back with `$TH workout read --program <id> --date Y-M-D --pw <id>`. There is no in-place
156
+ replace: to rebuild a date, remove the old session first with
157
+ `$TH workout remove --program <id> --pw <id> --yes`.
158
+
159
+ Two exercises in one block become a superset. Add `"leaderboard": "rounds"` (or
160
+ `reps`/`time`/`calories`/`meters`/… or `{"unit":"time","lowest_wins":true}`) to a block to
161
+ make it a scored Red Zone leaderboard (trophy + "FOR <UNIT>"). A top-level `"instruction"`
162
+ in the spec sets the session's Coach Instructions (the day-note above the blocks;
163
+ `PUT /3.0/coach/workout/{id}`, applied after the blocks save and without publishing — a
164
+ draft stays a draft). The read-back shows it under "Coach Instructions". For an **AMRAP**,
165
+ score by `rounds`/`reps` and program multiple sets (one per expected round); for **"for
166
+ time"**, score by `time`. Ask the coach when the scheme or score is ambiguous rather than
167
+ guessing.
168
+
169
+ Load `references/workout-creation.md` before building manually or for something the builder
170
+ does not cover (drop sets, pyramids, %-of-max, the raw field list, or the parameter-type
171
+ table).
172
+
173
+ ## Messaging
174
+
175
+ A **stream** is a conversation (a team, or a 1:1 with an athlete); a **comment** is a
176
+ message. Sending is athlete-facing and immediate — TrainHeroic chat has no server-side
177
+ draft state, so a POST is delivered at once. The CLI therefore separates drafting from
178
+ sending and requires `--yes` to actually send or delete.
179
+
180
+ ```bash
181
+ $TH message list # conversations (id, kind, title)
182
+ $TH message read 37730920 # recent messages
183
+ $TH message draft 37730920 "Great session today!" # PREVIEW only — never sends
184
+ $TH message send 37730920 "Great session today!" --yes # delivers (gate behind user OK)
185
+ $TH message send 37730920 "Nice PR!" --reply-to 125652586 --yes # threaded reply
186
+ $TH message delete 37730920 <commentId> --yes # soft delete (destructive)
187
+ ```
188
+
189
+ Default to `draft`: it prints the exact payload and target without touching the account.
190
+ Run `send` only after the user confirms the wording and recipient. Check
191
+ `$TH notifications` (`countMessagingNotViewed`) first as a cheap "anything new?" gate.
192
+
193
+ ## Gotchas
194
+
195
+ Environment-specific facts that defy reasonable assumptions:
196
+
197
+ - **Sending a chat message needs `feed_id` in the body** — the stream id from the path,
198
+ repeated. `{"content": "..."}` returns `400 Invalid parameters`; so does trimming any
199
+ field. The CLI sends the full body. There is **no draft state**: a POST is delivered
200
+ immediately, so gate sends behind explicit user confirmation (`--yes`).
201
+ - **Units are fixed per exercise — you can't set them at prescribe time.** On save the API
202
+ discards the `param_1_type`/`param_2_type` you send and restores the exercise's library
203
+ defaults (the _values_ are kept). So the stock `Run` (miles) can't be made metric; a
204
+ "200 m run" on it shows as 200 _miles_. And `param_2_type` `2` (% of max) and `14` (RPE)
205
+ coerce to weight on a weight lift, rendering as pounds — put % or RPE in the
206
+ `instruction` (the builder does this from `rpe`). You _can_ add weight (`1`) to a
207
+ no-secondary-param lift (weighted Pull-Ups). Check units with `$TH exercise resolve`
208
+ (the `units` array, ordered by entry slot `[param 1, param 2]`); the builder prints a
209
+ warning when a sent type will be overridden. "Max"/"AMRAP" reps work as free text in the
210
+ rep slots.
211
+ - **`saveWorkoutSetExercises` returns HTTP 500** unless every field is present: all ten
212
+ `param_1_data_N`/`param_2_data_N` slots (empty string for unused), `set_num`, `key`,
213
+ `setKey`, `eType`, `tags`, `use_count`. The builder fills these — prefer it over raw calls.
214
+ - **`GET /1.0/coach/programs/edit/{cal}/{y}/{m}/{d}` returns every session in that date's
215
+ _month_** — not just the day, and not the whole calendar. Match yours by the `id`
216
+ returned at create time, not `programWorkouts[0]`; to read a whole program, walk month by
217
+ month.
218
+ - **A created session exposes two IDs**: `workout_id` (for adding blocks) and `id` (the
219
+ programWorkout id, used to publish and to delete; the CLI calls it `pw`).
220
+ - **The API ignores the exercise `title` you send** and uses the real title for
221
+ `exercise_id`; it also overrides both `param_1_type` and `param_2_type` to the exercise
222
+ default (see the units gotcha above).
223
+ - **Response envelopes vary.** `2.0/coach/*` tag/exercise endpoints wrap data as
224
+ `{"success": 1, "data": {...}}`; most others return bare objects/arrays.
225
+ - **This API is undocumented and can change.** `references/api-reference.md` has a "Still
226
+ Unexplored" section listing known gaps.
227
+ - **Destructive actions always require explicit user action.** For any call that deletes,
228
+ archives, or removes live data — archive/restore athlete, delete team, delete custom
229
+ exercise, remove or unpublish a session, delete team code, delete a message — and for
230
+ **sending a chat message** (athlete-facing, instant, no draft state) — you must:
231
+ 1. **never run it autonomously** (no destructive call without the user's explicit
232
+ go-ahead in the moment — prior approval does not carry over to a new action);
233
+ 2. **print a clear WARNING** stating exactly what will be affected, that it acts on the
234
+ live account, and that it is hard or impossible to undo;
235
+ 3. **offer the user the option to do it themselves** — hand them the exact command (e.g.
236
+ `$TH request DELETE /v5/teamCodes/874586`) or the UI steps.
237
+
238
+ The CLI enforces a `--yes` flag on `message send`/`message delete`, `workout publish`,
239
+ `workout remove`, and `exercise forget`, but the gate above still applies: confirm in the
240
+ moment before adding `--yes`.
241
+
242
+ ## Beyond the CLI
243
+
244
+ The same SDK powers two MCP servers (see `README.md`):
245
+ `@trainheroic-unofficial/coach-mcp` (local, single-user, stdio) and
246
+ `@trainheroic-unofficial/cloudflare` (hosted, multi-tenant). The hosted server adds a
247
+ programming/messaging history warehouse (D1); `references/data-warehouse.md` documents
248
+ those zones and the sync rules.