@trainheroic-unofficial/cli 0.4.2 → 0.5.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
CHANGED
|
@@ -4,6 +4,13 @@ A command-line tool for the TrainHeroic coaching API. Takes credentials from the
|
|
|
4
4
|
|
|
5
5
|
Part of the [trainheroic-unofficial](../../README.md) workspace.
|
|
6
6
|
|
|
7
|
+
## Contents
|
|
8
|
+
|
|
9
|
+
- [Install](#install)
|
|
10
|
+
- [Usage](#usage)
|
|
11
|
+
- [Conventions](#conventions)
|
|
12
|
+
- [Develop](#develop)
|
|
13
|
+
|
|
7
14
|
## Install
|
|
8
15
|
|
|
9
16
|
```bash
|
|
@@ -11,39 +18,99 @@ npm install -g @trainheroic-unofficial/cli
|
|
|
11
18
|
# or: npx @trainheroic-unofficial/cli <command>
|
|
12
19
|
```
|
|
13
20
|
|
|
21
|
+
Needs Node >= 18 and an existing TrainHeroic account.
|
|
22
|
+
|
|
14
23
|
## Usage
|
|
15
24
|
|
|
25
|
+
Set your TrainHeroic login in the environment (stored in plaintext and shell history — treat
|
|
26
|
+
it as a secret), then run a command. Every command prints JSON to stdout.
|
|
27
|
+
|
|
16
28
|
```bash
|
|
17
29
|
export TRAINHEROIC_EMAIL="coach@example.com"
|
|
18
30
|
export TRAINHEROIC_PASSWORD="..."
|
|
19
31
|
|
|
20
|
-
trainheroic whoami
|
|
21
|
-
trainheroic exercise resolve "Back Squat"
|
|
22
|
-
trainheroic workout build --program 12345 --date 2026-6-22 --file day.json
|
|
32
|
+
trainheroic whoami # confirm auth; prints your account
|
|
33
|
+
trainheroic coach exercise resolve "Back Squat" # name -> exercise (id, units)
|
|
23
34
|
```
|
|
24
35
|
|
|
25
|
-
|
|
26
|
-
|
|
36
|
+
**Driving this from an AI agent?** Run `trainheroic skill` first — it prints the full
|
|
37
|
+
workflow guide (with copy-paste workout-spec examples) to stdout, version-matched to the
|
|
38
|
+
binary; `trainheroic skill --full` adds the API and workout-creation reference docs. It's the
|
|
39
|
+
fastest way to get the spec shapes right instead of guessing from flags.
|
|
40
|
+
|
|
41
|
+
Commands split into three groups. `whoami` and `request` are **shared** (role-agnostic).
|
|
42
|
+
Everything for managing a roster lives under **`coach`** — reads (`coach athletes`,
|
|
43
|
+
`coach programs`, `coach teams`, `coach program <id>`, …), the exercise library
|
|
44
|
+
(`coach exercise resolve|search|get|sync|create|forget|stats`), the workout lifecycle
|
|
45
|
+
(`coach workout build|read|publish|remove`), and messaging
|
|
46
|
+
(`coach message list|read|draft|send|delete`). Your own training lives under **`athlete`**.
|
|
47
|
+
Run `trainheroic` with no command for the full reference. Dates are `Y-M-D` with a 1-based
|
|
48
|
+
month, e.g. `2026-6-22` or `2026-06-22` for 22 June 2026.
|
|
27
49
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
50
|
+
### Building a workout
|
|
51
|
+
|
|
52
|
+
`coach workout build` reads a workout spec — a JSON file (`--file`), an inline argument, or
|
|
53
|
+
stdin — and writes it into a program on a date. `--program` is a program id (find it in the
|
|
54
|
+
program's URL in the TrainHeroic web app, or run `trainheroic coach programs`). The spec is
|
|
55
|
+
`{ blocks, instruction? }` (a bare blocks array is also accepted). Each exercise's `id` is a
|
|
56
|
+
library id; get one with `coach exercise resolve`. `reps` and `weight` take a scalar or a
|
|
57
|
+
per-set array (`"reps": [5, 5, 3]`); loads use the exercise's configured unit. The full schema
|
|
58
|
+
is `WorkoutSpec` in [`@trainheroic-unofficial/dto`](../dto).
|
|
59
|
+
|
|
60
|
+
```jsonc
|
|
61
|
+
// day.json — one block with one exercise (id 41822 is a library exercise id)
|
|
62
|
+
{
|
|
63
|
+
"instruction": "Warm up first.",
|
|
64
|
+
"blocks": [
|
|
65
|
+
{
|
|
66
|
+
"title": "Strength",
|
|
67
|
+
"exercises": [{ "id": 41822, "sets": 5, "reps": 5, "weight": 225, "rpe": 8 }],
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
# Build as a draft (988 is the program id):
|
|
75
|
+
trainheroic coach workout build --program 988 --date 2026-6-22 --file day.json
|
|
76
|
+
|
|
77
|
+
# Build and publish to athletes (publishing is athlete-facing, so it needs --yes):
|
|
78
|
+
trainheroic coach workout build --program 988 --date 2026-6-22 --file day.json --publish --yes
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Raw requests
|
|
82
|
+
|
|
83
|
+
For an endpoint with no dedicated command, call it directly:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
trainheroic request GET /user/simple
|
|
87
|
+
trainheroic request POST /some/path '{"key":"value"}' --base apis
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
`--base` selects the host: `coach` (the default, `api.trainheroic.com`) or `apis`
|
|
91
|
+
(`apis.trainheroic.com`). The body can be inline JSON (as above) or `--file <path>`.
|
|
33
92
|
|
|
34
93
|
## Conventions
|
|
35
94
|
|
|
36
|
-
- Output is JSON on stdout; errors go to stderr with a non-zero exit code
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
95
|
+
- Output is JSON on stdout; errors go to stderr with a non-zero exit code and a readable
|
|
96
|
+
validation path on bad input.
|
|
97
|
+
- Actions that touch an athlete or delete data require an explicit `--yes`:
|
|
98
|
+
`coach workout publish`, `coach workout remove`, `coach exercise forget`,
|
|
99
|
+
`coach message send`, `coach message delete`, `athlete log-set`, and
|
|
100
|
+
`coach workout build --publish`. (`coach workout build` alone creates a draft; `--publish`
|
|
101
|
+
makes it visible to athletes, which is why that combination needs `--yes`.)
|
|
40
102
|
- JSON input can be passed inline, with `--file <path>`, or piped on stdin. Inputs are
|
|
41
|
-
validated against the shared schemas from `@trainheroic-unofficial/dto
|
|
103
|
+
validated against the shared schemas from `@trainheroic-unofficial/dto` before sending.
|
|
42
104
|
- The session token and the exercise library are cached under `~/.trainheroic/`. The library
|
|
43
|
-
file
|
|
105
|
+
file matches the shape the local coach server (`@trainheroic-unofficial/coach-mcp`) writes,
|
|
106
|
+
so the CLI and that server share one cache.
|
|
44
107
|
|
|
45
108
|
## Develop
|
|
46
109
|
|
|
110
|
+
Run `pnpm install` once at the repo root (Node >= 22, pnpm 10), then from this package. During
|
|
111
|
+
development, run from source with `pnpm start <args>`; after `pnpm build`, the `trainheroic`
|
|
112
|
+
binary (`dist/cli.mjs`) does the same.
|
|
113
|
+
|
|
47
114
|
```bash
|
|
48
115
|
pnpm start whoami # tsx src/cli.ts
|
|
49
116
|
pnpm build # tsdown -> dist/cli.mjs
|
package/dist/cli.mjs
CHANGED
|
@@ -1448,38 +1448,48 @@ async function saveSession(sessionId) {
|
|
|
1448
1448
|
const HELP = `trainheroic — command-line tool for the TrainHeroic coaching API
|
|
1449
1449
|
|
|
1450
1450
|
Credentials come from TRAINHEROIC_EMAIL and TRAINHEROIC_PASSWORD. Output is JSON.
|
|
1451
|
+
Coaching commands live under 'coach'; your own training lives under 'athlete'.
|
|
1452
|
+
|
|
1453
|
+
Start here (for AI agents):
|
|
1454
|
+
trainheroic skill full workflow guide + copy-paste examples (esp. workout specs)
|
|
1455
|
+
trainheroic skill --full also print the API + workout-creation reference docs
|
|
1456
|
+
trainheroic skill list list the available guides (coach, athlete)
|
|
1451
1457
|
|
|
1452
1458
|
Setup:
|
|
1453
1459
|
install-skill copy the Claude Code skills to ~/.claude/skills/
|
|
1454
1460
|
|
|
1455
|
-
|
|
1456
|
-
whoami
|
|
1457
|
-
|
|
1458
|
-
|
|
1461
|
+
Shared:
|
|
1462
|
+
whoami the logged-in account
|
|
1463
|
+
request <METHOD> <path> [json] [--base coach|apis] [--file f] raw API call (--base defaults to coach)
|
|
1464
|
+
|
|
1465
|
+
Coach — manage a roster (needs a coach account):
|
|
1466
|
+
coach head-coach | athletes | programs | teams | notifications | analytics
|
|
1467
|
+
coach program <id> | team <id> | team-codes <id>
|
|
1459
1468
|
|
|
1460
|
-
|
|
1461
|
-
exercise resolve <name>
|
|
1462
|
-
exercise search <query> [--limit N]
|
|
1463
|
-
exercise get <id>
|
|
1464
|
-
exercise sync [--force]
|
|
1465
|
-
exercise create <json>|--file f
|
|
1466
|
-
exercise forget <id> --yes
|
|
1467
|
-
exercise stats
|
|
1469
|
+
exercise library (cached at ~/.trainheroic/library.json):
|
|
1470
|
+
coach exercise resolve <name>
|
|
1471
|
+
coach exercise search <query> [--limit N]
|
|
1472
|
+
coach exercise get <id>
|
|
1473
|
+
coach exercise sync [--force]
|
|
1474
|
+
coach exercise create <json>|--file f
|
|
1475
|
+
coach exercise forget <id> --yes
|
|
1476
|
+
coach exercise stats
|
|
1468
1477
|
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
workout
|
|
1472
|
-
workout
|
|
1473
|
-
workout
|
|
1478
|
+
workouts (spec: {"blocks":[{"title","exercises":[{"id","sets"?,"reps"?,"weight"?,"rpe"?}]}],"instruction"?};
|
|
1479
|
+
reps/weight may be a scalar or per-set array; omit --publish to leave a draft):
|
|
1480
|
+
coach workout build --program <id> (--date Y-M-D | --timeline-day <n>) [--publish --yes] <spec.json>|--file f
|
|
1481
|
+
coach workout read --program <id> --date Y-M-D --pw <id>
|
|
1482
|
+
coach workout publish --pw <id> --yes
|
|
1483
|
+
coach workout remove --program <id> --pw <id> --yes
|
|
1474
1484
|
|
|
1475
|
-
|
|
1476
|
-
message list
|
|
1477
|
-
message read <streamId> [--limit N]
|
|
1478
|
-
message draft <streamId> <text> [--reply-to <id>]
|
|
1479
|
-
message send <streamId> <text> [--reply-to <id>] --yes
|
|
1480
|
-
message delete <streamId> <commentId> --yes
|
|
1485
|
+
messaging:
|
|
1486
|
+
coach message list
|
|
1487
|
+
coach message read <streamId> [--limit N]
|
|
1488
|
+
coach message draft <streamId> <text> [--reply-to <id>]
|
|
1489
|
+
coach message send <streamId> <text> [--reply-to <id>] --yes
|
|
1490
|
+
coach message delete <streamId> <commentId> --yes
|
|
1481
1491
|
|
|
1482
|
-
Athlete
|
|
1492
|
+
Athlete — the logged-in user's own training (a coach account works too):
|
|
1483
1493
|
athlete whoami | profile [--metric] | prefs | working-maxes
|
|
1484
1494
|
athlete workouts --start Y-M-D --end Y-M-D [--raw]
|
|
1485
1495
|
athlete exercises [--q <text>] [--limit N]
|
|
@@ -1507,9 +1517,9 @@ function toInt(value, label) {
|
|
|
1507
1517
|
return n;
|
|
1508
1518
|
}
|
|
1509
1519
|
/** Validate input against a dto schema, failing with a readable message on mismatch. */
|
|
1510
|
-
function validate(schema, value, label) {
|
|
1520
|
+
function validate(schema, value, label, hint) {
|
|
1511
1521
|
const result = schema.safeParse(value);
|
|
1512
|
-
if (!result.success) fail(`invalid ${label} — ${result.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")}`);
|
|
1522
|
+
if (!result.success) fail(`invalid ${label} — ${result.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")}${hint ? `\n${hint}` : ""}`);
|
|
1513
1523
|
return result.data;
|
|
1514
1524
|
}
|
|
1515
1525
|
function parse(args, options) {
|
|
@@ -1577,14 +1587,14 @@ async function cmdExercise(client, rest) {
|
|
|
1577
1587
|
const [sub, ...a] = rest;
|
|
1578
1588
|
const lib = library(client);
|
|
1579
1589
|
switch (sub) {
|
|
1580
|
-
case "resolve": return out(await lib.resolve(need(a.join(" ").trim() || void 0, "exercise resolve <name>")));
|
|
1590
|
+
case "resolve": return out(await lib.resolve(need(a.join(" ").trim() || void 0, "coach exercise resolve <name>")));
|
|
1581
1591
|
case "search": {
|
|
1582
1592
|
const { values, positionals } = parse(a, { limit: { type: "string" } });
|
|
1583
|
-
const query = need(positionals.join(" ").trim() || void 0, "exercise search <query>");
|
|
1593
|
+
const query = need(positionals.join(" ").trim() || void 0, "coach exercise search <query>");
|
|
1584
1594
|
const limit = values.limit !== void 0 ? toInt(values.limit, "--limit") : 20;
|
|
1585
1595
|
return out(await lib.search(query, limit));
|
|
1586
1596
|
}
|
|
1587
|
-
case "get": return out(await lib.get(toInt(need(a[0], "exercise get <id>"), "id")));
|
|
1597
|
+
case "get": return out(await lib.get(toInt(need(a[0], "coach exercise get <id>"), "id")));
|
|
1588
1598
|
case "sync": {
|
|
1589
1599
|
const { values } = parse(a, { force: { type: "boolean" } });
|
|
1590
1600
|
if (values.force === true) return out(await lib.refresh());
|
|
@@ -1598,13 +1608,13 @@ async function cmdExercise(client, rest) {
|
|
|
1598
1608
|
}
|
|
1599
1609
|
case "forget": {
|
|
1600
1610
|
const { values, positionals } = parse(a, { yes: { type: "boolean" } });
|
|
1601
|
-
const id = toInt(need(positionals[0], "exercise forget <id> --yes"), "id");
|
|
1611
|
+
const id = toInt(need(positionals[0], "coach exercise forget <id> --yes"), "id");
|
|
1602
1612
|
if (values.yes !== true) fail(`add --yes to forget exercise ${id} from the local cache.`);
|
|
1603
1613
|
await lib.recordDelete(id);
|
|
1604
1614
|
return out({ forgotten: id });
|
|
1605
1615
|
}
|
|
1606
1616
|
case "stats": return out(await lib.stats());
|
|
1607
|
-
default: return fail("usage: trainheroic exercise <resolve|search|get|sync|create|forget|stats>");
|
|
1617
|
+
default: return fail("usage: trainheroic coach exercise <resolve|search|get|sync|create|forget|stats>");
|
|
1608
1618
|
}
|
|
1609
1619
|
}
|
|
1610
1620
|
async function cmdWorkout(client, rest) {
|
|
@@ -1619,10 +1629,10 @@ async function cmdWorkout(client, rest) {
|
|
|
1619
1629
|
yes: { type: "boolean" },
|
|
1620
1630
|
file: { type: "string" }
|
|
1621
1631
|
});
|
|
1622
|
-
const programId = toInt(need(values.program, "workout build --program <id> ..."), "--program");
|
|
1632
|
+
const programId = toInt(need(values.program, "coach workout build --program <id> ..."), "--program");
|
|
1623
1633
|
if (values.date === void 0 && values["timeline-day"] === void 0) fail("provide --date YYYY-M-D or --timeline-day <n>.");
|
|
1624
1634
|
const parsed = await jsonInput(positionals[0], values.file);
|
|
1625
|
-
const spec = validate(workoutSpecSchema, Array.isArray(parsed) ? { blocks: parsed } : parsed, "workout spec");
|
|
1635
|
+
const spec = validate(workoutSpecSchema, Array.isArray(parsed) ? { blocks: parsed } : parsed, "workout spec", "run 'trainheroic skill' for the workout spec format and copy-paste examples.");
|
|
1626
1636
|
const publish = values.publish === true;
|
|
1627
1637
|
if (publish && values.yes !== true) fail("publishing is athlete-facing; add --yes to build and publish.");
|
|
1628
1638
|
const opts = {
|
|
@@ -1649,14 +1659,14 @@ async function cmdWorkout(client, rest) {
|
|
|
1649
1659
|
date: { type: "string" },
|
|
1650
1660
|
pw: { type: "string" }
|
|
1651
1661
|
});
|
|
1652
|
-
return out(await readSession(client, toInt(need(values.program, "workout read --program <id> --date Y-M-D --pw <id>"), "--program"), parseWorkoutDate(need(values.date, "workout read --program <id> --date Y-M-D --pw <id>")), toInt(need(values.pw, "workout read --program <id> --date Y-M-D --pw <id>"), "--pw")));
|
|
1662
|
+
return out(await readSession(client, toInt(need(values.program, "coach workout read --program <id> --date Y-M-D --pw <id>"), "--program"), parseWorkoutDate(need(values.date, "coach workout read --program <id> --date Y-M-D --pw <id>")), toInt(need(values.pw, "coach workout read --program <id> --date Y-M-D --pw <id>"), "--pw")));
|
|
1653
1663
|
}
|
|
1654
1664
|
case "publish": {
|
|
1655
1665
|
const { values } = parse(a, {
|
|
1656
1666
|
pw: { type: "string" },
|
|
1657
1667
|
yes: { type: "boolean" }
|
|
1658
1668
|
});
|
|
1659
|
-
const pw = toInt(need(values.pw, "workout publish --pw <id> --yes"), "--pw");
|
|
1669
|
+
const pw = toInt(need(values.pw, "coach workout publish --pw <id> --yes"), "--pw");
|
|
1660
1670
|
if (values.yes !== true) fail(`publishing makes pw ${pw} athlete-visible; add --yes.`);
|
|
1661
1671
|
await publishSession(client, pw);
|
|
1662
1672
|
return out({ published: pw });
|
|
@@ -1667,13 +1677,13 @@ async function cmdWorkout(client, rest) {
|
|
|
1667
1677
|
pw: { type: "string" },
|
|
1668
1678
|
yes: { type: "boolean" }
|
|
1669
1679
|
});
|
|
1670
|
-
const programId = toInt(need(values.program, "workout remove --program <id> --pw <id> --yes"), "--program");
|
|
1671
|
-
const pw = toInt(need(values.pw, "workout remove --program <id> --pw <id> --yes"), "--pw");
|
|
1680
|
+
const programId = toInt(need(values.program, "coach workout remove --program <id> --pw <id> --yes"), "--program");
|
|
1681
|
+
const pw = toInt(need(values.pw, "coach workout remove --program <id> --pw <id> --yes"), "--pw");
|
|
1672
1682
|
if (values.yes !== true) fail(`removing pw ${pw} deletes the session; add --yes.`);
|
|
1673
1683
|
await removeSession(client, programId, pw);
|
|
1674
1684
|
return out({ removed: pw });
|
|
1675
1685
|
}
|
|
1676
|
-
default: return fail("usage: trainheroic workout <build|read|publish|remove>");
|
|
1686
|
+
default: return fail("usage: trainheroic coach workout <build|read|publish|remove>");
|
|
1677
1687
|
}
|
|
1678
1688
|
}
|
|
1679
1689
|
async function cmdMessage(client, rest) {
|
|
@@ -1688,14 +1698,14 @@ async function cmdMessage(client, rest) {
|
|
|
1688
1698
|
})));
|
|
1689
1699
|
case "read": {
|
|
1690
1700
|
const { values, positionals } = parse(a, { limit: { type: "string" } });
|
|
1691
|
-
return out(await readLive(client, toInt(need(positionals[0], "message read <streamId> [--limit N]"), "streamId"), values.limit !== void 0 ? toInt(values.limit, "--limit") : 20));
|
|
1701
|
+
return out(await readLive(client, toInt(need(positionals[0], "coach message read <streamId> [--limit N]"), "streamId"), values.limit !== void 0 ? toInt(values.limit, "--limit") : 20));
|
|
1692
1702
|
}
|
|
1693
1703
|
case "draft": {
|
|
1694
1704
|
const { values, positionals } = parse(a, { "reply-to": { type: "string" } });
|
|
1695
1705
|
return out({
|
|
1696
1706
|
draft: true,
|
|
1697
1707
|
note: "NOT sent. Run 'message send' with --yes to deliver.",
|
|
1698
|
-
payload: buildCommentPayload(toInt(need(positionals[0], "message draft <streamId> <text>"), "streamId"), need(positionals.slice(1).join(" ").trim() || void 0, "message draft <streamId> <text>"), values["reply-to"] !== void 0 ? toInt(values["reply-to"], "--reply-to") : null)
|
|
1708
|
+
payload: buildCommentPayload(toInt(need(positionals[0], "coach message draft <streamId> <text>"), "streamId"), need(positionals.slice(1).join(" ").trim() || void 0, "coach message draft <streamId> <text>"), values["reply-to"] !== void 0 ? toInt(values["reply-to"], "--reply-to") : null)
|
|
1699
1709
|
});
|
|
1700
1710
|
}
|
|
1701
1711
|
case "send": {
|
|
@@ -1703,8 +1713,8 @@ async function cmdMessage(client, rest) {
|
|
|
1703
1713
|
"reply-to": { type: "string" },
|
|
1704
1714
|
yes: { type: "boolean" }
|
|
1705
1715
|
});
|
|
1706
|
-
const streamId = toInt(need(positionals[0], "message send <streamId> <text> --yes"), "streamId");
|
|
1707
|
-
const text = need(positionals.slice(1).join(" ").trim() || void 0, "message send <streamId> <text> --yes");
|
|
1716
|
+
const streamId = toInt(need(positionals[0], "coach message send <streamId> <text> --yes"), "streamId");
|
|
1717
|
+
const text = need(positionals.slice(1).join(" ").trim() || void 0, "coach message send <streamId> <text> --yes");
|
|
1708
1718
|
const replyTo = values["reply-to"] !== void 0 ? toInt(values["reply-to"], "--reply-to") : null;
|
|
1709
1719
|
if (values.yes !== true) fail(`sending to stream ${streamId} is athlete-facing and immediate; add --yes.`);
|
|
1710
1720
|
return out({
|
|
@@ -1714,15 +1724,15 @@ async function cmdMessage(client, rest) {
|
|
|
1714
1724
|
}
|
|
1715
1725
|
case "delete": {
|
|
1716
1726
|
const { values, positionals } = parse(a, { yes: { type: "boolean" } });
|
|
1717
|
-
const streamId = toInt(need(positionals[0], "message delete <streamId> <commentId> --yes"), "streamId");
|
|
1718
|
-
const commentId = toInt(need(positionals[1], "message delete <streamId> <commentId> --yes"), "commentId");
|
|
1727
|
+
const streamId = toInt(need(positionals[0], "coach message delete <streamId> <commentId> --yes"), "streamId");
|
|
1728
|
+
const commentId = toInt(need(positionals[1], "coach message delete <streamId> <commentId> --yes"), "commentId");
|
|
1719
1729
|
if (values.yes !== true) fail(`deleting comment ${commentId} acts on the live account; add --yes.`);
|
|
1720
1730
|
return out({
|
|
1721
1731
|
deleted: true,
|
|
1722
1732
|
response: await deleteComment(client, streamId, commentId)
|
|
1723
1733
|
});
|
|
1724
1734
|
}
|
|
1725
|
-
default: return fail("usage: trainheroic message <list|read|draft|send|delete>");
|
|
1735
|
+
default: return fail("usage: trainheroic coach message <list|read|draft|send|delete>");
|
|
1726
1736
|
}
|
|
1727
1737
|
}
|
|
1728
1738
|
function isoDate(value, label) {
|
|
@@ -1883,6 +1893,28 @@ async function cmdAthlete(client, rest) {
|
|
|
1883
1893
|
default: return fail("usage: trainheroic athlete <whoami|profile|prefs|workouts|exercises|history|prs|stats|working-maxes|leaderboard|export|log-set>");
|
|
1884
1894
|
}
|
|
1885
1895
|
}
|
|
1896
|
+
async function cmdSkill(rest) {
|
|
1897
|
+
const skillRoot = join(import.meta.dirname, "../skill");
|
|
1898
|
+
const positional = rest.find((r) => !r.startsWith("-"));
|
|
1899
|
+
if (positional === "list") return out((await readdir(skillRoot, { withFileTypes: true })).filter((e) => e.isDirectory()).map((e) => e.name));
|
|
1900
|
+
const name = positional ?? "trainheroic-unofficial";
|
|
1901
|
+
const dir = join(skillRoot, name);
|
|
1902
|
+
let text;
|
|
1903
|
+
try {
|
|
1904
|
+
text = await readFile(join(dir, "SKILL.md"), "utf8");
|
|
1905
|
+
} catch {
|
|
1906
|
+
return fail(`unknown skill "${name}". Run 'trainheroic skill list'.`);
|
|
1907
|
+
}
|
|
1908
|
+
process.stdout.write(text);
|
|
1909
|
+
if (rest.includes("--full")) try {
|
|
1910
|
+
const refDir = join(dir, "references");
|
|
1911
|
+
const refs = (await readdir(refDir)).filter((f) => f.endsWith(".md")).sort();
|
|
1912
|
+
for (const f of refs) {
|
|
1913
|
+
process.stdout.write(`\n\n===== references/${f} =====\n\n`);
|
|
1914
|
+
process.stdout.write(await readFile(join(refDir, f), "utf8"));
|
|
1915
|
+
}
|
|
1916
|
+
} catch {}
|
|
1917
|
+
}
|
|
1886
1918
|
async function cmdInstallSkill() {
|
|
1887
1919
|
const home = process.env.HOME ?? process.env.USERPROFILE;
|
|
1888
1920
|
if (!home) fail("cannot determine home directory.");
|
|
@@ -1902,22 +1934,30 @@ async function cmdInstallSkill() {
|
|
|
1902
1934
|
}
|
|
1903
1935
|
out({ installed });
|
|
1904
1936
|
}
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1937
|
+
const COACH_USAGE = "usage: trainheroic coach <head-coach|athletes|programs|teams|notifications|analytics|program <id>|team <id>|team-codes <id>|exercise|workout|message>";
|
|
1938
|
+
async function cmdCoach(client, rest) {
|
|
1939
|
+
const [sub, ...a] = rest;
|
|
1940
|
+
switch (sub) {
|
|
1908
1941
|
case "head-coach": return out(await get(client, "/v5/headCoach"));
|
|
1909
1942
|
case "athletes": return out(await get(client, "/v5/athletes"));
|
|
1910
1943
|
case "programs": return out(await get(client, "/1.0/coach/programs"));
|
|
1911
1944
|
case "teams": return out(await get(client, "/1.0/coach/teams"));
|
|
1912
1945
|
case "notifications": return out(await get(client, "/v5/notifications/counts"));
|
|
1913
1946
|
case "analytics": return out(await get(client, "/v5/analytics"));
|
|
1914
|
-
case "program": return out(await get(client, `/3.0/coach/program/${encodeURIComponent(need(
|
|
1915
|
-
case "team": return out(await get(client, `/v5/teams/${encodeURIComponent(need(
|
|
1916
|
-
case "team-codes": return out(await get(client, `/v5/teams/${encodeURIComponent(need(
|
|
1947
|
+
case "program": return out(await get(client, `/3.0/coach/program/${encodeURIComponent(need(a[0], "coach program <id>"))}`));
|
|
1948
|
+
case "team": return out(await get(client, `/v5/teams/${encodeURIComponent(need(a[0], "coach team <id>"))}`));
|
|
1949
|
+
case "team-codes": return out(await get(client, `/v5/teams/${encodeURIComponent(need(a[0], "coach team-codes <id>"))}/teamCodes`));
|
|
1950
|
+
case "exercise": return cmdExercise(client, a);
|
|
1951
|
+
case "workout": return cmdWorkout(client, a);
|
|
1952
|
+
case "message": return cmdMessage(client, a);
|
|
1953
|
+
default: return fail(COACH_USAGE);
|
|
1954
|
+
}
|
|
1955
|
+
}
|
|
1956
|
+
async function dispatch(client, group, rest) {
|
|
1957
|
+
switch (group) {
|
|
1958
|
+
case "whoami": return out(await get(client, "/user/simple"));
|
|
1917
1959
|
case "request": return cmdRequest(client, rest);
|
|
1918
|
-
case "
|
|
1919
|
-
case "workout": return cmdWorkout(client, rest);
|
|
1920
|
-
case "message": return cmdMessage(client, rest);
|
|
1960
|
+
case "coach": return cmdCoach(client, rest);
|
|
1921
1961
|
case "athlete": return cmdAthlete(client, rest);
|
|
1922
1962
|
default: return fail(`unknown command "${group}". Run 'trainheroic help'.`);
|
|
1923
1963
|
}
|
|
@@ -1932,6 +1972,10 @@ async function main() {
|
|
|
1932
1972
|
await cmdInstallSkill();
|
|
1933
1973
|
return;
|
|
1934
1974
|
}
|
|
1975
|
+
if (group === "skill") {
|
|
1976
|
+
await cmdSkill(rest);
|
|
1977
|
+
return;
|
|
1978
|
+
}
|
|
1935
1979
|
const email = process.env.TRAINHEROIC_EMAIL;
|
|
1936
1980
|
const password = process.env.TRAINHEROIC_PASSWORD;
|
|
1937
1981
|
if (!email || !password) fail("set TRAINHEROIC_EMAIL and TRAINHEROIC_PASSWORD in the environment.");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@trainheroic-unofficial/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -21,8 +21,8 @@
|
|
|
21
21
|
],
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"zod": "^4.4.3",
|
|
24
|
-
"@trainheroic-unofficial/dto": "0.
|
|
25
|
-
"@trainheroic-unofficial/js": "0.
|
|
24
|
+
"@trainheroic-unofficial/dto": "0.5.0",
|
|
25
|
+
"@trainheroic-unofficial/js": "0.5.0"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"@types/node": "^26.0.0",
|
|
@@ -76,24 +76,25 @@ cat block.json | $TH request POST /2.0/coach/calendar/saveProgramWorkoutSets
|
|
|
76
76
|
```
|
|
77
77
|
|
|
78
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).
|
|
80
|
-
reads: `
|
|
81
|
-
`
|
|
79
|
+
`--base apis` (the default base is the coach host). `whoami` and `request` are top-level;
|
|
80
|
+
the coach reads live under the `coach` group: `coach head-coach`, `coach athletes`,
|
|
81
|
+
`coach programs`, `coach teams`, `coach notifications`, `coach analytics`,
|
|
82
|
+
`coach program <id>`, `coach team <id>`, `coach team-codes <id>`.
|
|
82
83
|
|
|
83
84
|
Load `references/api-reference.md` when you need an endpoint's exact request or
|
|
84
85
|
response shape, or an area not covered here.
|
|
85
86
|
|
|
86
87
|
## What you can do
|
|
87
88
|
|
|
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/*`
|
|
89
|
+
| Area | How |
|
|
90
|
+
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
91
|
+
| Athletes | `$TH coach athletes`; invite/archive/restore via `$TH request ...` |
|
|
92
|
+
| Teams | `$TH coach teams`, `$TH coach team <id>`, `$TH coach team-codes <id>`; create via `$TH request POST /1.0/coach/team/createWithTitleAndCode` |
|
|
93
|
+
| Programs | `$TH coach programs`, `$TH coach program <id>` |
|
|
94
|
+
| Exercises | `$TH coach exercise resolve\|search\|get\|sync\|create\|forget\|stats` (below) |
|
|
95
|
+
| Sessions / workouts | `$TH coach workout build\|read\|publish\|remove` (below) |
|
|
96
|
+
| Messaging | `$TH coach message list\|read\|draft\|send\|delete` (below) |
|
|
97
|
+
| Analytics | `$TH coach analytics`, then `$TH request POST /v5/analytics/*` |
|
|
97
98
|
|
|
98
99
|
## Resolving exercises
|
|
99
100
|
|
|
@@ -102,9 +103,9 @@ cache, which mirrors the library into `~/.trainheroic/library.json` and serves l
|
|
|
102
103
|
locally:
|
|
103
104
|
|
|
104
105
|
```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)
|
|
106
|
+
$TH coach exercise resolve "Back Squat" # exact/best match -> id (+ unit labels)
|
|
107
|
+
$TH coach exercise search "incline press" # ranked candidates
|
|
108
|
+
$TH coach exercise sync --force # refresh the mirror (7-day TTL otherwise)
|
|
108
109
|
```
|
|
109
110
|
|
|
110
111
|
`resolve` returns `match: null` plus a candidate list when a name is ambiguous; pick the
|
|
@@ -114,11 +115,11 @@ After creating a custom exercise, go through the cache so the mirror stays curre
|
|
|
114
115
|
deleting one via the API, drop it from the mirror:
|
|
115
116
|
|
|
116
117
|
```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
|
|
118
|
+
$TH coach exercise create '{"title":"Sandbag Clean","param_1_type":3,"param_2_type":1}'
|
|
119
|
+
$TH coach exercise forget 7721170 --yes # cache-only, run after an API delete
|
|
119
120
|
```
|
|
120
121
|
|
|
121
|
-
`$TH exercise stats` shows the cached row count.
|
|
122
|
+
`$TH coach exercise stats` shows the cached row count.
|
|
122
123
|
|
|
123
124
|
## Building a workout
|
|
124
125
|
|
|
@@ -130,7 +131,7 @@ follow-up questions first; do not guess.** Restate the program you intend to bui
|
|
|
130
131
|
explicit confirmation before publishing. Build a draft (the default), show the read-back
|
|
131
132
|
for review, and only publish once the user confirms.
|
|
132
133
|
|
|
133
|
-
`$TH workout build` runs the whole sequence (program → session → blocks → exercises),
|
|
134
|
+
`$TH coach workout build` runs the whole sequence (program → session → blocks → exercises),
|
|
134
135
|
fills every field that otherwise makes the exercise step return HTTP 500, encodes
|
|
135
136
|
prescriptions correctly, and prints unit advisories plus a read-back. Feed it a JSON spec
|
|
136
137
|
(inline, `--file`, or stdin):
|
|
@@ -146,15 +147,15 @@ cat > day.json <<'JSON'
|
|
|
146
147
|
] }
|
|
147
148
|
JSON
|
|
148
149
|
|
|
149
|
-
$TH workout build --program 4980851 --date 2026-6-22 --file day.json
|
|
150
|
-
$TH workout publish --pw <id> --yes
|
|
150
|
+
$TH coach workout build --program 4980851 --date 2026-6-22 --file day.json # draft
|
|
151
|
+
$TH coach workout publish --pw <id> --yes # publish after review
|
|
151
152
|
```
|
|
152
153
|
|
|
153
154
|
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
|
|
155
|
+
step, or publish later with `$TH coach workout publish --pw <id> --yes`. Read a built session
|
|
156
|
+
back with `$TH coach workout read --program <id> --date Y-M-D --pw <id>`. There is no in-place
|
|
156
157
|
replace: to rebuild a date, remove the old session first with
|
|
157
|
-
`$TH workout remove --program <id> --pw <id> --yes`.
|
|
158
|
+
`$TH coach workout remove --program <id> --pw <id> --yes`.
|
|
158
159
|
|
|
159
160
|
Two exercises in one block become a superset. Add `"leaderboard": "rounds"` (or
|
|
160
161
|
`reps`/`time`/`calories`/`meters`/… or `{"unit":"time","lowest_wins":true}`) to a block to
|
|
@@ -178,17 +179,17 @@ draft state, so a POST is delivered at once. The CLI therefore separates draftin
|
|
|
178
179
|
sending and requires `--yes` to actually send or delete.
|
|
179
180
|
|
|
180
181
|
```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)
|
|
182
|
+
$TH coach message list # conversations (id, kind, title)
|
|
183
|
+
$TH coach message read 37730920 # recent messages
|
|
184
|
+
$TH coach message draft 37730920 "Great session today!" # PREVIEW only — never sends
|
|
185
|
+
$TH coach message send 37730920 "Great session today!" --yes # delivers (gate behind user OK)
|
|
186
|
+
$TH coach message send 37730920 "Nice PR!" --reply-to 125652586 --yes # threaded reply
|
|
187
|
+
$TH coach message delete 37730920 <commentId> --yes # soft delete (destructive)
|
|
187
188
|
```
|
|
188
189
|
|
|
189
190
|
Default to `draft`: it prints the exact payload and target without touching the account.
|
|
190
191
|
Run `send` only after the user confirms the wording and recipient. Check
|
|
191
|
-
`$TH notifications` (`countMessagingNotViewed`) first as a cheap "anything new?" gate.
|
|
192
|
+
`$TH coach notifications` (`countMessagingNotViewed`) first as a cheap "anything new?" gate.
|
|
192
193
|
|
|
193
194
|
## Gotchas
|
|
194
195
|
|
|
@@ -204,7 +205,7 @@ Environment-specific facts that defy reasonable assumptions:
|
|
|
204
205
|
"200 m run" on it shows as 200 _miles_. And `param_2_type` `2` (% of max) and `14` (RPE)
|
|
205
206
|
coerce to weight on a weight lift, rendering as pounds — put % or RPE in the
|
|
206
207
|
`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
|
+
no-secondary-param lift (weighted Pull-Ups). Check units with `$TH coach exercise resolve`
|
|
208
209
|
(the `units` array, ordered by entry slot `[param 1, param 2]`); the builder prints a
|
|
209
210
|
warning when a sent type will be overridden. "Max"/"AMRAP" reps work as free text in the
|
|
210
211
|
rep slots.
|
|
@@ -235,9 +236,9 @@ Environment-specific facts that defy reasonable assumptions:
|
|
|
235
236
|
3. **offer the user the option to do it themselves** — hand them the exact command (e.g.
|
|
236
237
|
`$TH request DELETE /v5/teamCodes/874586`) or the UI steps.
|
|
237
238
|
|
|
238
|
-
The CLI enforces a `--yes` flag on `message send`/`message delete`,
|
|
239
|
-
`workout remove`, and `exercise forget`, but the gate
|
|
240
|
-
moment before adding `--yes`.
|
|
239
|
+
The CLI enforces a `--yes` flag on `coach message send`/`coach message delete`,
|
|
240
|
+
`coach workout publish`, `coach workout remove`, and `coach exercise forget`, but the gate
|
|
241
|
+
above still applies: confirm in the moment before adding `--yes`.
|
|
241
242
|
|
|
242
243
|
## Beyond the CLI
|
|
243
244
|
|
|
@@ -554,7 +554,7 @@ This creates A1: Back Squat, A2: Front Squat displayed as a superset.
|
|
|
554
554
|
> `Run` (id 82) is `10` (miles); sending `6` (meters) is ignored and `200` shows as
|
|
555
555
|
> _200 miles_. To program meters, pick a meters-native exercise (`Sprint` 127,
|
|
556
556
|
> `Rowing` 101, `Shuttle Sprint` 42523) or a custom exercise — there is no metric
|
|
557
|
-
> "Run". `$TH exercise resolve` now prints a `units` array (ordered by entry slot,
|
|
557
|
+
> "Run". `$TH coach exercise resolve` now prints a `units` array (ordered by entry slot,
|
|
558
558
|
> `[param 1, param 2]`); check it.
|
|
559
559
|
> - **`param_2_type` (secondary) is forced to the default too, with one exception:**
|
|
560
560
|
> if the exercise has no secondary param (default `0`/none) you may add weight
|
|
@@ -753,7 +753,7 @@ Creates a reusable session template in the library.
|
|
|
753
753
|
|
|
754
754
|
Chat between a coach and athletes/teams. A **stream** is a conversation; a
|
|
755
755
|
**comment** is a message in it. Verified against a live
|
|
756
|
-
account (see `$TH message`).
|
|
756
|
+
account (see `$TH coach message`).
|
|
757
757
|
|
|
758
758
|
| Method | Endpoint | Description |
|
|
759
759
|
| ------ | -------------------------------------------------------------- | ------------------------------------------------------------------------------ |
|
|
@@ -229,7 +229,7 @@ sent under the wrong assumed unit silently renders under the exercise's real uni
|
|
|
229
229
|
(`1`) to an exercise that has no secondary param (default `0`/none) — that is how
|
|
230
230
|
weighted Pull-Ups/Dips work. `2` (% of max) and `14` (RPE) do **not** stick on a
|
|
231
231
|
weight-default lift; both coerce to weight and render as pounds.
|
|
232
|
-
- Check an exercise's real units first: `$TH exercise resolve "<name>"` prints a
|
|
232
|
+
- Check an exercise's real units first: `$TH coach exercise resolve "<name>"` prints a
|
|
233
233
|
`units` array, ordered by entry slot (`[param 1, param 2]`). the workout builder also
|
|
234
234
|
prints a `WARNING` when a sent param type will be overridden, and its read-back labels
|
|
235
235
|
the stored units.
|