@trainheroic-unofficial/cli 0.1.0 → 0.3.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 +45 -10
- package/package.json +6 -5
- package/skill/trainheroic-unofficial/SKILL.md +248 -0
- package/skill/trainheroic-unofficial/references/api-reference.md +1361 -0
- package/skill/trainheroic-unofficial/references/data-warehouse.md +127 -0
- package/skill/trainheroic-unofficial/references/workout-creation.md +310 -0
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()]);
|
|
@@ -270,12 +270,31 @@ function unitLabel(paramType) {
|
|
|
270
270
|
if (t === null) return null;
|
|
271
271
|
return PARAM_UNIT[t] ?? null;
|
|
272
272
|
}
|
|
273
|
-
/**
|
|
273
|
+
/**
|
|
274
|
+
* Fixed measurement units for an exercise, ordered by entry slot (param 1, then param 2).
|
|
275
|
+
* Positional, not semantic: some exercises reverse the slots, so the array is not labelled
|
|
276
|
+
* by role. A null entry is an unset slot.
|
|
277
|
+
*/
|
|
278
|
+
function exerciseUnits(param1, param2) {
|
|
279
|
+
return [unitLabel(param1), unitLabel(param2)];
|
|
280
|
+
}
|
|
281
|
+
/** Present a row for display: drop the raw param-type codes, surface units positionally. */
|
|
274
282
|
function withUnits(row) {
|
|
283
|
+
const { param_1_type, param_2_type, ...rest } = row;
|
|
275
284
|
return {
|
|
276
|
-
...
|
|
277
|
-
|
|
278
|
-
|
|
285
|
+
...rest,
|
|
286
|
+
units: exerciseUnits(param_1_type, param_2_type)
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Present a full raw exercise object for display: drop the raw param-type codes and add the
|
|
291
|
+
* positional `units` array. Keeps every other field of the raw object intact.
|
|
292
|
+
*/
|
|
293
|
+
function presentExercise(raw) {
|
|
294
|
+
const { param_1_type, param_2_type, ...rest } = raw;
|
|
295
|
+
return {
|
|
296
|
+
...rest,
|
|
297
|
+
units: exerciseUnits(param_1_type, param_2_type)
|
|
279
298
|
};
|
|
280
299
|
}
|
|
281
300
|
function buildSearchText(title) {
|
|
@@ -831,10 +850,7 @@ var ExerciseLibrary = class {
|
|
|
831
850
|
await this.ensureFresh();
|
|
832
851
|
const s = this.#byId.get(id);
|
|
833
852
|
if (!s) return null;
|
|
834
|
-
|
|
835
|
-
full.param_1_unit = unitLabel(full.param_1_type);
|
|
836
|
-
full.param_2_unit = unitLabel(full.param_2_type);
|
|
837
|
-
return full;
|
|
853
|
+
return presentExercise(s.raw);
|
|
838
854
|
}
|
|
839
855
|
async defaults(id) {
|
|
840
856
|
const s = this.#byId.get(id);
|
|
@@ -971,6 +987,9 @@ const HELP = `trainheroic — command-line tool for the TrainHeroic coaching API
|
|
|
971
987
|
|
|
972
988
|
Credentials come from TRAINHEROIC_EMAIL and TRAINHEROIC_PASSWORD. Output is JSON.
|
|
973
989
|
|
|
990
|
+
Setup:
|
|
991
|
+
install-skill copy the Claude Code skill to ~/.claude/skills/trainheroic-unofficial/
|
|
992
|
+
|
|
974
993
|
Reads:
|
|
975
994
|
whoami | head-coach | athletes | programs | teams | notifications | analytics
|
|
976
995
|
program <id> | team <id> | team-codes <id>
|
|
@@ -1233,6 +1252,18 @@ async function cmdMessage(client, rest) {
|
|
|
1233
1252
|
default: return fail("usage: trainheroic message <list|read|draft|send|delete>");
|
|
1234
1253
|
}
|
|
1235
1254
|
}
|
|
1255
|
+
async function cmdInstallSkill() {
|
|
1256
|
+
const home = process.env.HOME ?? process.env.USERPROFILE;
|
|
1257
|
+
if (!home) fail("cannot determine home directory.");
|
|
1258
|
+
const skillSrc = join(import.meta.dirname, "../skill/trainheroic-unofficial");
|
|
1259
|
+
const skillDest = join(home, ".claude/skills/trainheroic-unofficial");
|
|
1260
|
+
await mkdir(join(home, ".claude/skills"), { recursive: true });
|
|
1261
|
+
await cp(skillSrc, skillDest, {
|
|
1262
|
+
recursive: true,
|
|
1263
|
+
force: true
|
|
1264
|
+
});
|
|
1265
|
+
out({ installed: skillDest });
|
|
1266
|
+
}
|
|
1236
1267
|
async function dispatch(client, group, rest) {
|
|
1237
1268
|
switch (group) {
|
|
1238
1269
|
case "whoami": return out(await get(client, "/user/simple"));
|
|
@@ -1258,6 +1289,10 @@ async function main() {
|
|
|
1258
1289
|
process.stdout.write(HELP);
|
|
1259
1290
|
return;
|
|
1260
1291
|
}
|
|
1292
|
+
if (group === "install-skill") {
|
|
1293
|
+
await cmdInstallSkill();
|
|
1294
|
+
return;
|
|
1295
|
+
}
|
|
1261
1296
|
const email = process.env.TRAINHEROIC_EMAIL;
|
|
1262
1297
|
const password = process.env.TRAINHEROIC_PASSWORD;
|
|
1263
1298
|
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.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
|
-
"url": "https://github.com/alandotcom/trainheroic-
|
|
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.
|
|
24
|
-
"@trainheroic-unofficial/js": "0.
|
|
24
|
+
"@trainheroic-unofficial/dto": "0.3.0",
|
|
25
|
+
"@trainheroic-unofficial/js": "0.3.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.
|