@trainheroic-unofficial/athlete-mcp 3.5.2 → 3.7.1

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.
Files changed (2) hide show
  1. package/dist/server.mjs +360 -117
  2. package/package.json +7 -7
package/dist/server.mjs CHANGED
@@ -441,10 +441,35 @@ z.object({
441
441
  match: exerciseViewSchema.nullable(),
442
442
  candidates: z.array(exerciseViewSchema)
443
443
  });
444
+ /** TrainHeroic's documented exercise parameter-type codes. */
445
+ const exerciseParamTypeSchema = z.union([
446
+ z.literal(0),
447
+ z.literal(1),
448
+ z.literal(2),
449
+ z.literal(3),
450
+ z.literal(4),
451
+ z.literal(5),
452
+ z.literal(6),
453
+ z.literal(7),
454
+ z.literal(10),
455
+ z.literal(11),
456
+ z.literal(12),
457
+ z.literal(13),
458
+ z.literal(14),
459
+ z.literal(18)
460
+ ]);
461
+ const exerciseWriteShape = {
462
+ title: z.string().trim().min(1),
463
+ param_1_type: exerciseParamTypeSchema.optional(),
464
+ param_2_type: exerciseParamTypeSchema.optional()
465
+ };
444
466
  z.looseObject({
445
- title: z.string().min(1),
446
- param_1_type: z.number().optional(),
447
- param_2_type: z.number().optional()
467
+ ...exerciseWriteShape,
468
+ points_of_performance: z.string().default("")
469
+ });
470
+ z.looseObject({
471
+ ...exerciseWriteShape,
472
+ points_of_performance: z.string().optional()
448
473
  });
449
474
  z.object({
450
475
  streamId: idSchema,
@@ -1000,34 +1025,234 @@ function confirmGate(ctx, message, confirmArg) {
1000
1025
  */
1001
1026
  const SERVER_INSTRUCTIONS = "Speak to the user in plain, everyday language about their training. Describe what you are doing in the TrainHeroic app's own terms (for example, say you are creating a workout rather than naming a tool). Do not surface internal tool names (the snake_case identifiers such as athlete_session_create), raw parameter names, or numeric ids in your replies unless the user explicitly asks for them; they are implementation details. The tool descriptions cross-reference each other by name only so you can chain them correctly. Keep that wiring to yourself.";
1002
1027
  //#endregion
1028
+ //#region ../js/src/date-window.ts
1029
+ const DAY_MS = 864e5;
1030
+ const ISO_DAY = /^\d{4}-\d{2}-\d{2}$/u;
1031
+ function dayNumber(value) {
1032
+ if (!ISO_DAY.test(value)) return null;
1033
+ const timestamp = Date.parse(`${value}T00:00:00Z`);
1034
+ if (!Number.isFinite(timestamp) || new Date(timestamp).toISOString().slice(0, 10) !== value) return null;
1035
+ return timestamp / DAY_MS;
1036
+ }
1037
+ /** Number of calendar days in an inclusive ISO-date range, or null for an invalid range. */
1038
+ function dateSpanDays(start, end) {
1039
+ const first = dayNumber(start);
1040
+ const last = dayNumber(end);
1041
+ return first === null || last === null || first > last ? null : last - first + 1;
1042
+ }
1043
+ //#endregion
1003
1044
  //#region ../js/src/http-error.ts
1004
- /**
1005
- * A final non-2xx response from TrainHeroic. The error deliberately carries only request
1006
- * metadata that is safe to send to telemetry: never the path, query string, request body,
1007
- * response body, credentials, or session token.
1008
- */
1045
+ const MAX_REQUEST_KEYS = 50;
1046
+ const MAX_RESPONSE_DEPTH = 4;
1047
+ const MAX_RESPONSE_KEYS = 20;
1048
+ const MAX_RESPONSE_ITEMS = 10;
1049
+ const MAX_RESPONSE_NODES = 50;
1050
+ const MAX_RESPONSE_STRING = 2e3;
1051
+ const RESPONSE_DIAGNOSTIC_KEYS = /* @__PURE__ */ new Set([
1052
+ "code",
1053
+ "detail",
1054
+ "details",
1055
+ "error",
1056
+ "error_code",
1057
+ "errors",
1058
+ "message",
1059
+ "reason",
1060
+ "status",
1061
+ "status_code",
1062
+ "success"
1063
+ ]);
1064
+ const RESPONSE_CONTAINER_KEYS = /* @__PURE__ */ new Set([
1065
+ "data",
1066
+ "received",
1067
+ "response",
1068
+ "result"
1069
+ ]);
1070
+ const RESPONSE_STATUS_KEYS = /* @__PURE__ */ new Set(["status", "status_code"]);
1071
+ const RESPONSE_BOOLEAN_KEYS = /* @__PURE__ */ new Set(["success"]);
1072
+ const PARAMETER_TYPES = /* @__PURE__ */ new Set([
1073
+ 0,
1074
+ 1,
1075
+ 2,
1076
+ 3,
1077
+ 4,
1078
+ 5,
1079
+ 6,
1080
+ 7,
1081
+ 10,
1082
+ 11,
1083
+ 12,
1084
+ 13,
1085
+ 14,
1086
+ 18
1087
+ ]);
1088
+ /** Keep provider diagnostics useful while removing common credentials and direct identifiers. */
1089
+ function redactText(value) {
1090
+ const redacted = value.slice(0, MAX_RESPONSE_STRING).replace(/\bAuthorization(["']?\s*[:=]\s*)(?:"(?:\\(?:[\s\S]|$)|[^"\\])*(?:"|$)|'(?:\\(?:[\s\S]|$)|[^'\\])*(?:'|$)|[^"'\r\n,;&]+)/giu, (_match, separator) => `Authorization${separator}[Redacted]`).replace(/\bBearer\s+[A-Za-z0-9._~+/-]+=*/giu, "Bearer [Redacted]").replace(/\b(password|passwd|secret|token|access[_-]?token|refresh[_-]?token|session(?:[_-]?(?:id|token))?|api[_-]?key|client[_-]?secret)(["']?\s*[:=]\s*)(?:"(?:\\(?:[\s\S]|$)|[^"\\])*(?:"|$)|'(?:\\(?:[\s\S]|$)|[^'\\])*(?:'|$)|[^"'\r\n,;&}\]]+)/giu, (_match, key, separator) => `${key}${separator}[Redacted]`).replace(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/giu, "[Redacted email]").replace(/\b(?:\d{1,3}\.){3}\d{1,3}\b/gu, "[Redacted IP]").replace(/\b\d{3}-\d{2}-\d{4}\b/gu, "[Redacted]").replace(/\b(?:bearer\s+)?[a-f0-9]{32,}\b/giu, "[Redacted]");
1091
+ return value.length <= MAX_RESPONSE_STRING ? redacted : `${redacted}…[truncated]`;
1092
+ }
1093
+ function safeFieldName(key) {
1094
+ if (key === "__proto__" || key === "constructor" || key === "prototype") return "[Redacted key]";
1095
+ if (!/^[A-Za-z_][A-Za-z0-9_.-]{0,79}$/.test(key)) return "[Redacted key]";
1096
+ return key;
1097
+ }
1098
+ function boundedEntries(object, limit) {
1099
+ const entries = [];
1100
+ for (const key of Object.keys(object)) {
1101
+ const descriptor = Object.getOwnPropertyDescriptor(object, key);
1102
+ entries.push([key, descriptor && "value" in descriptor ? descriptor.value : void 0]);
1103
+ if (entries.length >= limit) break;
1104
+ }
1105
+ return entries;
1106
+ }
1107
+ function safeRequestValue(key, value) {
1108
+ if (key === "is_circuit" && typeof value === "boolean") return value;
1109
+ if (["param_1_type", "param_2_type"].includes(key) && typeof value === "number" && PARAMETER_TYPES.has(value)) return value;
1110
+ }
1111
+ function requestBodySummary(body) {
1112
+ if (Array.isArray(body)) return {
1113
+ type: "array",
1114
+ length: body.length
1115
+ };
1116
+ if (body && typeof body === "object") {
1117
+ const entries = boundedEntries(body, MAX_REQUEST_KEYS).sort(([left], [right]) => left.localeCompare(right));
1118
+ const keys = entries.map(([key]) => safeFieldName(key));
1119
+ const values = {};
1120
+ const arrayLengths = {};
1121
+ for (const [key, value] of entries) {
1122
+ const safeValue = safeRequestValue(key, value);
1123
+ if (safeValue !== void 0) values[safeFieldName(key)] = safeValue;
1124
+ if (Array.isArray(value)) arrayLengths[safeFieldName(key)] = value.length;
1125
+ }
1126
+ const byKey = new Map(entries);
1127
+ const dateStart = byKey.get("date_start");
1128
+ const dateEnd = byKey.get("date_end");
1129
+ const span = typeof dateStart === "string" && typeof dateEnd === "string" ? dateSpanDays(dateStart, dateEnd) : null;
1130
+ return {
1131
+ type: "object",
1132
+ keys,
1133
+ ...Object.keys(values).length > 0 ? { values } : {},
1134
+ ...Object.keys(arrayLengths).length > 0 ? { arrayLengths } : {},
1135
+ ...span === null ? {} : { dateSpanDays: span }
1136
+ };
1137
+ }
1138
+ const primitive = typeof body;
1139
+ return primitive === "boolean" || primitive === "number" || primitive === "string" ? { type: primitive } : { type: "other" };
1140
+ }
1141
+ function sanitizedDiagnosticValue(value, depth, budget, field) {
1142
+ if (budget.nodes <= 0) return "[Truncated]";
1143
+ budget.nodes -= 1;
1144
+ if (value === null) return null;
1145
+ if (typeof value === "boolean") return field && RESPONSE_BOOLEAN_KEYS.has(field) ? value : "[Redacted]";
1146
+ if (typeof value === "number") return field && RESPONSE_STATUS_KEYS.has(field) && Number.isInteger(value) && value >= 100 && value <= 599 ? value : "[Redacted]";
1147
+ if (typeof value === "string") return redactText(value);
1148
+ if (depth >= MAX_RESPONSE_DEPTH) return "[Truncated]";
1149
+ if (Array.isArray(value)) {
1150
+ const result = [];
1151
+ for (const item of value.slice(0, MAX_RESPONSE_ITEMS)) {
1152
+ if (budget.nodes <= 0) {
1153
+ result.push("[Truncated]");
1154
+ break;
1155
+ }
1156
+ result.push(sanitizedDiagnosticValue(item, depth + 1, budget));
1157
+ }
1158
+ return result;
1159
+ }
1160
+ if (value && typeof value === "object") {
1161
+ const result = {};
1162
+ for (const [key, item] of boundedEntries(value, MAX_RESPONSE_KEYS)) {
1163
+ if (budget.nodes <= 0) {
1164
+ result.truncated = "[Truncated]";
1165
+ break;
1166
+ }
1167
+ const safeKey = safeFieldName(key);
1168
+ const safeValue = safeRequestValue(key, item);
1169
+ if (RESPONSE_DIAGNOSTIC_KEYS.has(key)) result[safeKey] = sanitizedDiagnosticValue(item, depth + 1, budget, key);
1170
+ else if (RESPONSE_CONTAINER_KEYS.has(key) && item && typeof item === "object") result[safeKey] = sanitizedDiagnosticValue(item, depth + 1, budget);
1171
+ else if (safeValue !== void 0) {
1172
+ budget.nodes -= 1;
1173
+ result[safeKey] = safeValue;
1174
+ }
1175
+ }
1176
+ return result;
1177
+ }
1178
+ return `[${typeof value}]`;
1179
+ }
1180
+ function responseBodyDiagnostics(body, budget, depth = 0) {
1181
+ const activeBudget = budget ?? { nodes: MAX_RESPONSE_NODES };
1182
+ if (activeBudget.nodes <= 0 || depth >= MAX_RESPONSE_DEPTH) return "[Truncated]";
1183
+ if (!body || typeof body !== "object" || Array.isArray(body)) return sanitizedDiagnosticValue(body, depth, activeBudget);
1184
+ const object = body;
1185
+ const result = {};
1186
+ for (const [key, value] of boundedEntries(object, MAX_RESPONSE_KEYS)) {
1187
+ if (activeBudget.nodes <= 0) {
1188
+ result.truncated = "[Truncated]";
1189
+ break;
1190
+ }
1191
+ if (RESPONSE_DIAGNOSTIC_KEYS.has(key)) {
1192
+ result[safeFieldName(key)] = sanitizedDiagnosticValue(value, depth + 1, activeBudget, key);
1193
+ continue;
1194
+ }
1195
+ if (RESPONSE_CONTAINER_KEYS.has(key) && value && typeof value === "object") {
1196
+ activeBudget.nodes -= 1;
1197
+ const nested = responseBodyDiagnostics(value, activeBudget, depth + 1);
1198
+ if (nested === "[Truncated]" || nested && typeof nested === "object" && Object.keys(nested).length > 0) result[safeFieldName(key)] = nested;
1199
+ }
1200
+ }
1201
+ if (Object.keys(result).length > 0) return result;
1202
+ return { type: "object" };
1203
+ }
1204
+ function parseResponseText(text) {
1205
+ if (text.length === 0) return text;
1206
+ try {
1207
+ return JSON.parse(text);
1208
+ } catch {
1209
+ return text;
1210
+ }
1211
+ }
1212
+ function safeRequestBodySummary(body) {
1213
+ try {
1214
+ return requestBodySummary(body);
1215
+ } catch {
1216
+ return { type: "other" };
1217
+ }
1218
+ }
1219
+ function safeResponseBodyDiagnostics(body) {
1220
+ try {
1221
+ return responseBodyDiagnostics(body);
1222
+ } catch {
1223
+ return "[Unavailable]";
1224
+ }
1225
+ }
1226
+ /** A final non-2xx response plus bounded, telemetry-safe request and response diagnostics. */
1009
1227
  var TrainHeroicHttpError = class extends Error {
1010
1228
  name = "TrainHeroicHttpError";
1011
1229
  method;
1012
1230
  status;
1013
1231
  host;
1014
- constructor(method, url, status) {
1232
+ requestBody;
1233
+ responseBody;
1234
+ constructor(method, url, status, diagnostics = {}) {
1015
1235
  const parsed = new URL(url);
1016
1236
  const normalizedMethod = method.toUpperCase();
1017
1237
  super(`TrainHeroic ${normalizedMethod} request failed with HTTP ${status}`);
1018
1238
  this.method = normalizedMethod;
1019
1239
  this.status = status;
1020
1240
  this.host = parsed.host;
1241
+ this.requestBody = diagnostics.requestBody === void 0 ? void 0 : safeRequestBodySummary(diagnostics.requestBody);
1242
+ this.responseBody = diagnostics.responseBody === void 0 ? void 0 : safeResponseBodyDiagnostics(diagnostics.responseBody);
1021
1243
  }
1022
1244
  };
1023
1245
  /** Call observability hooks without allowing them to change SDK behavior. */
1024
- function notifyHttpError(handler, method, url, status) {
1246
+ function notifyHttpError(handler, method, url, status, diagnostics = {}) {
1025
1247
  if (!handler) return;
1026
1248
  try {
1027
- Promise.resolve(handler(new TrainHeroicHttpError(method, url, status))).catch(() => {});
1249
+ Promise.resolve(handler(new TrainHeroicHttpError(method, url, status, diagnostics))).catch(() => {});
1028
1250
  } catch {}
1029
1251
  }
1030
1252
  //#endregion
1253
+ //#region ../js/src/transport.ts
1254
+ const defaultTrainHeroicTransport = (url, init) => fetch(url, init);
1255
+ //#endregion
1031
1256
  //#region ../js/src/auth.ts
1032
1257
  const DEFAULT_AUTH_URL = "https://apis.trainheroic.com/auth";
1033
1258
  /**
@@ -1051,7 +1276,7 @@ function authUrl() {
1051
1276
  */
1052
1277
  async function loginTrainHeroic(email, password, options = {}) {
1053
1278
  const url = authUrl();
1054
- const res = await fetch(url, {
1279
+ const res = await (options.transport ?? defaultTrainHeroicTransport)(url, {
1055
1280
  method: "POST",
1056
1281
  headers: {
1057
1282
  "content-type": "application/x-www-form-urlencoded",
@@ -1063,6 +1288,7 @@ async function loginTrainHeroic(email, password, options = {}) {
1063
1288
  }).toString()
1064
1289
  });
1065
1290
  if (!res.ok) {
1291
+ await res.body?.cancel().catch(() => {});
1066
1292
  notifyHttpError(options.onHttpError, "POST", url, res.status);
1067
1293
  return null;
1068
1294
  }
@@ -1076,110 +1302,6 @@ async function loginTrainHeroic(email, password, options = {}) {
1076
1302
  };
1077
1303
  }
1078
1304
  //#endregion
1079
- //#region ../js/src/client.ts
1080
- const DEFAULT_COACH_BASE = "https://api.trainheroic.com";
1081
- const DEFAULT_APIS_BASE = "https://apis.trainheroic.com";
1082
- /**
1083
- * Resolve an API host, allowing an env override. The override exists so a test harness can point
1084
- * the client at a local fake backend (and it doubles as a staging knob); production leaves these
1085
- * unset and gets the real hosts. Read through `globalThis.process?.env` — not an `import process`
1086
- * — so the runtime-agnostic `.` entry stays free of `node:*` and runs unchanged on workerd, and
1087
- * read per request (not at module load) so a value the harness sets in the child env always wins.
1088
- */
1089
- function envBase(key, fallback) {
1090
- const v = (globalThis.process?.env)?.[key];
1091
- return v && v.length > 0 ? v : fallback;
1092
- }
1093
- var TrainHeroicAuthError = class extends Error {
1094
- name = "TrainHeroicAuthError";
1095
- };
1096
- /**
1097
- * Authenticated TrainHeroic API client. Holds the credentials (on the hosted Worker, from the
1098
- * grant's encrypted props) and a lazily-acquired session token cached in memory for the life of
1099
- * *this client instance* — nothing longer. On a 401/403 it re-logs in once and retries, since
1100
- * TrainHeroic has no refresh token and sessions expire after ~1-2h.
1101
- *
1102
- * Anything that wants a session to outlive one client owns that itself: pass a previously
1103
- * acquired token as `sessionId` and keep the new one via `options.onSession`. That matters
1104
- * wherever clients are short-lived — the hosted Worker builds one per HTTP request, and each CLI
1105
- * invocation is a fresh process — because otherwise every operation replays the password.
1106
- */
1107
- var TrainHeroicClient = class {
1108
- #email;
1109
- #password;
1110
- #onSession;
1111
- #onHttpError;
1112
- #sessionId;
1113
- #loginInFlight = null;
1114
- constructor(email, password, sessionId = null, options = {}) {
1115
- this.#email = email;
1116
- this.#password = password;
1117
- this.#sessionId = sessionId;
1118
- this.#onSession = options.onSession;
1119
- this.#onHttpError = options.onHttpError;
1120
- }
1121
- get sessionId() {
1122
- return this.#sessionId;
1123
- }
1124
- async #ensureSession() {
1125
- if (this.#sessionId) return this.#sessionId;
1126
- this.#loginInFlight ??= this.#login();
1127
- try {
1128
- return await this.#loginInFlight;
1129
- } finally {
1130
- this.#loginInFlight = null;
1131
- }
1132
- }
1133
- async #login() {
1134
- const session = await loginTrainHeroic(this.#email, this.#password, this.#onHttpError ? { onHttpError: this.#onHttpError } : {});
1135
- if (!session) throw new TrainHeroicAuthError("TrainHeroic login failed");
1136
- this.#sessionId = session.sessionId;
1137
- try {
1138
- this.#onSession?.(this.#sessionId);
1139
- } catch {}
1140
- return this.#sessionId;
1141
- }
1142
- async request(method, path, options = {}) {
1143
- const url = `${options.base === "apis" ? envBase("TH_APIS_BASE", DEFAULT_APIS_BASE) : envBase("TH_COACH_BASE", DEFAULT_COACH_BASE)}/${path.replace(/^\//, "")}`;
1144
- let session = await this.#ensureSession();
1145
- let res = await this.#send(method, url, session, options.body);
1146
- if (res.status === 401 || res.status === 403) {
1147
- if (this.#sessionId === session) this.#sessionId = null;
1148
- session = await this.#ensureSession();
1149
- res = await this.#send(method, url, session, options.body);
1150
- }
1151
- if (!res.ok && !options.expectedStatuses?.includes(res.status)) notifyHttpError(this.#onHttpError, method, url, res.status);
1152
- const text = await res.text();
1153
- let data = text;
1154
- if (text.length > 0) try {
1155
- data = JSON.parse(text);
1156
- } catch {
1157
- data = text;
1158
- }
1159
- return {
1160
- status: res.status,
1161
- ok: res.ok,
1162
- data
1163
- };
1164
- }
1165
- #send(method, url, session, body) {
1166
- const upper = method.toUpperCase();
1167
- const headers = {
1168
- accept: "application/json",
1169
- "session-token": session
1170
- };
1171
- const init = {
1172
- method: upper,
1173
- headers
1174
- };
1175
- if (body !== void 0 && upper !== "GET" && upper !== "DELETE") {
1176
- headers["content-type"] = "application/json";
1177
- init.body = JSON.stringify(body);
1178
- }
1179
- return fetch(url, init);
1180
- }
1181
- };
1182
- //#endregion
1183
1305
  //#region ../js/src/exercise-util.ts
1184
1306
  /**
1185
1307
  * Display labels for TrainHeroic parameter types. The unit is FIXED PER EXERCISE
@@ -1359,6 +1481,127 @@ function assertPositiveInteger(value, label) {
1359
1481
  if (!Number.isInteger(value) || value < 1) throw new RangeError(`${label} must be a positive integer; received ${value}.`);
1360
1482
  }
1361
1483
  //#endregion
1484
+ //#region ../js/src/client.ts
1485
+ const DEFAULT_COACH_BASE = "https://api.trainheroic.com";
1486
+ const DEFAULT_APIS_BASE = "https://apis.trainheroic.com";
1487
+ const MAX_REQUEST_CONCURRENCY = 4;
1488
+ /**
1489
+ * Resolve an API host, allowing an env override. The override exists so a test harness can point
1490
+ * the client at a local fake backend (and it doubles as a staging knob); production leaves these
1491
+ * unset and gets the real hosts. Read through `globalThis.process?.env` — not an `import process`
1492
+ * — so the runtime-agnostic `.` entry stays free of `node:*` and runs unchanged on workerd, and
1493
+ * read per request (not at module load) so a value the harness sets in the child env always wins.
1494
+ */
1495
+ function envBase(key, fallback) {
1496
+ const v = (globalThis.process?.env)?.[key];
1497
+ return v && v.length > 0 ? v : fallback;
1498
+ }
1499
+ var TrainHeroicAuthError = class extends Error {
1500
+ name = "TrainHeroicAuthError";
1501
+ };
1502
+ /**
1503
+ * Authenticated TrainHeroic API client. Holds the credentials (on the hosted Worker, from the
1504
+ * grant's encrypted props) and a lazily-acquired session token cached in memory for the life of
1505
+ * *this client instance* — nothing longer. On a 401/403 it re-logs in once and retries, since
1506
+ * TrainHeroic has no refresh token and sessions expire after ~1-2h.
1507
+ *
1508
+ * Anything that wants a session to outlive one client owns that itself: pass a previously
1509
+ * acquired token as `sessionId` and keep the new one via `options.onSession`. That matters
1510
+ * wherever clients are short-lived — the hosted Worker builds one per HTTP request, and each CLI
1511
+ * invocation is a fresh process — because otherwise every operation replays the password.
1512
+ */
1513
+ var TrainHeroicClient = class {
1514
+ #email;
1515
+ #password;
1516
+ #onSession;
1517
+ #onHttpError;
1518
+ #transport;
1519
+ #requestLimit = createLimiter(MAX_REQUEST_CONCURRENCY);
1520
+ #sessionId;
1521
+ #loginInFlight = null;
1522
+ constructor(email, password, sessionId = null, options = {}) {
1523
+ this.#email = email;
1524
+ this.#password = password;
1525
+ this.#sessionId = sessionId;
1526
+ this.#onSession = options.onSession;
1527
+ this.#onHttpError = options.onHttpError;
1528
+ this.#transport = options.transport ?? defaultTrainHeroicTransport;
1529
+ }
1530
+ get sessionId() {
1531
+ return this.#sessionId;
1532
+ }
1533
+ async #ensureSession() {
1534
+ if (this.#sessionId) return this.#sessionId;
1535
+ this.#loginInFlight ??= this.#login();
1536
+ try {
1537
+ return await this.#loginInFlight;
1538
+ } finally {
1539
+ this.#loginInFlight = null;
1540
+ }
1541
+ }
1542
+ async #login() {
1543
+ const session = await loginTrainHeroic(this.#email, this.#password, {
1544
+ ...this.#onHttpError ? { onHttpError: this.#onHttpError } : {},
1545
+ transport: this.#transport
1546
+ });
1547
+ if (!session) throw new TrainHeroicAuthError("TrainHeroic login failed");
1548
+ this.#sessionId = session.sessionId;
1549
+ try {
1550
+ this.#onSession?.(this.#sessionId);
1551
+ } catch {}
1552
+ return this.#sessionId;
1553
+ }
1554
+ async request(method, path, options = {}) {
1555
+ await this.#ensureSession();
1556
+ return this.#requestLimit.run(() => this.#request(method, path, options));
1557
+ }
1558
+ async #request(method, path, options) {
1559
+ const url = `${options.base === "apis" ? envBase("TH_APIS_BASE", DEFAULT_APIS_BASE) : envBase("TH_COACH_BASE", DEFAULT_COACH_BASE)}/${path.replace(/^\//, "")}`;
1560
+ let session = await this.#ensureSession();
1561
+ let res = await this.#send(method, url, session, options.body);
1562
+ if (res.status === 401 || res.status === 403) {
1563
+ await res.body?.cancel().catch(() => {});
1564
+ if (this.#sessionId === session) this.#sessionId = null;
1565
+ session = await this.#ensureSession();
1566
+ res = await this.#send(method, url, session, options.body);
1567
+ }
1568
+ const shouldReport = !res.ok && !options.expectedStatuses?.includes(res.status);
1569
+ let text;
1570
+ try {
1571
+ text = await res.text();
1572
+ } catch (error) {
1573
+ if (shouldReport) notifyHttpError(this.#onHttpError, method, url, res.status, { requestBody: options.body });
1574
+ throw error;
1575
+ }
1576
+ const data = parseResponseText(text);
1577
+ if (shouldReport) notifyHttpError(this.#onHttpError, method, url, res.status, {
1578
+ requestBody: options.body,
1579
+ responseBody: data
1580
+ });
1581
+ return {
1582
+ status: res.status,
1583
+ ok: res.ok,
1584
+ data
1585
+ };
1586
+ }
1587
+ #send(method, url, session, body) {
1588
+ const upper = method.toUpperCase();
1589
+ const headers = {
1590
+ accept: "application/json",
1591
+ "session-token": session
1592
+ };
1593
+ const init = {
1594
+ method: upper,
1595
+ headers
1596
+ };
1597
+ if (body !== void 0 && upper !== "GET" && upper !== "DELETE") {
1598
+ headers["content-type"] = "application/json";
1599
+ init.body = JSON.stringify(body);
1600
+ }
1601
+ return this.#transport(url, init);
1602
+ }
1603
+ };
1604
+ //#endregion
1362
1605
  //#region ../js/src/athlete.ts
1363
1606
  async function getJson(client, path, label) {
1364
1607
  const res = await client.request("GET", path);
@@ -2960,7 +3203,7 @@ function registerAthleteTrainingTools(server, ctx) {
2960
3203
  }
2961
3204
  //#endregion
2962
3205
  //#region package.json
2963
- var version = "3.5.2";
3206
+ var version = "3.7.1";
2964
3207
  //#endregion
2965
3208
  //#region src/server.ts
2966
3209
  function main() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trainheroic-unofficial/athlete-mcp",
3
- "version": "3.5.2",
3
+ "version": "3.7.1",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -20,14 +20,14 @@
20
20
  ],
21
21
  "dependencies": {
22
22
  "@modelcontextprotocol/server": "2.0.0",
23
- "zod": "^4.4.3",
24
- "@trainheroic-unofficial/core": "3.5.2",
25
- "@trainheroic-unofficial/js": "3.5.2"
23
+ "zod": "^4.6.3",
24
+ "@trainheroic-unofficial/core": "3.7.1",
25
+ "@trainheroic-unofficial/js": "3.7.1"
26
26
  },
27
27
  "devDependencies": {
28
- "@types/node": "^26.3.0",
29
- "tsdown": "^0.22.14",
30
- "tsx": "^4.23.12",
28
+ "@types/node": "^26.5.1",
29
+ "tsdown": "^0.23.0",
30
+ "tsx": "^4.23.13",
31
31
  "typescript": "^7.0.2",
32
32
  "vitest": "^4.1.11"
33
33
  },