technogym-js 0.0.0-stage → 0.1.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/AGENTS.md ADDED
@@ -0,0 +1,83 @@
1
+ # AGENTS.md — technogym-js
2
+
3
+ Briefing for coding agents. Not a tutorial; see `README.md` for that. This file states facts an
4
+ agent needs to avoid confidently wrong code.
5
+
6
+ ## 1. What this is, and the hard constraints
7
+
8
+ `technogym-js` is a zero-dependency TypeScript client for Technogym's mywellness cloud, for **Node
9
+ and Next.js server runtimes**. It talks to the undocumented JSON API of the official end-user web
10
+ app. There is no official public API; do not invent endpoints.
11
+
12
+ - **Read-only.** No method changes anything on the account. Every mywellness call, reads
13
+ included, is an HTTP POST; the transport retries reads (`ServiceOptions.retry`, default `true`)
14
+ but never the login POST.
15
+ - **A session is addressed by `sessionId` AND its date.** Sessions are partitioned by day. Methods
16
+ take `date` as optional only for convenience: without it `findSessionDate` scans a year of
17
+ history first (one extra request). Pass it whenever you have it.
18
+ - **`partitionDate` is a STRING (`"20260908"`) for `GetPerformedWorkoutSessionByIdCr` but a
19
+ NUMBER (`20260908`) for `GetHrSession`.** This mirrors the web app; do not "fix" it.
20
+ - **The API spells two keys wrong, and the parsers depend on it:** `analitics` (the analytics
21
+ block; `analytics` is accepted as a fallback) and `displayPrescibedPhysicalActivity`.
22
+ - **Every request carries `X-MWAPPS-APPID` / `X-MWAPPS-CLIENT` / `X-MWAPPS-CLIENTVERSION`**
23
+ (`src/auth/constants.ts`). Without them mywellness answers `ClientApplicationNotTrusted`.
24
+ - **Errors come in a 200.** mywellness returns `{ data, errors }`; a non-empty `errors` is a
25
+ failure. Auth-looking `details` (`TokenNotValid`, `TokenExpired`, anything containing "token")
26
+ become `TechnogymAuthError`; the rest `TechnogymApiError`.
27
+ - **Re-login is automatic only with credentials** (constructor `credentials`, or a prior
28
+ `login()` on the same client). A session loaded from a store with no credentials throws
29
+ `TechnogymAuthError` when rejected. Concurrent re-logins collapse into one request.
30
+ - **Rotated tokens:** when a response envelope carries a new `token`, the client adopts it and
31
+ saves it to the store.
32
+ - **Property units:** `value` is the base unit (s, m, kg); `unit` belongs to `displayValue`, not
33
+ to `value`. A Skillrun `Duration` is `value: 1847` (seconds) with `displayValue: 30.78`,
34
+ `unit: "min"`.
35
+ - **Strength exercises have no time series.** Use `Exercise.sets`, not `getExerciseAnalytics`.
36
+ - **Node runtime only, server-only, zero runtime dependencies, Node >= 18.**
37
+
38
+ ## 2. Minimal working example
39
+
40
+ ```ts
41
+ import { FileSessionStore, Technogym, TechnogymClient } from "technogym-js";
42
+
43
+ const client = new TechnogymClient({ sessionStore: new FileSessionStore("./session") });
44
+ if (!(await client.loadSession())) await client.login(process.env.TECHNOGYM_EMAIL!, process.env.TECHNOGYM_PASSWORD!);
45
+
46
+ const technogym = new Technogym(client);
47
+ const [latest] = await technogym.listSessions("2026-09-01", "2026-09-30");
48
+ const session = await technogym.getWorkoutSession(latest!.sessionId, latest!.date);
49
+ ```
50
+
51
+ ## 3. Methods and endpoints
52
+
53
+ All training paths are `POST https://services.mywellness.com/{facilityUrl}{path}`. An account can
54
+ have NO facilities (the login's `facilities` is `[]`); then paths go out with no prefix. Verified
55
+ live 2026-10-31: on such an account the server answered identically with no prefix and with any
56
+ made-up prefix.
57
+
58
+ Live-verified on 2026-10-31 (one account, bike sessions): login, `getMovergy`, `listSessions`,
59
+ `findSessionDate`, `getWorkoutSession`, `getExerciseAnalytics` / `analyzeExercise` (channels
60
+ Power, Rpm, HDistance, Level, Calories). `getSessionHeartRate` returned an empty trace for a bike
61
+ session whose exercise analytics did carry HR; for per-exercise HR use `ExerciseAnalytics.hr`.
62
+
63
+ | Method | Path | Body |
64
+ |---|---|---|
65
+ | `TechnogymClient.login` | `POST core.mywellness.com/v2/enduser/authentication/login` | `{username, password, keepMeLoggedIn: true}` |
66
+ | `getMovergy` | `/Training/User/{userId}/MyMovergy` | `{}` |
67
+ | `listSessions`, `getActivityHistoryRaw`, `findSessionDate` | `/Training/User/{userId}/ActivityHistory` | `{startDay, endDay[, justThisType: "WorkoutSession"]}` |
68
+ | `getWorkoutSession[Raw]` | `/Training/User/{userId}/GetPerformedWorkoutSessionByIdCr` | `{idCr: number, partitionDate: "yyyyMMdd"}` |
69
+ | `getSessionHeartRate[Raw]` | `/Training/User/{userId}/GetHrSession` | `{idCr: number, partitionDate: yyyyMMdd}` |
70
+ | `getExerciseAnalytics[Raw]`, `analyzeExercise` | `/Training/CardioLog/{analyticsId}/Details` | `{}` |
71
+
72
+ `getUser()` makes no request. `findSessionDate` searches every activity type
73
+ (`onlyWorkouts: false`). `getRecentWorkouts` loads sessions sequentially on purpose.
74
+
75
+ Request shapes follow health_mcp's working Python client.
76
+
77
+ ## 4. Layout
78
+
79
+ `src/client.ts` transport and session · `src/technogym.ts` endpoint methods · `src/parse.ts`
80
+ pure parsers · `src/analysis.ts` stats and segmentation · `src/auth/` constants and session
81
+ stores · `mcp/` the MCP server, which may import only from `"technogym-js"` (lint-enforced).
82
+
83
+ `npm run check` runs typecheck, lint, build and both test suites.
package/CHANGELOG.md ADDED
@@ -0,0 +1,15 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 — 2026-10-31
4
+
5
+ First release.
6
+
7
+ - `TechnogymClient`: mywellness sign-in, session stores (`FileSessionStore`,
8
+ `MemorySessionStore`), automatic re-login with credentials, rotated-token adoption, retries for
9
+ reads, typed error hierarchy.
10
+ - `Technogym`: `getUser`, `getMovergy`, `listSessions`, `findSessionDate`, `getWorkoutSession`,
11
+ `getRecentWorkouts`, `getExerciseAnalytics`, `analyzeExercise`, `getSessionHeartRate`, and raw
12
+ twins of each read.
13
+ - Analytics helpers: `channelStats`, `segmentAnalytics` (interval detection from setpoint
14
+ changes, laps, fixed windows), `windowSamples`, `downsample`.
15
+ - `@dynamicsninja/technogym-mcp`: an MCP server with six read-only tools.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ivan Ficko
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/NOTICE ADDED
@@ -0,0 +1,12 @@
1
+ technogym-js
2
+
3
+ The endpoint surface, request shapes and response parsing of this library follow the MIT-licensed
4
+ Technogym MCP server in health_mcp:
5
+
6
+ health_mcp/technogym — https://github.com/ErikAnkerKilbergSkallevold/health_mcp/tree/main/technogym
7
+ Copyright (c) Erik Anker Kilberg Skallevold and contributors
8
+
9
+ That project is not affiliated with this one.
10
+
11
+ Technogym and mywellness are trademarks of Technogym S.p.A. This project is not affiliated with,
12
+ endorsed by, or supported by Technogym.
package/README.md CHANGED
@@ -1,3 +1,148 @@
1
- # Temporary Holding Version
1
+ <div align="center">
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ # technogym-js
4
+
5
+ **TypeScript client for Technogym mywellness**
6
+
7
+ <sub>**Unofficial.** Not affiliated with, endorsed by, or supported by Technogym. Technogym and
8
+ mywellness are trademarks of Technogym S.p.A.</sub>
9
+
10
+ </div>
11
+
12
+ A zero-dependency TypeScript client for Technogym's **mywellness** cloud, for **Node and Next.js
13
+ server runtimes**. It reads your gym training history: workout sessions, every exercise with
14
+ per-set reps and weight, the machine's full summary (a Skillrun's running cadence, power, step
15
+ length and ground contact time; a strength machine's estimated 1RM), per-second treadmill and
16
+ bike data, and whole-session heart rate.
17
+
18
+ Technogym's official API is partner-only. This library speaks the same JSON API the official
19
+ end-user web app (endusernext.mywellness.com) uses, signing in with your normal email and password.
20
+ No HTML scraping, but it will break if Technogym changes the login flow or its client-trust rules.
21
+
22
+ On top of the endpoints, it does the analysis the API leaves out: the summary block omits most
23
+ machine channels, so `analyzeExercise` computes per-channel stats and splits a run into **intervals
24
+ detected from setpoint changes** (speed, grade, level), console laps, or fixed windows.
25
+
26
+ If you're an AI coding agent (or configuring one), read [`AGENTS.md`](AGENTS.md) first.
27
+
28
+ ## 📦 Install
29
+
30
+ ```sh
31
+ npm install technogym-js
32
+ ```
33
+
34
+ Node 18 or later. Server-only: credentials and the session token must never reach a browser.
35
+
36
+ ## 🚀 Quick start
37
+
38
+ ```ts
39
+ import { FileSessionStore, Technogym, TechnogymClient } from "technogym-js";
40
+
41
+ const client = new TechnogymClient({ sessionStore: new FileSessionStore("./session") });
42
+ if (!(await client.loadSession())) {
43
+ await client.login(process.env.TECHNOGYM_EMAIL!, process.env.TECHNOGYM_PASSWORD!);
44
+ }
45
+
46
+ const technogym = new Technogym(client);
47
+
48
+ // Overview: newest first. sessionId + date address a session.
49
+ const sessions = await technogym.listSessions("2026-09-01", "2026-09-30");
50
+
51
+ // Every exercise, sets, machine summary, efficacy and muscle scores.
52
+ const session = await technogym.getWorkoutSession(sessions[0].sessionId, sessions[0].date);
53
+
54
+ // Per-second data of a cardio exercise, plus intervals and per-channel stats.
55
+ const run = session.exercises.find((e) => e.isCardio)!;
56
+ const analyzed = await technogym.analyzeExercise(run.analyticsId);
57
+ for (const s of analyzed.segments) {
58
+ console.log(s.interval, s.setpoint, s.paceMinPerKm, s.channels.RunningCadence);
59
+ }
60
+ ```
61
+
62
+ Or let the client sign in on its own, lazily and again whenever the token is rejected:
63
+
64
+ ```ts
65
+ const client = new TechnogymClient({
66
+ credentials: { email: process.env.TECHNOGYM_EMAIL!, password: process.env.TECHNOGYM_PASSWORD! },
67
+ });
68
+ ```
69
+
70
+ In a Next.js route handler add `export const runtime = "nodejs";`.
71
+
72
+ ## 📚 API
73
+
74
+ ### `Technogym`
75
+
76
+ | Method | Returns |
77
+ |---|---|
78
+ | `getUser()` | The account: user id, name, culture, measurement system, facilities. From the session, no request. |
79
+ | `getMovergy()` | The current Movergy score, or `null`. |
80
+ | `listSessions(from, to, { onlyWorkouts = true })` | `SessionSummary[]`, newest first: id, date, time, name, duration, MOVEs, calories, exercise counts, effectiveness and compliance. |
81
+ | `findSessionDate(sessionId, lookbackDays = 365)` | A session's `YYYY-MM-DD`, found by scanning history. |
82
+ | `getWorkoutSession(sessionId, date?)` | `WorkoutSession`: every performed exercise with machine, sets, prescribed sets, summary, workload ranges, estimated figures, console metadata; session efficacy, muscle scores, totals. |
83
+ | `getRecentWorkouts(days = 14)` | `WorkoutSession[]` for the last N days. |
84
+ | `getExerciseAnalytics(analyticsId)` | `ExerciseAnalytics`: channel names, 1 Hz samples, HR, HR zones, laps, summary. |
85
+ | `analyzeExercise(analyticsId, { segmentBy, segmentSeconds })` | The above plus `channelStats`, `segmentMode` and `segments`. |
86
+ | `getSessionHeartRate(sessionId, date?)` | `HrSession`: timestamped HR, zones, avg/min/max HR. |
87
+ | `getWorkoutSessionRaw`, `getExerciseAnalyticsRaw`, `getSessionHeartRateRaw`, `getActivityHistoryRaw` | The untouched payloads, for anything the types do not model. |
88
+
89
+ Pass `date` whenever you have it: the API partitions sessions by day, and without it the last year
90
+ of history is scanned first. Dates are `YYYY-MM-DD` strings or `Date`s (their local calendar day).
91
+
92
+ Every metric is a `Property` of `{ value, displayValue, unit, formatted }`: `value` is in the API's
93
+ base unit (seconds, metres, kg), `displayValue` is in `unit` (what the console shows), and
94
+ `formatted` is the localised string. Names and units come back in the account's culture.
95
+
96
+ ### Analytics helpers
97
+
98
+ Pure functions, exported for samples you already hold: `channelStats(samples, channels)`,
99
+ `segmentAnalytics(samples, channels, laps, { segmentBy, segmentSeconds })`,
100
+ `windowSamples(rows, fromSec, toSec)`, `downsample(rows, step)`.
101
+
102
+ `segmentBy` is `auto` (default: intervals when there are at least two, else laps, else fixed
103
+ windows), `workload`, `laps` or `fixed`. A 10 × (1 min hard / 1 min easy) treadmill session yields
104
+ 20 segments, each with its setpoint, pace, distance covered and avg / max / min of every channel.
105
+ Ramps between setpoints are folded into the interval they lead into.
106
+
107
+ ### `TechnogymClient`
108
+
109
+ | Member | |
110
+ |---|---|
111
+ | `new TechnogymClient(options)` | `sessionStore`, `credentials`, `facility` (id or url slug; default the first), `timeoutMs` (30 s), `retries` (3), `backoffMs`, `fetchImpl`. |
112
+ | `login(email, password)` | Signs in and saves the session. Keeps the credentials in memory to renew a rejected token. |
113
+ | `loadSession()` / `setSession()` / `getSession()` / `logout()` | Session handling. |
114
+ | `services(path, body?)` | POST to any training endpoint under the facility; `{userId}` in `path` is filled in. For endpoints `Technogym` has no method for. |
115
+
116
+ Sessions are stored by `FileSessionStore(dir)` (`session.json`, mode 0600) or
117
+ `MemorySessionStore`; implement `SessionStore` for a database.
118
+
119
+ ### Errors
120
+
121
+ All extend `TechnogymError`: `TechnogymAuthError` (login failed, no session, token rejected),
122
+ `TechnogymApiError` (a 200 whose `errors` envelope is not about the session; `.action`,
123
+ `.errors`), `TechnogymNotFoundError`, `TechnogymRateLimitError` (`.retryAfter`),
124
+ `TechnogymHttpError` (`.status`, `.url`, `.body`), `TechnogymConnectionError`.
125
+
126
+ ## 🤖 MCP server
127
+
128
+ [`mcp/`](mcp/README.md) is an MCP server built on this library, exposing six read-only tools to
129
+ Claude and other MCP clients.
130
+
131
+ ## 🛠️ Development
132
+
133
+ ```sh
134
+ cp .env.example .env # fill in TECHNOGYM_EMAIL / TECHNOGYM_PASSWORD
135
+ npm install
136
+ npm run check # typecheck, lint, build, test (library and MCP server)
137
+ npm run login && npm run demo
138
+ ```
139
+
140
+ ## 🙏 Credits
141
+
142
+ The endpoint surface and parsing follow the Technogym server in
143
+ [health_mcp](https://github.com/ErikAnkerKilbergSkallevold/health_mcp/tree/main/technogym)
144
+ (MIT). See [`NOTICE`](NOTICE).
145
+
146
+ ## License
147
+
148
+ MIT