@trainheroic-unofficial/core 0.2.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/README.md +12 -7
- package/dist/index.d.mts +20 -7
- package/dist/index.mjs +341 -31
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -16,7 +16,9 @@ functions:
|
|
|
16
16
|
```ts
|
|
17
17
|
import {
|
|
18
18
|
registerReadTools,
|
|
19
|
-
|
|
19
|
+
registerAthleteTools,
|
|
20
|
+
registerTeamTools,
|
|
21
|
+
registerAnalyticsTools,
|
|
20
22
|
registerExerciseTools,
|
|
21
23
|
registerWorkoutTools,
|
|
22
24
|
registerMessagingTools,
|
|
@@ -36,15 +38,18 @@ change.
|
|
|
36
38
|
## What the tools cover
|
|
37
39
|
|
|
38
40
|
The tools group into coach reads (profile, athletes, teams, programs, notifications,
|
|
39
|
-
analytics),
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
analytics catalog), athlete management (invite, archive, restore), team management (create,
|
|
42
|
+
rename, delete, join codes), analytics report pulls (`analytics_query`: readiness, 1RM and
|
|
43
|
+
working-max history, training summary, compliance, lift progress), exercise library
|
|
44
|
+
operations (resolve, search, get, sync, create,
|
|
45
|
+
forget, stats), the workout/session lifecycle (build a draft, read it back, publish,
|
|
46
|
+
unpublish, copy, save as template, remove), and messaging (list, read, draft, send, delete).
|
|
47
|
+
There is no raw-request escape hatch; every endpoint reaches the model through a typed tool.
|
|
43
48
|
|
|
44
49
|
Tools return their result in-band. A failure comes back as an error result the model can
|
|
45
50
|
read and correct, not a thrown exception. Reads are marked read-only. Athlete-facing or
|
|
46
|
-
destructive actions (publish, remove, send, delete,
|
|
47
|
-
confirmation gate before they run.
|
|
51
|
+
destructive actions (publish, unpublish, remove, send, delete, archive, team/code delete)
|
|
52
|
+
pass through a confirmation gate before they run.
|
|
48
53
|
|
|
49
54
|
The D1-backed warehouse sync tools are not here; they live in the `cloudflare` package
|
|
50
55
|
because they depend on its storage.
|
package/dist/index.d.mts
CHANGED
|
@@ -63,13 +63,26 @@ declare function confirmGate(server: McpServer, requestId: string | number | und
|
|
|
63
63
|
/** Read-only coach/athlete queries. Exercise lookups live in the exercise store tools. */
|
|
64
64
|
declare function registerReadTools(server: McpServer, ctx: ToolContext): void;
|
|
65
65
|
//#endregion
|
|
66
|
-
//#region src/tools/
|
|
66
|
+
//#region src/tools/athletes.d.ts
|
|
67
67
|
/**
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
* the
|
|
68
|
+
* Athlete management. TrainHeroic has no "create athlete" primitive: a coach invites a
|
|
69
|
+
* person by email to a team, and the athlete record only exists once they accept and set
|
|
70
|
+
* their own name. So the create path is the two-step invite from the API reference:
|
|
71
|
+
* validate the address (POST /v5/emails/validate), then inviteToTeam
|
|
72
|
+
* (POST /v5/athletes/inviteToTeam). Both act on the live account, so the invite is gated.
|
|
71
73
|
*/
|
|
72
|
-
declare function
|
|
74
|
+
declare function registerAthleteTools(server: McpServer, ctx: ToolContext): void;
|
|
75
|
+
//#endregion
|
|
76
|
+
//#region src/tools/teams.d.ts
|
|
77
|
+
/**
|
|
78
|
+
* Team write tools. The team reads (list_teams, get_team, list_team_codes) live in
|
|
79
|
+
* reads.ts; this module covers create/rename/delete plus the join-code lifecycle.
|
|
80
|
+
*/
|
|
81
|
+
declare function registerTeamTools(server: McpServer, ctx: ToolContext): void;
|
|
82
|
+
//#endregion
|
|
83
|
+
//#region src/tools/analytics.d.ts
|
|
84
|
+
/** Analytics report pulls. Read-only despite the POST verb, so ungated. */
|
|
85
|
+
declare function registerAnalyticsTools(server: McpServer, ctx: ToolContext): void;
|
|
73
86
|
//#endregion
|
|
74
87
|
//#region src/tools/exercises.d.ts
|
|
75
88
|
/**
|
|
@@ -80,11 +93,11 @@ declare function registerRawTools(server: McpServer, ctx: ToolContext): void;
|
|
|
80
93
|
declare function registerExerciseTools(server: McpServer, ctx: ToolContext): void;
|
|
81
94
|
//#endregion
|
|
82
95
|
//#region src/tools/workout.d.ts
|
|
83
|
-
/** Workout building, read-back, publishing, and
|
|
96
|
+
/** Workout building, read-back, publishing, and the session calendar lifecycle. */
|
|
84
97
|
declare function registerWorkoutTools(server: McpServer, ctx: ToolContext): void;
|
|
85
98
|
//#endregion
|
|
86
99
|
//#region src/tools/messaging.d.ts
|
|
87
100
|
/** Live messaging: list/read conversations, draft a message, and the gated send/delete. */
|
|
88
101
|
declare function registerMessagingTools(server: McpServer, ctx: ToolContext): void;
|
|
89
102
|
//#endregion
|
|
90
|
-
export { BudgetHint, DEFAULT_RESULT_BUDGET, DESTRUCTIVE, NOT_CONFIRMED, READ, SYNC, ToolContext, apiCall, attempt, boundedSerialize, confirmGate, errorResult, idParam, jsonResult, registerExerciseTools, registerMessagingTools,
|
|
103
|
+
export { BudgetHint, DEFAULT_RESULT_BUDGET, DESTRUCTIVE, NOT_CONFIRMED, READ, SYNC, ToolContext, apiCall, attempt, boundedSerialize, confirmGate, errorResult, idParam, jsonResult, registerAnalyticsTools, registerAthleteTools, registerExerciseTools, registerMessagingTools, registerReadTools, registerTeamTools, registerWorkoutTools, resultBudget, toId };
|
package/dist/index.mjs
CHANGED
|
@@ -221,12 +221,12 @@ const SIMPLE_GETS = [
|
|
|
221
221
|
{
|
|
222
222
|
name: "analytics_categories",
|
|
223
223
|
title: "Analytics categories",
|
|
224
|
-
description: "Lists available analytics types. Pull
|
|
224
|
+
description: "Lists available analytics types. Pull a report with analytics_query (pick the matching metric).",
|
|
225
225
|
path: "/v5/analytics"
|
|
226
226
|
}
|
|
227
227
|
];
|
|
228
|
-
/**
|
|
229
|
-
function
|
|
228
|
+
/** Roster-level reads: the fixed GETs plus the filterable athlete and team lists. */
|
|
229
|
+
function registerRosterReads(server, ctx) {
|
|
230
230
|
for (const t of SIMPLE_GETS) server.registerTool(t.name, {
|
|
231
231
|
title: t.title,
|
|
232
232
|
description: t.description,
|
|
@@ -279,6 +279,9 @@ function registerReadTools(server, ctx) {
|
|
|
279
279
|
const query = qs.toString();
|
|
280
280
|
return apiCall(ctx, "GET", `/1.0/coach/teams${query ? `?${query}` : ""}`);
|
|
281
281
|
});
|
|
282
|
+
}
|
|
283
|
+
/** Reads scoped to a single entity (team, program, athlete) plus the activity feed. */
|
|
284
|
+
function registerEntityReads(server, ctx) {
|
|
282
285
|
server.registerTool("get_team", {
|
|
283
286
|
title: "Get team",
|
|
284
287
|
description: "Full team object by team id.",
|
|
@@ -297,39 +300,283 @@ function registerReadTools(server, ctx) {
|
|
|
297
300
|
inputSchema: { programId: idParam },
|
|
298
301
|
annotations: READ
|
|
299
302
|
}, ({ programId }) => apiCall(ctx, "GET", `/3.0/coach/program/${enc(programId)}`, void 0, "This is a large, deep object. If it is truncated, fetch a narrower view (a specific session) instead."));
|
|
303
|
+
server.registerTool("coach_activity_feed", {
|
|
304
|
+
title: "Coach activity feed",
|
|
305
|
+
description: "Recent athlete activity across the roster (GET /v5/coaches/activityFeed): completed workouts, PRs, and posts. Paginate with page/pageSize.",
|
|
306
|
+
inputSchema: {
|
|
307
|
+
page: z.number().int().positive().optional(),
|
|
308
|
+
pageSize: z.number().int().positive().optional()
|
|
309
|
+
},
|
|
310
|
+
annotations: READ
|
|
311
|
+
}, ({ page, pageSize }) => {
|
|
312
|
+
const qs = new URLSearchParams();
|
|
313
|
+
if (page !== void 0) qs.set("page", String(page));
|
|
314
|
+
if (pageSize !== void 0) qs.set("pageSize", String(pageSize));
|
|
315
|
+
const query = qs.toString();
|
|
316
|
+
return apiCall(ctx, "GET", `/v5/coaches/activityFeed${query ? `?${query}` : ""}`);
|
|
317
|
+
});
|
|
318
|
+
server.registerTool("athlete_workouts", {
|
|
319
|
+
title: "Athlete workout history",
|
|
320
|
+
description: "One athlete's workouts with their survey/readiness data (GET /v5/coaches/athletes/{athleteId}/workouts). Bound it with startDate/endDate (YYYY-MM-DD) to keep the response small.",
|
|
321
|
+
inputSchema: {
|
|
322
|
+
athleteId: idParam,
|
|
323
|
+
startDate: z.string().optional(),
|
|
324
|
+
endDate: z.string().optional()
|
|
325
|
+
},
|
|
326
|
+
annotations: READ
|
|
327
|
+
}, ({ athleteId, startDate, endDate }) => {
|
|
328
|
+
const qs = new URLSearchParams();
|
|
329
|
+
if (startDate !== void 0) qs.set("startDate", startDate);
|
|
330
|
+
if (endDate !== void 0) qs.set("endDate", endDate);
|
|
331
|
+
const query = qs.toString();
|
|
332
|
+
return apiCall(ctx, "GET", `/v5/coaches/athletes/${enc(athleteId)}/workouts${query ? `?${query}` : ""}`, void 0, "Narrow the date range to shrink this if it is truncated.");
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
/** Read-only coach/athlete queries. Exercise lookups live in the exercise store tools. */
|
|
336
|
+
function registerReadTools(server, ctx) {
|
|
337
|
+
registerRosterReads(server, ctx);
|
|
338
|
+
registerEntityReads(server, ctx);
|
|
300
339
|
}
|
|
301
340
|
//#endregion
|
|
302
|
-
//#region src/tools/
|
|
341
|
+
//#region src/tools/athletes.ts
|
|
342
|
+
const DEFAULT_INVITE_MESSAGE = "Follow these steps and you'll be set up and ready to go!";
|
|
343
|
+
/** Normalize one-or-many emails into a deduped, trimmed list. */
|
|
344
|
+
function asEmailList(emails) {
|
|
345
|
+
return [...new Set((Array.isArray(emails) ? emails : [emails]).map((e) => e.trim()).filter((e) => e.length > 0))];
|
|
346
|
+
}
|
|
303
347
|
/**
|
|
304
|
-
*
|
|
305
|
-
*
|
|
306
|
-
* the
|
|
348
|
+
* Athlete management. TrainHeroic has no "create athlete" primitive: a coach invites a
|
|
349
|
+
* person by email to a team, and the athlete record only exists once they accept and set
|
|
350
|
+
* their own name. So the create path is the two-step invite from the API reference:
|
|
351
|
+
* validate the address (POST /v5/emails/validate), then inviteToTeam
|
|
352
|
+
* (POST /v5/athletes/inviteToTeam). Both act on the live account, so the invite is gated.
|
|
307
353
|
*/
|
|
308
|
-
function
|
|
309
|
-
server.registerTool("
|
|
310
|
-
title: "
|
|
311
|
-
description: "
|
|
354
|
+
function registerAthleteTools(server, ctx) {
|
|
355
|
+
server.registerTool("athlete_invite", {
|
|
356
|
+
title: "Invite an athlete",
|
|
357
|
+
description: "Add athletes by emailing them a TrainHeroic team invitation — this is how you 'create' an athlete; there is no other create path, and no name is collected here (the athlete sets their own on accept). Targets a team by id, so list_teams first if you need one. Validates each address, then sends the invite. ATHLETE-FACING and immediate; requires confirmation (elicitation, or confirm:true).",
|
|
358
|
+
inputSchema: {
|
|
359
|
+
teamId: idParam,
|
|
360
|
+
emails: z.union([z.string(), z.array(z.string()).min(1)]),
|
|
361
|
+
message: z.string().optional(),
|
|
362
|
+
confirm: z.boolean().optional()
|
|
363
|
+
},
|
|
364
|
+
annotations: DESTRUCTIVE
|
|
365
|
+
}, ({ teamId, emails, message, confirm }, extra) => attempt(async () => {
|
|
366
|
+
const list = asEmailList(emails);
|
|
367
|
+
if (list.length === 0) return errorResult("Provide at least one email address to invite.");
|
|
368
|
+
const id = toId(teamId);
|
|
369
|
+
if (!await confirmGate(server, extra.requestId, `Invite ${list.join(", ")} to team ${id}? This emails them a real TrainHeroic invitation.`, confirm)) return errorResult(NOT_CONFIRMED);
|
|
370
|
+
const validation = await ctx.client.request("POST", "/v5/emails/validate", { body: { emails: list.join(",") } });
|
|
371
|
+
if (!validation.ok) {
|
|
372
|
+
const detail = typeof validation.data === "string" ? validation.data : JSON.stringify(validation.data);
|
|
373
|
+
return errorResult(`Email validation failed (HTTP ${validation.status}): ${detail}`);
|
|
374
|
+
}
|
|
375
|
+
const valid = Array.isArray(validation.data) ? validation.data : list;
|
|
376
|
+
if (valid.length === 0) return errorResult(`No valid addresses among: ${list.join(", ")}. They may be malformed or already on the team.`);
|
|
377
|
+
const invite = await ctx.client.request("POST", "/v5/athletes/inviteToTeam", { body: {
|
|
378
|
+
teamType: 0,
|
|
379
|
+
teamId: id,
|
|
380
|
+
orgId: null,
|
|
381
|
+
emails: valid,
|
|
382
|
+
message: message ?? DEFAULT_INVITE_MESSAGE
|
|
383
|
+
} });
|
|
384
|
+
if (!invite.ok) {
|
|
385
|
+
const detail = typeof invite.data === "string" ? invite.data : JSON.stringify(invite.data);
|
|
386
|
+
return errorResult(`Invite failed (HTTP ${invite.status}): ${detail}`);
|
|
387
|
+
}
|
|
388
|
+
return jsonResult({
|
|
389
|
+
invited: true,
|
|
390
|
+
teamId: id,
|
|
391
|
+
result: invite.data
|
|
392
|
+
});
|
|
393
|
+
}));
|
|
394
|
+
server.registerTool("athlete_archive", {
|
|
395
|
+
title: "Archive athletes",
|
|
396
|
+
description: "Remove one or more athletes from the active roster (PUT /v5/athletes/archive). Their data is preserved and they can be restored. Acts on the live account; requires confirmation (elicitation, or confirm:true).",
|
|
397
|
+
inputSchema: {
|
|
398
|
+
athleteIds: z.array(idParam).min(1),
|
|
399
|
+
confirm: z.boolean().optional()
|
|
400
|
+
},
|
|
401
|
+
annotations: DESTRUCTIVE
|
|
402
|
+
}, ({ athleteIds, confirm }, extra) => attempt(async () => {
|
|
403
|
+
const ids = athleteIds.map(toId);
|
|
404
|
+
if (!await confirmGate(server, extra.requestId, `Archive athlete(s) ${ids.join(", ")}? They leave the active roster (data is kept and restorable).`, confirm)) return errorResult(NOT_CONFIRMED);
|
|
405
|
+
return apiCall(ctx, "PUT", "/v5/athletes/archive", { body: { athleteIds: ids } });
|
|
406
|
+
}));
|
|
407
|
+
server.registerTool("athlete_restore", {
|
|
408
|
+
title: "Restore athletes",
|
|
409
|
+
description: "Restore previously archived athletes to the active roster (PUT /v5/athletes/restore).",
|
|
410
|
+
inputSchema: { athleteIds: z.array(idParam).min(1) },
|
|
411
|
+
annotations: {
|
|
412
|
+
readOnlyHint: false,
|
|
413
|
+
idempotentHint: true,
|
|
414
|
+
destructiveHint: false,
|
|
415
|
+
openWorldHint: true
|
|
416
|
+
}
|
|
417
|
+
}, ({ athleteIds }) => apiCall(ctx, "PUT", "/v5/athletes/restore", { body: { athleteIds: athleteIds.map(toId) } }));
|
|
418
|
+
}
|
|
419
|
+
//#endregion
|
|
420
|
+
//#region src/tools/teams.ts
|
|
421
|
+
const ADDITIVE = {
|
|
422
|
+
readOnlyHint: false,
|
|
423
|
+
destructiveHint: false,
|
|
424
|
+
openWorldHint: true
|
|
425
|
+
};
|
|
426
|
+
/**
|
|
427
|
+
* Team write tools. The team reads (list_teams, get_team, list_team_codes) live in
|
|
428
|
+
* reads.ts; this module covers create/rename/delete plus the join-code lifecycle.
|
|
429
|
+
*/
|
|
430
|
+
function registerTeamTools(server, ctx) {
|
|
431
|
+
server.registerTool("team_create", {
|
|
432
|
+
title: "Create a team",
|
|
433
|
+
description: "Create a team (POST /1.0/coach/team/createWithTitleAndCode). Also creates the team's calendar/program. Returns the new team including its calendar id. Use the team id with athlete_invite.",
|
|
434
|
+
inputSchema: { title: z.string().min(1) },
|
|
435
|
+
annotations: ADDITIVE
|
|
436
|
+
}, ({ title }) => apiCall(ctx, "POST", "/1.0/coach/team/createWithTitleAndCode", { body: { title } }));
|
|
437
|
+
server.registerTool("team_update", {
|
|
438
|
+
title: "Rename a team",
|
|
439
|
+
description: "Update a team's settings, e.g. its title (PUT /v5/teams/{teamId}).",
|
|
440
|
+
inputSchema: {
|
|
441
|
+
teamId: idParam,
|
|
442
|
+
title: z.string().min(1)
|
|
443
|
+
},
|
|
444
|
+
annotations: ADDITIVE
|
|
445
|
+
}, ({ teamId, title }) => apiCall(ctx, "PUT", `/v5/teams/${toId(teamId)}`, { body: { title } }));
|
|
446
|
+
server.registerTool("team_delete", {
|
|
447
|
+
title: "Delete a team",
|
|
448
|
+
description: "Delete a team (DELETE /v5/teams/{teamId}). Removes the team and its calendar from the live account; hard to undo. Requires confirmation (elicitation, or confirm:true).",
|
|
449
|
+
inputSchema: {
|
|
450
|
+
teamId: idParam,
|
|
451
|
+
confirm: z.boolean().optional()
|
|
452
|
+
},
|
|
453
|
+
annotations: DESTRUCTIVE
|
|
454
|
+
}, ({ teamId, confirm }, extra) => attempt(async () => {
|
|
455
|
+
const id = toId(teamId);
|
|
456
|
+
if (!await confirmGate(server, extra.requestId, `Delete team ${id}? This removes the team and its calendar from the live account.`, confirm)) return errorResult(NOT_CONFIRMED);
|
|
457
|
+
return apiCall(ctx, "DELETE", `/v5/teams/${id}`);
|
|
458
|
+
}));
|
|
459
|
+
server.registerTool("team_code_create", {
|
|
460
|
+
title: "Create a team join code",
|
|
461
|
+
description: "Create an access code athletes use to self-join a team (POST /v5/teams/{teamId}/teamCodes). `type` defaults to 2, the standard join code.",
|
|
312
462
|
inputSchema: {
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
463
|
+
teamId: idParam,
|
|
464
|
+
type: z.number().int().optional()
|
|
465
|
+
},
|
|
466
|
+
annotations: ADDITIVE
|
|
467
|
+
}, ({ teamId, type }) => apiCall(ctx, "POST", `/v5/teams/${toId(teamId)}/teamCodes`, { body: { type: type ?? 2 } }));
|
|
468
|
+
server.registerTool("team_code_delete", {
|
|
469
|
+
title: "Delete a team join code",
|
|
470
|
+
description: "Delete a team access code by its id (DELETE /v5/teamCodes/{codeId}). Athletes can no longer use it to join. Requires confirmation (elicitation, or confirm:true).",
|
|
471
|
+
inputSchema: {
|
|
472
|
+
codeId: idParam,
|
|
322
473
|
confirm: z.boolean().optional()
|
|
323
474
|
},
|
|
324
475
|
annotations: DESTRUCTIVE
|
|
325
|
-
},
|
|
326
|
-
|
|
327
|
-
|
|
476
|
+
}, ({ codeId, confirm }, extra) => attempt(async () => {
|
|
477
|
+
const id = toId(codeId);
|
|
478
|
+
if (!await confirmGate(server, extra.requestId, `Delete team join code ${id}? Athletes can no longer use it to join.`, confirm)) return errorResult(NOT_CONFIRMED);
|
|
479
|
+
return apiCall(ctx, "DELETE", `/v5/teamCodes/${id}`);
|
|
480
|
+
}));
|
|
481
|
+
}
|
|
482
|
+
//#endregion
|
|
483
|
+
//#region src/tools/analytics.ts
|
|
484
|
+
const METRIC_KEYS = [
|
|
485
|
+
"readiness-team",
|
|
486
|
+
"readiness-athlete",
|
|
487
|
+
"lift-1rm-history",
|
|
488
|
+
"training-summary-athlete",
|
|
489
|
+
"training-summary-team",
|
|
490
|
+
"compliance-team",
|
|
491
|
+
"lift-progress-team",
|
|
492
|
+
"working-max-history"
|
|
493
|
+
];
|
|
494
|
+
const METRICS = {
|
|
495
|
+
"readiness-team": {
|
|
496
|
+
path: "/v5/analytics/readiness/teams",
|
|
497
|
+
scope: "team",
|
|
498
|
+
fields: ["date"]
|
|
499
|
+
},
|
|
500
|
+
"readiness-athlete": {
|
|
501
|
+
path: "/v5/analytics/readiness/users",
|
|
502
|
+
scope: "user",
|
|
503
|
+
fields: ["date"]
|
|
504
|
+
},
|
|
505
|
+
"lift-1rm-history": {
|
|
506
|
+
path: "/v5/analytics/lift-one-rep-max-history/users",
|
|
507
|
+
scope: "user",
|
|
508
|
+
fields: [
|
|
509
|
+
"date_start",
|
|
510
|
+
"date_end",
|
|
511
|
+
"exercise_id",
|
|
512
|
+
"use_metric"
|
|
513
|
+
]
|
|
514
|
+
},
|
|
515
|
+
"training-summary-athlete": {
|
|
516
|
+
path: "/v5/analytics/training-summary/users",
|
|
517
|
+
scope: "user",
|
|
518
|
+
fields: ["date_start", "date_end"]
|
|
519
|
+
},
|
|
520
|
+
"training-summary-team": {
|
|
521
|
+
path: "/v5/analytics/training-summary/teams",
|
|
522
|
+
scope: "team",
|
|
523
|
+
fields: ["date_start", "date_end"]
|
|
524
|
+
},
|
|
525
|
+
"compliance-team": {
|
|
526
|
+
path: "/v5/analytics/compliance",
|
|
527
|
+
scope: "team",
|
|
528
|
+
fields: ["date_start", "date_end"]
|
|
529
|
+
},
|
|
530
|
+
"lift-progress-team": {
|
|
531
|
+
path: "/v5/analytics/lift-progress/teams",
|
|
532
|
+
scope: "team",
|
|
533
|
+
fields: [
|
|
534
|
+
"exercise_id",
|
|
535
|
+
"date_start",
|
|
536
|
+
"date_end"
|
|
537
|
+
]
|
|
538
|
+
},
|
|
539
|
+
"working-max-history": {
|
|
540
|
+
path: "/v5/analytics/working-max-history/users",
|
|
541
|
+
scope: "user",
|
|
542
|
+
fields: ["date_start", "date_end"]
|
|
543
|
+
}
|
|
544
|
+
};
|
|
545
|
+
/** Analytics report pulls. Read-only despite the POST verb, so ungated. */
|
|
546
|
+
function registerAnalyticsTools(server, ctx) {
|
|
547
|
+
server.registerTool("analytics_query", {
|
|
548
|
+
title: "Pull an analytics report",
|
|
549
|
+
description: "Read a TrainHeroic analytics report. `metric` picks the report (run analytics_categories for the catalog). Team metrics need teamId; athlete metrics need userIds. Dates are YYYY-MM-DD: readiness takes a single `date`, history/summary metrics take dateStart/dateEnd. lift-1rm-history and lift-progress-team also take exerciseId.",
|
|
550
|
+
inputSchema: {
|
|
551
|
+
metric: z.enum(METRIC_KEYS),
|
|
552
|
+
teamId: idParam.optional(),
|
|
553
|
+
userIds: z.array(idParam).optional(),
|
|
554
|
+
exerciseId: idParam.optional(),
|
|
555
|
+
date: z.string().optional(),
|
|
556
|
+
dateStart: z.string().optional(),
|
|
557
|
+
dateEnd: z.string().optional(),
|
|
558
|
+
useMetric: z.boolean().optional()
|
|
559
|
+
},
|
|
560
|
+
annotations: READ
|
|
561
|
+
}, async ({ metric, teamId, userIds, exerciseId, date, dateStart, dateEnd, useMetric }) => {
|
|
562
|
+
const spec = METRICS[metric];
|
|
563
|
+
const body = {};
|
|
564
|
+
if (spec.scope === "team") {
|
|
565
|
+
if (teamId === void 0) return errorResult(`${metric} needs teamId.`);
|
|
566
|
+
body.teamId = toId(teamId);
|
|
567
|
+
} else {
|
|
568
|
+
if (userIds === void 0 || userIds.length === 0) return errorResult(`${metric} needs userIds (one or more athlete ids).`);
|
|
569
|
+
body.user_ids = userIds.map((u) => String(toId(u)));
|
|
328
570
|
}
|
|
329
|
-
const
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
571
|
+
const provided = {
|
|
572
|
+
date,
|
|
573
|
+
date_start: dateStart,
|
|
574
|
+
date_end: dateEnd,
|
|
575
|
+
exercise_id: exerciseId === void 0 ? void 0 : String(toId(exerciseId)),
|
|
576
|
+
use_metric: useMetric
|
|
577
|
+
};
|
|
578
|
+
for (const field of spec.fields) if (provided[field] !== void 0) body[field] = provided[field];
|
|
579
|
+
return apiCall(ctx, "POST", spec.path, { body }, "Narrow with a tighter date range or fewer athletes.");
|
|
333
580
|
});
|
|
334
581
|
}
|
|
335
582
|
//#endregion
|
|
@@ -343,7 +590,7 @@ function registerExerciseTools(server, ctx) {
|
|
|
343
590
|
const index = ctx.index;
|
|
344
591
|
server.registerTool("exercise_resolve", {
|
|
345
592
|
title: "Resolve exercise name",
|
|
346
|
-
description: "Map a name to an exercise id via the local mirror. Returns the match plus ranked candidates; when ambiguous, match is null and you should pick from candidates.
|
|
593
|
+
description: "Map a name to an exercise id via the local mirror. Returns the match plus ranked candidates; when ambiguous, match is null and you should pick from candidates. Each result's `units` array lists the fixed measurement units by entry slot; they are fixed per exercise — check them before prescribing.",
|
|
347
594
|
inputSchema: { name: z.string().min(1) },
|
|
348
595
|
annotations: READ
|
|
349
596
|
}, ({ name }) => attempt(async () => jsonResult(await index.resolve(name))));
|
|
@@ -407,8 +654,8 @@ function registerExerciseTools(server, ctx) {
|
|
|
407
654
|
}
|
|
408
655
|
//#endregion
|
|
409
656
|
//#region src/tools/workout.ts
|
|
410
|
-
/**
|
|
411
|
-
function
|
|
657
|
+
/** Build a draft, read it back, and publish it. */
|
|
658
|
+
function registerBuild(server, ctx) {
|
|
412
659
|
server.registerTool("workout_build", {
|
|
413
660
|
title: "Build a workout session (draft)",
|
|
414
661
|
description: "Build an UNPUBLISHED session from a spec (program -> session -> blocks -> exercises). Two exercises in one block become a superset. Add a block 'leaderboard' for a Red-Zone score, or a top-level 'instruction' for the session note (Coach Instructions). Returns the draft ids, a read-back, and unit advisories. Review, then workout_publish.",
|
|
@@ -481,6 +728,9 @@ function registerWorkoutTools(server, ctx) {
|
|
|
481
728
|
readback: await readSession(ctx.client, programId, parseWorkoutDate(date), pwId)
|
|
482
729
|
});
|
|
483
730
|
}));
|
|
731
|
+
}
|
|
732
|
+
/** Calendar lifecycle for an existing session: remove, unpublish, copy, save to library. */
|
|
733
|
+
function registerLifecycle(server, ctx) {
|
|
484
734
|
server.registerTool("session_remove", {
|
|
485
735
|
title: "Remove a session",
|
|
486
736
|
description: "Delete a session from the live calendar (also the way to replace a date: remove then build). Hard to undo. Requires confirmation (elicitation, or confirm:true).",
|
|
@@ -499,6 +749,66 @@ function registerWorkoutTools(server, ctx) {
|
|
|
499
749
|
await removeSession(ctx.client, programId, pwId);
|
|
500
750
|
return jsonResult({ removed: pwId });
|
|
501
751
|
}));
|
|
752
|
+
server.registerTool("session_unpublish", {
|
|
753
|
+
title: "Unpublish a session",
|
|
754
|
+
description: "Unpublish a previously published session (POST .../programWorkout/unPublish/{pwId}). It is no longer athlete-facing. Requires confirmation (elicitation, or confirm:true).",
|
|
755
|
+
inputSchema: {
|
|
756
|
+
pwId: z.number(),
|
|
757
|
+
confirm: z.boolean().optional()
|
|
758
|
+
},
|
|
759
|
+
annotations: {
|
|
760
|
+
readOnlyHint: false,
|
|
761
|
+
destructiveHint: true,
|
|
762
|
+
openWorldHint: true
|
|
763
|
+
}
|
|
764
|
+
}, ({ pwId, confirm }, extra) => attempt(async () => {
|
|
765
|
+
if (!await confirmGate(server, extra.requestId, `Unpublish session ${pwId}? Athletes will no longer see it.`, confirm)) return errorResult(NOT_CONFIRMED);
|
|
766
|
+
return apiCall(ctx, "POST", `/2.0/coach/calendar/programWorkout/unPublish/${pwId}`);
|
|
767
|
+
}));
|
|
768
|
+
server.registerTool("session_copy", {
|
|
769
|
+
title: "Copy a session to a date",
|
|
770
|
+
description: "Copy/repeat a session to a target date on a program (POST .../copyProgramWorkout). toDate is YYYY-M-D. Creates a new session; review and publish it separately.",
|
|
771
|
+
inputSchema: {
|
|
772
|
+
toProgramId: z.number(),
|
|
773
|
+
pwId: z.number(),
|
|
774
|
+
toDate: z.string()
|
|
775
|
+
},
|
|
776
|
+
annotations: {
|
|
777
|
+
readOnlyHint: false,
|
|
778
|
+
destructiveHint: false,
|
|
779
|
+
openWorldHint: true
|
|
780
|
+
}
|
|
781
|
+
}, ({ toProgramId, pwId, toDate }) => attempt(async () => {
|
|
782
|
+
const [year, month, day] = parseWorkoutDate(toDate);
|
|
783
|
+
const dayOfWeek = new Date(Date.UTC(year, month - 1, day)).getUTCDay();
|
|
784
|
+
return apiCall(ctx, "POST", "/2.0/coach/calendar/copyProgramWorkout", { body: {
|
|
785
|
+
toProgramId,
|
|
786
|
+
pwId,
|
|
787
|
+
toDate: {
|
|
788
|
+
date: `${year}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")}`,
|
|
789
|
+
day,
|
|
790
|
+
month,
|
|
791
|
+
year,
|
|
792
|
+
dayOfWeek,
|
|
793
|
+
isToday: false
|
|
794
|
+
}
|
|
795
|
+
} });
|
|
796
|
+
}));
|
|
797
|
+
server.registerTool("session_save_as_template", {
|
|
798
|
+
title: "Save a session to the library",
|
|
799
|
+
description: "Save an existing session as a reusable template in the session library (POST .../programWorkout/saveWorkoutAsTemplate/{workoutId}). Pass the workout_id.",
|
|
800
|
+
inputSchema: { workoutId: z.number() },
|
|
801
|
+
annotations: {
|
|
802
|
+
readOnlyHint: false,
|
|
803
|
+
destructiveHint: false,
|
|
804
|
+
openWorldHint: true
|
|
805
|
+
}
|
|
806
|
+
}, ({ workoutId }) => apiCall(ctx, "POST", `/2.0/coach/calendar/programWorkout/saveWorkoutAsTemplate/${workoutId}`));
|
|
807
|
+
}
|
|
808
|
+
/** Workout building, read-back, publishing, and the session calendar lifecycle. */
|
|
809
|
+
function registerWorkoutTools(server, ctx) {
|
|
810
|
+
registerBuild(server, ctx);
|
|
811
|
+
registerLifecycle(server, ctx);
|
|
502
812
|
}
|
|
503
813
|
//#endregion
|
|
504
814
|
//#region src/tools/messaging.ts
|
|
@@ -581,4 +891,4 @@ function registerMessagingTools(server, ctx) {
|
|
|
581
891
|
registerWrites(server, ctx);
|
|
582
892
|
}
|
|
583
893
|
//#endregion
|
|
584
|
-
export { DEFAULT_RESULT_BUDGET, DESTRUCTIVE, NOT_CONFIRMED, READ, SYNC, apiCall, attempt, boundedSerialize, confirmGate, errorResult, idParam, jsonResult, registerExerciseTools, registerMessagingTools,
|
|
894
|
+
export { DEFAULT_RESULT_BUDGET, DESTRUCTIVE, NOT_CONFIRMED, READ, SYNC, apiCall, attempt, boundedSerialize, confirmGate, errorResult, idParam, jsonResult, registerAnalyticsTools, registerAthleteTools, registerExerciseTools, registerMessagingTools, registerReadTools, registerTeamTools, registerWorkoutTools, resultBudget, toId };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@trainheroic-unofficial/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -25,8 +25,8 @@
|
|
|
25
25
|
"@cfworker/json-schema": "^4.1.1",
|
|
26
26
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
27
27
|
"zod": "^4.4.3",
|
|
28
|
-
"@trainheroic-unofficial/dto": "0.
|
|
29
|
-
"@trainheroic-unofficial/js": "0.
|
|
28
|
+
"@trainheroic-unofficial/dto": "0.3.0",
|
|
29
|
+
"@trainheroic-unofficial/js": "0.3.0"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
32
|
"@types/node": "^26.0.0",
|