@wattsonclub/wattson-cli 0.1.0 → 0.1.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.
- package/CHANGELOG.md +11 -0
- package/README.md +118 -0
- package/dist/bin.js +366 -192
- package/package.json +8 -3
- package/skill/SKILL.md +9 -7
- package/skill/references/commands.md +197 -292
- package/skill/references/workout-building-blocks.md +266 -481
- package/skill/references/workout-text-format.md +71 -409
package/dist/bin.js
CHANGED
|
@@ -5,8 +5,8 @@ var __export = (target, all) => {
|
|
|
5
5
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
6
6
|
};
|
|
7
7
|
|
|
8
|
-
// src/
|
|
9
|
-
import { readFile
|
|
8
|
+
// src/tool-command.ts
|
|
9
|
+
import { readFile } from "node:fs/promises";
|
|
10
10
|
|
|
11
11
|
// ../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/external.js
|
|
12
12
|
var external_exports = {};
|
|
@@ -6169,13 +6169,13 @@ function createPlannerClient(options = {}) {
|
|
|
6169
6169
|
try {
|
|
6170
6170
|
return await fetchImpl(buildUrl(baseUrl, input.path, input.query), {
|
|
6171
6171
|
method,
|
|
6172
|
+
redirect: "error",
|
|
6172
6173
|
headers,
|
|
6173
6174
|
...body === void 0 ? {} : { body }
|
|
6174
6175
|
});
|
|
6175
6176
|
} catch (error) {
|
|
6176
6177
|
throw new PlannerClientError({
|
|
6177
|
-
message:
|
|
6178
|
-
cause: error
|
|
6178
|
+
message: "Unable to reach the planner API. Check the connection and selected API URL."
|
|
6179
6179
|
});
|
|
6180
6180
|
}
|
|
6181
6181
|
}
|
|
@@ -6193,18 +6193,33 @@ function createPlannerClient(options = {}) {
|
|
|
6193
6193
|
const parsedError = PlannerApiErrorSchema.safeParse(payload);
|
|
6194
6194
|
if (parsedError.success) {
|
|
6195
6195
|
throw new PlannerClientError({
|
|
6196
|
-
message:
|
|
6196
|
+
message: {
|
|
6197
|
+
bad_request: "Invalid request. Check the command input schema.",
|
|
6198
|
+
unauthorized: "Authentication required. Sign in again.",
|
|
6199
|
+
forbidden: "This operation is not permitted for this account or access mode.",
|
|
6200
|
+
not_found: "Requested resource was not found.",
|
|
6201
|
+
conflict: "The resource changed or conflicts with this request. Fetch the latest version and retry.",
|
|
6202
|
+
unprocessable: "The request could not be processed. Check its inputs and validation result.",
|
|
6203
|
+
too_many_requests: "Too many requests. Try again later.",
|
|
6204
|
+
service_unavailable: "The planner service is temporarily unavailable.",
|
|
6205
|
+
internal: "The planner service encountered an error."
|
|
6206
|
+
}[parsedError.data.error.code],
|
|
6197
6207
|
statusCode: response.status,
|
|
6198
6208
|
code: parsedError.data.error.code,
|
|
6199
|
-
|
|
6209
|
+
// Never forward server diagnostic text or echoed sensitive input.
|
|
6210
|
+
details: []
|
|
6200
6211
|
});
|
|
6201
6212
|
}
|
|
6202
6213
|
throw new PlannerClientError({
|
|
6203
|
-
message:
|
|
6214
|
+
message: `Planner request failed with HTTP ${response.status}.`,
|
|
6204
6215
|
statusCode: response.status
|
|
6205
6216
|
});
|
|
6206
6217
|
}
|
|
6207
|
-
|
|
6218
|
+
const parsedOutput = input.outputSchema.safeParse(payload);
|
|
6219
|
+
if (!parsedOutput.success) {
|
|
6220
|
+
throw new PlannerClientError({ message: "The planner API returned an unexpected response.", statusCode: response.status });
|
|
6221
|
+
}
|
|
6222
|
+
return parsedOutput.data;
|
|
6208
6223
|
}
|
|
6209
6224
|
return {
|
|
6210
6225
|
baseUrl,
|
|
@@ -6658,6 +6673,16 @@ function createPlannerClient(options = {}) {
|
|
|
6658
6673
|
outputSchema: ListMySavedWorkoutsOutputSchema
|
|
6659
6674
|
});
|
|
6660
6675
|
},
|
|
6676
|
+
async createMyWorkoutShare(input) {
|
|
6677
|
+
const parsed = CreateWorkoutShareInputSchema.parse(input);
|
|
6678
|
+
return request({ method: "POST", path: "/me/workout-shares", body: parsed, outputSchema: CreateWorkoutShareOutputSchema });
|
|
6679
|
+
},
|
|
6680
|
+
async getMyWorkoutSharePreview(shareId) {
|
|
6681
|
+
return request({ method: "GET", path: `/me/workout-shares/${encodeURIComponent(shareId)}`, outputSchema: ImportWorkoutShareOutputSchema.omit({ alreadyImported: true }) });
|
|
6682
|
+
},
|
|
6683
|
+
async importMyWorkoutShare(shareId) {
|
|
6684
|
+
return request({ method: "POST", path: `/me/workout-shares/${encodeURIComponent(shareId)}/import`, outputSchema: ImportWorkoutShareOutputSchema });
|
|
6685
|
+
},
|
|
6661
6686
|
async uploadMyCompletedRide(input) {
|
|
6662
6687
|
const { rideId, ...body } = UploadCompletedRideInputSchema.parse(input);
|
|
6663
6688
|
return request({
|
|
@@ -6850,10 +6875,273 @@ function createPlannerClient(options = {}) {
|
|
|
6850
6875
|
};
|
|
6851
6876
|
}
|
|
6852
6877
|
|
|
6878
|
+
// ../planner-client/src/research.ts
|
|
6879
|
+
function scoreResearchDocument(document, query) {
|
|
6880
|
+
const tokens = query.toLowerCase().split(/[^a-z0-9]+/i).map((token) => token.trim()).filter((token) => token.length > 0);
|
|
6881
|
+
if (tokens.length === 0) {
|
|
6882
|
+
return 1;
|
|
6883
|
+
}
|
|
6884
|
+
const haystack = [
|
|
6885
|
+
document.title,
|
|
6886
|
+
document.url,
|
|
6887
|
+
document.text,
|
|
6888
|
+
JSON.stringify(document.metadata ?? {})
|
|
6889
|
+
].join("\n").toLowerCase();
|
|
6890
|
+
return tokens.reduce((score, token) => score + (haystack.includes(token) ? 1 : 0), 0);
|
|
6891
|
+
}
|
|
6892
|
+
async function buildResearchDocuments(client) {
|
|
6893
|
+
const documents = [];
|
|
6894
|
+
const [profileResult, sessionsResult, workoutsResult] = await Promise.allSettled([
|
|
6895
|
+
client.getMyProfile(),
|
|
6896
|
+
client.getMyRecentSessions({ limit: 10 }),
|
|
6897
|
+
client.listMySavedWorkouts()
|
|
6898
|
+
]);
|
|
6899
|
+
if (profileResult.status === "fulfilled") {
|
|
6900
|
+
documents.push({
|
|
6901
|
+
id: `profile:${profileResult.value.profile.athleteId}`,
|
|
6902
|
+
title: `${profileResult.value.profile.athleteId} athlete profile`,
|
|
6903
|
+
url: `${client.baseUrl}/me/profile`,
|
|
6904
|
+
text: JSON.stringify(profileResult.value.profile, null, 2),
|
|
6905
|
+
metadata: {
|
|
6906
|
+
kind: "athlete_profile",
|
|
6907
|
+
athleteId: profileResult.value.profile.athleteId
|
|
6908
|
+
}
|
|
6909
|
+
});
|
|
6910
|
+
}
|
|
6911
|
+
if (sessionsResult.status === "fulfilled") {
|
|
6912
|
+
for (const session of sessionsResult.value.sessions) {
|
|
6913
|
+
documents.push({
|
|
6914
|
+
id: `session:${session.id}`,
|
|
6915
|
+
title: `Workout session ${session.id}`,
|
|
6916
|
+
url: `${client.baseUrl}/me/runtime/sessions/${encodeURIComponent(session.id)}/summary`,
|
|
6917
|
+
text: JSON.stringify(session, null, 2),
|
|
6918
|
+
metadata: {
|
|
6919
|
+
kind: "workout_session",
|
|
6920
|
+
athleteId: session.athleteId,
|
|
6921
|
+
sessionId: session.id,
|
|
6922
|
+
workoutId: session.workoutId,
|
|
6923
|
+
status: session.status
|
|
6924
|
+
}
|
|
6925
|
+
});
|
|
6926
|
+
}
|
|
6927
|
+
}
|
|
6928
|
+
if (workoutsResult.status === "fulfilled") {
|
|
6929
|
+
for (const record of workoutsResult.value.workouts) {
|
|
6930
|
+
documents.push({
|
|
6931
|
+
id: `workout:${record.workout.id}:v${record.workout.version}`,
|
|
6932
|
+
title: record.workout.title ?? record.runtimePlan.title,
|
|
6933
|
+
url: `${client.baseUrl}/me/workouts/${encodeURIComponent(record.workout.id)}?workoutVersion=${record.workout.version}`,
|
|
6934
|
+
text: JSON.stringify(record, null, 2),
|
|
6935
|
+
metadata: {
|
|
6936
|
+
kind: "saved_workout",
|
|
6937
|
+
athleteId: record.workout.athleteId,
|
|
6938
|
+
workoutId: record.workout.id,
|
|
6939
|
+
workoutVersion: record.workout.version
|
|
6940
|
+
}
|
|
6941
|
+
});
|
|
6942
|
+
}
|
|
6943
|
+
}
|
|
6944
|
+
return documents;
|
|
6945
|
+
}
|
|
6946
|
+
async function searchPlannerResearch(client, query) {
|
|
6947
|
+
const documents = await buildResearchDocuments(client);
|
|
6948
|
+
return { results: documents.map((document) => ({ document, score: scoreResearchDocument(document, query) })).filter((entry) => entry.score > 0).sort((a, b) => b.score - a.score || a.document.title.localeCompare(b.document.title)).slice(0, 8).map(({ document, score }) => ({ id: document.id, title: document.title, url: document.url, score })) };
|
|
6949
|
+
}
|
|
6950
|
+
async function fetchPlannerResearch(client, id) {
|
|
6951
|
+
const document = (await buildResearchDocuments(client)).find((candidate) => candidate.id === id);
|
|
6952
|
+
if (!document) throw new Error(`Unknown research document ${id}.`);
|
|
6953
|
+
return document;
|
|
6954
|
+
}
|
|
6955
|
+
|
|
6956
|
+
// src/parity.ts
|
|
6957
|
+
var shareSchema = external_exports.object({ shareId: external_exports.string().uuid() });
|
|
6958
|
+
var emptySchema = external_exports.object({});
|
|
6959
|
+
var toolRoutes = [
|
|
6960
|
+
{ tool: "list_saved_workouts", command: "workout list", internal: "listSavedWorkouts", public: "listMySavedWorkouts", schema: ListSavedWorkoutsInputSchema, publicSchema: ListMySavedWorkoutsInputSchema, extended: false, signedInOnly: false, limited: false },
|
|
6961
|
+
{ tool: "health", command: "health", internal: "health", public: "health", schema: emptySchema, publicSchema: emptySchema, extended: false, signedInOnly: false, limited: false },
|
|
6962
|
+
{ tool: "get_athlete_profile", command: "athlete profile", internal: "getAthleteProfile", public: "getMyProfile", schema: GetAthleteProfileInputSchema, publicSchema: emptySchema, extended: false, signedInOnly: false, limited: false },
|
|
6963
|
+
{ tool: "get_readiness_snapshot", command: "athlete readiness latest", internal: "getReadinessSnapshot", public: "getMyReadinessSnapshot", schema: GetReadinessSnapshotInputSchema, publicSchema: GetMyReadinessSnapshotInputSchema, extended: false, signedInOnly: false, limited: false },
|
|
6964
|
+
{ tool: "get_recent_sessions", command: "athlete sessions", internal: "getRecentSessions", public: "getMyRecentSessions", schema: GetRecentSessionsInputSchema, publicSchema: GetMyRecentSessionsInputSchema, extended: false, signedInOnly: false, limited: false },
|
|
6965
|
+
{ tool: "get_saved_workout", command: "workout get", internal: "getSavedWorkout", public: "getMySavedWorkout", schema: GetSavedWorkoutInputSchema, publicSchema: GetSavedWorkoutInputSchema, extended: false, signedInOnly: false, limited: false },
|
|
6966
|
+
{ tool: "get_workout_runtime_plan", command: "workout runtime-plan", internal: "getWorkoutRuntimePlan", public: "getMyWorkoutRuntimePlan", schema: GetWorkoutRuntimePlanInputSchema, publicSchema: GetWorkoutRuntimePlanInputSchema, extended: false, signedInOnly: false, limited: false },
|
|
6967
|
+
{ tool: "list_saved_workout_revisions", command: "workout revisions", internal: "listSavedWorkoutRevisions", public: "listMySavedWorkoutRevisions", schema: ListSavedWorkoutRevisionsInputSchema, publicSchema: ListSavedWorkoutRevisionsInputSchema, extended: false, signedInOnly: false, limited: false },
|
|
6968
|
+
{ tool: "summarize_session", command: "session summary", internal: "summarizeSession", public: "summarizeMySession", schema: SummarizeSessionInputSchema, publicSchema: SummarizeSessionInputSchema, extended: false, signedInOnly: false, limited: false },
|
|
6969
|
+
{ tool: "get_session_telemetry", command: "session telemetry", internal: "getSessionTelemetry", public: "getMySessionTelemetry", schema: GetSessionTelemetryInputSchema, publicSchema: GetSessionTelemetryInputSchema, extended: false, signedInOnly: false, limited: false },
|
|
6970
|
+
{ tool: "get_session_artifacts", command: "session artifacts", internal: "getSessionArtifacts", public: "getMySessionArtifacts", schema: GetSessionArtifactsInputSchema, publicSchema: GetSessionArtifactsInputSchema, extended: false, signedInOnly: false, limited: false },
|
|
6971
|
+
{ tool: "get_session_fit_export", command: "session fit", internal: "getSessionFitExport", public: "getMySessionFitExport", schema: GetSessionFitExportInputSchema, publicSchema: GetSessionFitExportInputSchema, extended: false, signedInOnly: false, limited: false },
|
|
6972
|
+
{ tool: "lookup_climb", command: "course lookup", internal: "lookupClimb", public: "lookupMyClimb", schema: LookupClimbInputSchema, publicSchema: LookupMyClimbInputSchema, extended: true, signedInOnly: false, limited: false, positional: "query" },
|
|
6973
|
+
{ tool: "find_my_strava_segments", command: "strava find-segments", internal: "findMyStravaSegments", public: "findMyStravaSegments", schema: FindStravaSegmentsInputSchema, publicSchema: FindStravaSegmentsInputSchema, extended: true, signedInOnly: true, limited: false, positional: "query" },
|
|
6974
|
+
{ tool: "import_strava_segment", command: "course import-strava-segment", internal: "importMyStravaSegment", public: "importMyStravaSegment", schema: ImportStravaSegmentInputSchema, publicSchema: ImportStravaSegmentInputSchema, extended: true, signedInOnly: true, limited: false, positional: "segment" },
|
|
6975
|
+
{ tool: "import_gpx_course", command: "course import-gpx", internal: "importGpxCourse", public: "importMyGpxCourse", schema: ImportGpxCourseInputSchema, publicSchema: ImportMyGpxCourseInputSchema, extended: true, signedInOnly: false, limited: false },
|
|
6976
|
+
{ tool: "generate_terrain_course", command: "course generate-terrain", internal: "generateTerrainCourse", public: "generateMyTerrainCourse", schema: GenerateTerrainCourseInputSchema, publicSchema: GenerateMyTerrainCourseInputSchema, extended: true, signedInOnly: false, limited: false },
|
|
6977
|
+
{ tool: "import_strava_activity_course", command: "course import-strava-activity", internal: "importMyStravaActivityCourse", public: "importMyStravaActivityCourse", schema: ImportStravaActivityCourseInputSchema, publicSchema: ImportStravaActivityCourseInputSchema, extended: true, signedInOnly: true, limited: true, positional: "activity" },
|
|
6978
|
+
{ tool: "build_road_course", command: "course build-road", internal: "buildRoadCourse", public: "buildMyRoadCourse", schema: BuildRoadCourseInputSchema, publicSchema: BuildMyRoadCourseInputSchema, extended: true, signedInOnly: false, limited: false },
|
|
6979
|
+
{ tool: "import_climb_page", command: "course import-climb-page", internal: "importClimbPage", public: "importMyClimbPage", schema: ImportClimbPageInputSchema, publicSchema: ImportMyClimbPageInputSchema, extended: true, signedInOnly: false, limited: false, positional: "url" },
|
|
6980
|
+
{ tool: "get_my_climb_history", command: "course history", internal: "getClimbHistory", public: "getMyClimbHistory", schema: GetClimbHistoryInputSchema, publicSchema: GetMyClimbHistoryInputSchema, extended: true, signedInOnly: false, limited: false },
|
|
6981
|
+
{ tool: "explore_strava_segments", command: "strava explore", internal: "exploreMyStravaSegments", public: "exploreMyStravaSegments", schema: ExploreStravaSegmentsInputSchema, publicSchema: ExploreStravaSegmentsInputSchema, extended: true, signedInOnly: true, limited: true },
|
|
6982
|
+
{ tool: "get_my_strava_segment_efforts", command: "strava segment-efforts", internal: "getMyStravaSegmentEfforts", public: "getMyStravaSegmentEfforts", schema: ListStravaSegmentEffortsInputSchema, publicSchema: ListStravaSegmentEffortsInputSchema, extended: true, signedInOnly: true, limited: true, positional: "segment" },
|
|
6983
|
+
{ tool: "list_my_strava_starred_segments", command: "strava starred-segments", internal: "listMyStravaStarredSegments", public: "listMyStravaStarredSegments", schema: ListStravaStarredSegmentsInputSchema, publicSchema: ListStravaStarredSegmentsInputSchema, extended: true, signedInOnly: true, limited: true },
|
|
6984
|
+
{ tool: "list_my_favourite_strava_climbs", command: "strava favourite-climbs", internal: "listMyFavouriteStravaClimbs", public: "listMyFavouriteStravaClimbs", schema: ListFavouriteStravaClimbsInputSchema, publicSchema: ListFavouriteStravaClimbsInputSchema, extended: true, signedInOnly: true, limited: true },
|
|
6985
|
+
{ tool: "recommend_first_workout", command: "recommend first", internal: "recommendMyOnboardingWorkout", public: "recommendMyOnboardingWorkout", schema: RecommendOnboardingWorkoutInputSchema, publicSchema: RecommendOnboardingWorkoutInputSchema, extended: true, signedInOnly: true, limited: false },
|
|
6986
|
+
{ tool: "list_my_strava_routes", command: "strava routes", internal: "listMyStravaRoutes", public: "listMyStravaRoutes", schema: ListStravaRoutesInputSchema, publicSchema: ListStravaRoutesInputSchema, extended: true, signedInOnly: true, limited: true },
|
|
6987
|
+
{ tool: "import_strava_route", command: "course import-strava-route", internal: "importMyStravaRoute", public: "importMyStravaRoute", schema: ImportStravaRouteInputSchema, publicSchema: ImportStravaRouteInputSchema, extended: true, signedInOnly: true, limited: true, positional: "route" },
|
|
6988
|
+
{ tool: "draft_workout", command: "workout draft", internal: "draftWorkout", public: "draftMyWorkout", schema: DraftWorkoutInputSchema, publicSchema: DraftMyWorkoutInputSchema, extended: false, signedInOnly: false, limited: false },
|
|
6989
|
+
{ tool: "revise_workout", command: "workout revise", internal: "reviseWorkout", public: "reviseMyWorkout", schema: ReviseWorkoutInputSchema, publicSchema: ReviseWorkoutInputSchema, extended: false, signedInOnly: false, limited: false },
|
|
6990
|
+
{ tool: "validate_workout", command: "workout validate", internal: "validateWorkout", public: "validateWorkout", schema: ValidateWorkoutInputSchema, publicSchema: ValidateWorkoutInputSchema, extended: false, signedInOnly: false, limited: false },
|
|
6991
|
+
{ tool: "save_workout", command: "workout save", internal: "saveWorkout", public: "saveMyWorkout", schema: SaveWorkoutInputSchema, publicSchema: SaveWorkoutInputSchema, extended: false, signedInOnly: false, limited: false },
|
|
6992
|
+
{ tool: "schedule_saved_workout", command: "workout schedule", internal: "scheduleSavedWorkout", public: "scheduleMySavedWorkout", schema: ScheduleSavedWorkoutInputSchema, publicSchema: ScheduleSavedWorkoutInputSchema, extended: false, signedInOnly: false, limited: false },
|
|
6993
|
+
{ tool: "retire_saved_workout", command: "workout retire", internal: "retireSavedWorkout", public: "retireMySavedWorkout", schema: RetireSavedWorkoutInputSchema, publicSchema: RetireSavedWorkoutInputSchema, extended: false, signedInOnly: false, limited: false },
|
|
6994
|
+
{ tool: "trigger_session_strava_sync", command: "session strava-sync", internal: "triggerSessionStravaSync", public: "triggerMySessionStravaSync", schema: TriggerSessionStravaSyncInputSchema, publicSchema: TriggerSessionStravaSyncInputSchema, extended: false, signedInOnly: false, limited: false },
|
|
6995
|
+
{ tool: "create_workout_share", command: "share create", internal: "createMyWorkoutShare", public: "createMyWorkoutShare", schema: CreateWorkoutShareInputSchema, publicSchema: CreateWorkoutShareInputSchema, extended: true, signedInOnly: true, limited: false, positional: "workoutId" },
|
|
6996
|
+
{ tool: "preview_workout_share", command: "share preview", internal: "getMyWorkoutSharePreview", public: "getMyWorkoutSharePreview", schema: shareSchema, publicSchema: shareSchema, extended: true, signedInOnly: true, limited: false, positional: "shareId", scalar: "shareId" },
|
|
6997
|
+
{ tool: "import_workout_share", command: "share import", internal: "importMyWorkoutShare", public: "importMyWorkoutShare", schema: shareSchema, publicSchema: shareSchema, extended: true, signedInOnly: true, limited: false, positional: "shareId", scalar: "shareId" }
|
|
6998
|
+
];
|
|
6999
|
+
|
|
7000
|
+
// src/tool-command.ts
|
|
7001
|
+
var researchRoutes = [
|
|
7002
|
+
{ tool: "search", command: "research search", schema: external_exports.object({ query: external_exports.string().min(1) }) },
|
|
7003
|
+
{ tool: "fetch", command: "research fetch", schema: external_exports.object({ id: external_exports.string().min(1) }) }
|
|
7004
|
+
];
|
|
7005
|
+
function unwrap(schema) {
|
|
7006
|
+
if (schema instanceof external_exports.ZodEffects) return unwrap(schema.innerType());
|
|
7007
|
+
if (schema instanceof external_exports.ZodOptional || schema instanceof external_exports.ZodNullable) return unwrap(schema.unwrap());
|
|
7008
|
+
if (schema instanceof external_exports.ZodDefault) return unwrap(schema.removeDefault());
|
|
7009
|
+
return schema;
|
|
7010
|
+
}
|
|
7011
|
+
var kebab = (s) => s.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`);
|
|
7012
|
+
function describeSchema(schema) {
|
|
7013
|
+
const meta = schema.description ? { description: schema.description } : {};
|
|
7014
|
+
if (schema instanceof external_exports.ZodEffects) return { ...describeSchema(schema.innerType()), ...meta };
|
|
7015
|
+
if (schema instanceof external_exports.ZodOptional) return { ...describeSchema(schema.unwrap()), ...meta };
|
|
7016
|
+
if (schema instanceof external_exports.ZodNullable) return { ...meta, anyOf: [describeSchema(schema.unwrap()), { type: "null" }] };
|
|
7017
|
+
if (schema instanceof external_exports.ZodDefault) return { ...describeSchema(schema.removeDefault()), ...meta, default: schema._def.defaultValue() };
|
|
7018
|
+
if (schema instanceof external_exports.ZodObject) return { ...meta, type: "object", properties: Object.fromEntries(Object.entries(schema.shape).map(([key, value]) => [key, describeSchema(value)])), required: Object.entries(schema.shape).filter(([, value]) => !value.isOptional()).map(([key]) => key), additionalProperties: schema._def.unknownKeys === "passthrough" };
|
|
7019
|
+
if (schema instanceof external_exports.ZodArray) return { ...meta, type: "array", items: describeSchema(schema.element), ...schema._def.minLength ? { minItems: schema._def.minLength.value } : {}, ...schema._def.maxLength ? { maxItems: schema._def.maxLength.value } : {}, ...schema._def.exactLength ? { minItems: schema._def.exactLength.value, maxItems: schema._def.exactLength.value } : {} };
|
|
7020
|
+
if (schema instanceof external_exports.ZodEnum) return { ...meta, type: "string", enum: schema.options };
|
|
7021
|
+
if (schema instanceof external_exports.ZodUnion || schema instanceof external_exports.ZodDiscriminatedUnion) return { ...meta, anyOf: schema.options.map(describeSchema) };
|
|
7022
|
+
if (schema instanceof external_exports.ZodLiteral) return { ...meta, const: schema.value };
|
|
7023
|
+
if (schema instanceof external_exports.ZodRecord) return { ...meta, type: "object", additionalProperties: describeSchema(schema.valueSchema) };
|
|
7024
|
+
if (schema instanceof external_exports.ZodNull) return { ...meta, type: "null" };
|
|
7025
|
+
if (schema instanceof external_exports.ZodBoolean) return { ...meta, type: "boolean" };
|
|
7026
|
+
if (schema instanceof external_exports.ZodUnknown || schema instanceof external_exports.ZodAny) return meta;
|
|
7027
|
+
if (schema instanceof external_exports.ZodLazy) return describeSchema(schema.schema);
|
|
7028
|
+
const result = { ...meta, type: schema instanceof external_exports.ZodNumber ? "number" : "string" };
|
|
7029
|
+
for (const check of schema._def.checks ?? []) {
|
|
7030
|
+
if (check.kind === "int") result.type = "integer";
|
|
7031
|
+
if (check.kind === "min") result[schema instanceof external_exports.ZodString ? "minLength" : check.inclusive ? "minimum" : "exclusiveMinimum"] = check.value;
|
|
7032
|
+
if (check.kind === "max") result[schema instanceof external_exports.ZodString ? "maxLength" : check.inclusive ? "maximum" : "exclusiveMaximum"] = check.value;
|
|
7033
|
+
if (check.kind === "length") {
|
|
7034
|
+
result.minLength = check.value;
|
|
7035
|
+
result.maxLength = check.value;
|
|
7036
|
+
}
|
|
7037
|
+
if (check.kind === "multipleOf") result.multipleOf = check.value;
|
|
7038
|
+
if (check.kind === "regex") result.pattern = check.regex.source;
|
|
7039
|
+
if (["uuid", "email", "url", "datetime"].includes(check.kind)) result.format = { url: "uri", datetime: "date-time" }[check.kind] ?? check.kind;
|
|
7040
|
+
}
|
|
7041
|
+
return result;
|
|
7042
|
+
}
|
|
7043
|
+
function decode(raw, schema) {
|
|
7044
|
+
const inner = unwrap(schema);
|
|
7045
|
+
if (raw === "null" && schema.isNullable()) return null;
|
|
7046
|
+
if (inner instanceof external_exports.ZodNumber) {
|
|
7047
|
+
if (raw.trim() === "" || !Number.isFinite(Number(raw))) throw new Error(`Invalid number: ${raw}`);
|
|
7048
|
+
return Number(raw);
|
|
7049
|
+
}
|
|
7050
|
+
if (inner instanceof external_exports.ZodBoolean) {
|
|
7051
|
+
if (!["true", "false"].includes(raw)) throw new Error("Boolean values must be true or false.");
|
|
7052
|
+
return raw === "true";
|
|
7053
|
+
}
|
|
7054
|
+
if (inner instanceof external_exports.ZodObject || inner instanceof external_exports.ZodArray || inner instanceof external_exports.ZodUnion) return JSON.parse(raw);
|
|
7055
|
+
return raw;
|
|
7056
|
+
}
|
|
7057
|
+
function field(schema, name) {
|
|
7058
|
+
const inner = unwrap(schema);
|
|
7059
|
+
if (!(inner instanceof external_exports.ZodObject)) throw new Error(`Not an object: ${name}`);
|
|
7060
|
+
const key = Object.keys(inner.shape).find((key2) => kebab(key2) === name || key2 === name);
|
|
7061
|
+
if (!key) throw new Error(`Unknown option: --${name}`);
|
|
7062
|
+
return [key, inner.shape[key]];
|
|
7063
|
+
}
|
|
7064
|
+
async function parseToolArgs(args, schema, positional) {
|
|
7065
|
+
let input = {};
|
|
7066
|
+
const jsonIndex = args.indexOf("--input-json"), fileIndex = args.indexOf("--input-file");
|
|
7067
|
+
if (jsonIndex >= 0 && fileIndex >= 0) throw new Error("Use only one of --input-json or --input-file.");
|
|
7068
|
+
const index = Math.max(jsonIndex, fileIndex);
|
|
7069
|
+
if (index >= 0) {
|
|
7070
|
+
const value = args[index + 1];
|
|
7071
|
+
if (!value || value.startsWith("--")) throw new Error("Missing input value.");
|
|
7072
|
+
input = JSON.parse(fileIndex >= 0 ? await readFile(value, "utf8") : value);
|
|
7073
|
+
args.splice(index, 2);
|
|
7074
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) throw new Error("Input must be a JSON object.");
|
|
7075
|
+
if (args.length) throw new Error("Do not combine JSON input with other arguments.");
|
|
7076
|
+
return input;
|
|
7077
|
+
}
|
|
7078
|
+
if (positional && args[0] && !args[0].startsWith("--")) input[positional] = args.shift();
|
|
7079
|
+
while (args.length) {
|
|
7080
|
+
const option = args.shift();
|
|
7081
|
+
if (!option.startsWith("--")) throw new Error(`Unexpected argument: ${option}`);
|
|
7082
|
+
const raw = args.shift();
|
|
7083
|
+
if (raw === void 0 || raw.startsWith("--")) throw new Error(`Missing value for ${option}.`);
|
|
7084
|
+
if (option === "--gpx-file") {
|
|
7085
|
+
field(schema, "gpx");
|
|
7086
|
+
if ("gpx" in input) throw new Error("Duplicate GPX input.");
|
|
7087
|
+
input.gpx = await readFile(raw, "utf8");
|
|
7088
|
+
continue;
|
|
7089
|
+
}
|
|
7090
|
+
let current = input, currentSchema = schema;
|
|
7091
|
+
const parts = option.slice(2).split(".");
|
|
7092
|
+
for (const [i, part] of parts.entries()) {
|
|
7093
|
+
const [key, valueSchema] = field(currentSchema, part);
|
|
7094
|
+
if (i < parts.length - 1) {
|
|
7095
|
+
current[key] ??= {};
|
|
7096
|
+
current = current[key];
|
|
7097
|
+
currentSchema = valueSchema;
|
|
7098
|
+
} else {
|
|
7099
|
+
const inner = unwrap(valueSchema);
|
|
7100
|
+
if (inner instanceof external_exports.ZodArray && !raw.startsWith("[")) {
|
|
7101
|
+
current[key] = [...current[key] ?? [], decode(raw, inner.element)];
|
|
7102
|
+
} else {
|
|
7103
|
+
if (key in current) throw new Error(`Duplicate option: ${option}`);
|
|
7104
|
+
current[key] = decode(raw, valueSchema);
|
|
7105
|
+
}
|
|
7106
|
+
}
|
|
7107
|
+
}
|
|
7108
|
+
}
|
|
7109
|
+
return input;
|
|
7110
|
+
}
|
|
7111
|
+
async function runToolCommand(args, client, signedIn, limited) {
|
|
7112
|
+
const generic = args[0] === "tool";
|
|
7113
|
+
if (generic && args[1] === "list" && args.length !== 2) throw new Error("tool list does not accept arguments.");
|
|
7114
|
+
if (generic && args[1] === "schema" && args.length !== 3) throw new Error("Use tool schema <MCP-name>.");
|
|
7115
|
+
if (generic && args[1] === "list") return { result: [...toolRoutes.filter((r) => !limited || !r.limited).map(({ tool, command }) => ({ tool, command })), ...researchRoutes.map(({ tool, command }) => ({ tool, command }))] };
|
|
7116
|
+
const schemaOnly = generic && args[1] === "schema";
|
|
7117
|
+
const toolName = generic ? args[schemaOnly ? 2 : 1] : void 0;
|
|
7118
|
+
const route = toolRoutes.find((r) => generic ? r.tool === toolName : r.extended && r.command.split(" ").every((s, i) => args[i] === s));
|
|
7119
|
+
const research = researchRoutes.find((r) => generic ? r.tool === toolName : r.command.split(" ").every((s, i) => args[i] === s));
|
|
7120
|
+
if (!route && !research) {
|
|
7121
|
+
if (generic) throw new Error(`Unknown tool: ${toolName}`);
|
|
7122
|
+
return void 0;
|
|
7123
|
+
}
|
|
7124
|
+
if (route?.limited && limited) throw new Error("This command is unavailable in Strava limited-access mode.");
|
|
7125
|
+
const schema = research?.schema ?? (signedIn ? route.publicSchema : route.schema);
|
|
7126
|
+
if (schemaOnly) return { result: { tool: toolName, command: (route ?? research).command, inputSchema: describeSchema(schema), note: "Contract refinements are validated on invocation. Flags use kebab-case; nested fields use dots; arrays accept JSON or repeated values." } };
|
|
7127
|
+
if ((research || route?.signedInOnly) && !signedIn) throw new Error("Sign in with `wattson auth login` to use this command.");
|
|
7128
|
+
args.splice(0, generic ? 2 : (route ?? research).command.split(" ").length);
|
|
7129
|
+
const input = await parseToolArgs(args, schema, generic ? void 0 : research ? research.tool === "search" ? "query" : "id" : route.positional);
|
|
7130
|
+
if (signedIn && "athleteId" in input && !unwrap(schema).shape.athleteId) throw new Error("Authenticated commands target the signed-in athlete; athleteId is not accepted.");
|
|
7131
|
+
const parsed = schema.parse(input);
|
|
7132
|
+
if (research) return { result: research.tool === "search" ? await searchPlannerResearch(client, parsed.query) : await fetchPlannerResearch(client, parsed.id) };
|
|
7133
|
+
const selected = route;
|
|
7134
|
+
const method = client[signedIn ? selected.public : selected.internal];
|
|
7135
|
+
return { result: await method(selected.scalar ? parsed[selected.scalar] : parsed) };
|
|
7136
|
+
}
|
|
7137
|
+
|
|
7138
|
+
// src/cli.ts
|
|
7139
|
+
import { readFile as readFile3 } from "node:fs/promises";
|
|
7140
|
+
|
|
6853
7141
|
// src/auth.ts
|
|
6854
7142
|
import { spawn } from "node:child_process";
|
|
6855
7143
|
import { createHash, randomBytes } from "node:crypto";
|
|
6856
|
-
import { chmod, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
7144
|
+
import { chmod, mkdir, readFile as readFile2, rename, rm, writeFile } from "node:fs/promises";
|
|
6857
7145
|
import { createServer } from "node:http";
|
|
6858
7146
|
import os from "node:os";
|
|
6859
7147
|
import path from "node:path";
|
|
@@ -6967,7 +7255,7 @@ function parseJsonSafely2(raw) {
|
|
|
6967
7255
|
}
|
|
6968
7256
|
async function readStoredAuthState(authFilePath) {
|
|
6969
7257
|
try {
|
|
6970
|
-
const raw = await
|
|
7258
|
+
const raw = await readFile2(authFilePath, "utf8");
|
|
6971
7259
|
const trimmed = raw.trim();
|
|
6972
7260
|
if (trimmed.length === 0) {
|
|
6973
7261
|
return void 0;
|
|
@@ -6977,7 +7265,7 @@ async function readStoredAuthState(authFilePath) {
|
|
|
6977
7265
|
if (error.code === "ENOENT") {
|
|
6978
7266
|
return void 0;
|
|
6979
7267
|
}
|
|
6980
|
-
throw
|
|
7268
|
+
throw new Error("Unable to read saved login. Repair or remove the credential file, then sign in again.");
|
|
6981
7269
|
}
|
|
6982
7270
|
}
|
|
6983
7271
|
async function writeStoredAuthState(authFilePath, state) {
|
|
@@ -6995,6 +7283,7 @@ async function writeStoredAuthState(authFilePath, state) {
|
|
|
6995
7283
|
async function postTokenRequest(baseUrl, payload, fetchImpl) {
|
|
6996
7284
|
const response = await fetchImpl(`${normalizeBaseUrl2(baseUrl)}/token`, {
|
|
6997
7285
|
method: "POST",
|
|
7286
|
+
redirect: "error",
|
|
6998
7287
|
headers: {
|
|
6999
7288
|
"content-type": "application/x-www-form-urlencoded"
|
|
7000
7289
|
},
|
|
@@ -7006,7 +7295,7 @@ async function postTokenRequest(baseUrl, payload, fetchImpl) {
|
|
|
7006
7295
|
throw new Error(`OAuth token endpoint returned an unexpected response (HTTP ${response.status}).`);
|
|
7007
7296
|
}
|
|
7008
7297
|
if (!response.ok || parsed.data.error) {
|
|
7009
|
-
throw new Error(
|
|
7298
|
+
throw new Error(`OAuth token request failed with HTTP ${response.status}.`);
|
|
7010
7299
|
}
|
|
7011
7300
|
return parsed.data;
|
|
7012
7301
|
}
|
|
@@ -7264,6 +7553,7 @@ async function loginWithDeviceFlow(options) {
|
|
|
7264
7553
|
if (remaining <= 0) throw new Error("Device login timed out. Run login again.");
|
|
7265
7554
|
return await fetchImpl(url, {
|
|
7266
7555
|
...init,
|
|
7556
|
+
redirect: "error",
|
|
7267
7557
|
signal: AbortSignal.timeout(Math.max(1, Math.ceil(Math.min(3e4, remaining))))
|
|
7268
7558
|
});
|
|
7269
7559
|
};
|
|
@@ -7358,34 +7648,41 @@ function usage() {
|
|
|
7358
7648
|
" auth status",
|
|
7359
7649
|
" auth logout",
|
|
7360
7650
|
"",
|
|
7361
|
-
"Inspection
|
|
7651
|
+
"Inspection commands:",
|
|
7362
7652
|
" health",
|
|
7363
7653
|
" athlete profile [athleteId]",
|
|
7364
7654
|
" athlete readiness latest [athleteId] [--as-of <isoDateTime>]",
|
|
7365
7655
|
" athlete sessions [athleteId] [--from <date>] [--to <date>] [--limit <n>]",
|
|
7366
|
-
"
|
|
7367
|
-
" runtime-plan today [athleteId] [--date <date>]",
|
|
7656
|
+
" workout list [athleteId] [--limit <n>]",
|
|
7368
7657
|
" workout get <workoutId> [--version <n>]",
|
|
7369
7658
|
" workout revisions <workoutId>",
|
|
7370
7659
|
" workout runtime-plan <workoutId> --version <n>",
|
|
7371
|
-
" session load-workout [--athlete-id <athleteId>] --workout-id <id> --version <n>",
|
|
7372
|
-
" session complete <sessionId> [--status <status>] [--elapsed-sec <n>] [--completion-kind <kind>] [--started-at <iso>] [--ended-at <iso>] [--execution-mode <mode>] [--avg-power <n>] [--avg-hr-bpm <n>] [--max-hr-bpm <n>] [--compliance-pct <n>] [--completed-duration-sec <n>] [--completed-tss <n>] [--note <text> ...]",
|
|
7373
|
-
" session complete <sessionId> --input-json <json> | --input-file <path>",
|
|
7374
7660
|
" session summary <sessionId>",
|
|
7375
7661
|
" session telemetry <sessionId>",
|
|
7376
7662
|
" session artifacts <sessionId>",
|
|
7377
7663
|
" session fit <sessionId>",
|
|
7378
7664
|
" session strava-sync <sessionId>",
|
|
7665
|
+
" strava routes [--limit <n>]",
|
|
7666
|
+
" strava starred-segments [--limit <n>]",
|
|
7667
|
+
" course import-strava-route --input-json <json> | --input-file <path>",
|
|
7668
|
+
" course import-strava-segment --input-json <json> | --input-file <path>",
|
|
7669
|
+
"",
|
|
7670
|
+
"Extended MCP commands (all accept --input-json or --input-file):",
|
|
7671
|
+
...toolRoutes.filter((route) => route.extended).map((route) => ` ${route.command}${route.positional ? ` <${route.positional}>` : ""}`),
|
|
7672
|
+
" research search <query> | research fetch <documentId>",
|
|
7673
|
+
" tool list | tool schema <MCP-name> | tool <MCP-name> [--input-json <json>]",
|
|
7674
|
+
" Use tool schema for fields; flags use --kebab-case, nested --field.child, repeated arrays.",
|
|
7379
7675
|
"",
|
|
7380
|
-
"Authoring/
|
|
7676
|
+
"Authoring/library commands:",
|
|
7381
7677
|
" recommend daily [athleteId] --available-time-min <n> [--date <date>] [--prefer-objective <objective>] [--hard-constraint <text> ...]",
|
|
7382
7678
|
" workout draft [athleteId] --objective <objective> --duration-min <n> [--execution-mode <mode> ...] [--max-pct-ftp <n>] [--style-hint <text> ...] [--readiness-snapshot-id <id>]",
|
|
7383
7679
|
" workout draft --input-json <json> | --input-file <path>",
|
|
7384
7680
|
" workout revise --input-json <json> | --input-file <path>",
|
|
7385
7681
|
" workout validate --input-json <json> | --input-file <path>",
|
|
7386
7682
|
" workout save --input-json <json> | --input-file <path>",
|
|
7387
|
-
"
|
|
7388
|
-
"
|
|
7683
|
+
" workout schedule <workoutId> --date <YYYY-MM-DD>",
|
|
7684
|
+
" workout unschedule <workoutId>",
|
|
7685
|
+
" workout retire <workoutId>"
|
|
7389
7686
|
].join("\n");
|
|
7390
7687
|
}
|
|
7391
7688
|
function shiftRequired(args, label) {
|
|
@@ -7463,8 +7760,8 @@ function parseIntegerOption(raw, label) {
|
|
|
7463
7760
|
if (raw === void 0) {
|
|
7464
7761
|
return void 0;
|
|
7465
7762
|
}
|
|
7466
|
-
const parsed = Number
|
|
7467
|
-
if (!Number.
|
|
7763
|
+
const parsed = Number(raw);
|
|
7764
|
+
if (raw.trim() === "" || !Number.isInteger(parsed)) {
|
|
7468
7765
|
throw new Error(`Invalid integer for ${label}: ${raw}.`);
|
|
7469
7766
|
}
|
|
7470
7767
|
return parsed;
|
|
@@ -7496,7 +7793,7 @@ async function readJsonInput(args, jsonFlag = "--input-json", fileFlag = "--inpu
|
|
|
7496
7793
|
return parseJsonValue(inlineJson, jsonFlag);
|
|
7497
7794
|
}
|
|
7498
7795
|
if (filePath !== void 0) {
|
|
7499
|
-
const raw = await
|
|
7796
|
+
const raw = await readFile3(filePath, "utf8");
|
|
7500
7797
|
return parseJsonValue(raw, fileFlag);
|
|
7501
7798
|
}
|
|
7502
7799
|
return void 0;
|
|
@@ -7671,24 +7968,6 @@ function summarizeWorkoutRuntimePlan(result) {
|
|
|
7671
7968
|
`validation: ${result.validation.status}`
|
|
7672
7969
|
].join("\n");
|
|
7673
7970
|
}
|
|
7674
|
-
function summarizeQueuedSession(result) {
|
|
7675
|
-
return [
|
|
7676
|
-
"session queued",
|
|
7677
|
-
`sessionId: ${result.sessionId}`,
|
|
7678
|
-
`status: ${result.status}`,
|
|
7679
|
-
...result.runtimePlanId ? [`runtimePlanId: ${result.runtimePlanId}`] : []
|
|
7680
|
-
].join("\n");
|
|
7681
|
-
}
|
|
7682
|
-
function summarizeCompletedSession(result) {
|
|
7683
|
-
return [
|
|
7684
|
-
"session completed",
|
|
7685
|
-
`sessionId: ${result.session.id}`,
|
|
7686
|
-
`status: ${result.session.status}`,
|
|
7687
|
-
...result.session.completionKind ? [`completionKind: ${result.session.completionKind}`] : [],
|
|
7688
|
-
...result.session.actualMetrics?.completedDurationSec === void 0 ? [] : [`completedDurationSec: ${result.session.actualMetrics.completedDurationSec}`],
|
|
7689
|
-
...result.session.actualMetrics?.compliancePct === void 0 ? [] : [`compliancePct: ${result.session.actualMetrics.compliancePct}`]
|
|
7690
|
-
].join("\n");
|
|
7691
|
-
}
|
|
7692
7971
|
function summarizeStravaSync(result) {
|
|
7693
7972
|
const strava = result.session.externalSync?.strava;
|
|
7694
7973
|
return [
|
|
@@ -7710,15 +7989,6 @@ function summarizeRecommendation(result) {
|
|
|
7710
7989
|
...alternatives.length === 0 ? [] : ["alternatives:", ...alternatives.map((alt) => `- ${alt.objective} (${alt.durationMin} min)`)]
|
|
7711
7990
|
].join("\n");
|
|
7712
7991
|
}
|
|
7713
|
-
function summarizeAdaptedSession(result) {
|
|
7714
|
-
const adaptations = result.session.adaptations ?? [];
|
|
7715
|
-
return [
|
|
7716
|
-
"session adapted",
|
|
7717
|
-
`sessionId: ${result.session.id}`,
|
|
7718
|
-
`status: ${result.session.status}`,
|
|
7719
|
-
`adaptations: ${adaptations.length}`
|
|
7720
|
-
].join("\n");
|
|
7721
|
-
}
|
|
7722
7992
|
function summarizeAuthStatus(state) {
|
|
7723
7993
|
return [
|
|
7724
7994
|
"auth status",
|
|
@@ -7736,6 +8006,14 @@ function formatDefaultResult(commandKey, result) {
|
|
|
7736
8006
|
return summarizeRecentSessions(result);
|
|
7737
8007
|
case "recommend_daily":
|
|
7738
8008
|
return summarizeRecommendation(result);
|
|
8009
|
+
case "workout_list": {
|
|
8010
|
+
const workouts = result.workouts;
|
|
8011
|
+
return [
|
|
8012
|
+
"saved workouts",
|
|
8013
|
+
`count: ${workouts.length}`,
|
|
8014
|
+
...workouts.map(({ workout }) => `- ${workout.id} | v${workout.version} | ${workout.title} | ${workout.status} | ${workout.scheduledDate ?? "unscheduled"}`)
|
|
8015
|
+
].join("\n");
|
|
8016
|
+
}
|
|
7739
8017
|
case "workout_get":
|
|
7740
8018
|
return summarizeSavedWorkout(result);
|
|
7741
8019
|
case "workout_draft":
|
|
@@ -7748,10 +8026,6 @@ function formatDefaultResult(commandKey, result) {
|
|
|
7748
8026
|
return summarizeWorkoutRevisions(result);
|
|
7749
8027
|
case "workout_runtime_plan":
|
|
7750
8028
|
return summarizeWorkoutRuntimePlan(result);
|
|
7751
|
-
case "session_load_workout":
|
|
7752
|
-
return summarizeQueuedSession(result);
|
|
7753
|
-
case "session_complete":
|
|
7754
|
-
return summarizeCompletedSession(result);
|
|
7755
8029
|
case "session_summary":
|
|
7756
8030
|
return summarizeSessionSummary(result);
|
|
7757
8031
|
case "session_telemetry":
|
|
@@ -7762,8 +8036,6 @@ function formatDefaultResult(commandKey, result) {
|
|
|
7762
8036
|
return summarizeFit(result);
|
|
7763
8037
|
case "session_strava_sync":
|
|
7764
8038
|
return summarizeStravaSync(result);
|
|
7765
|
-
case "session_adapt":
|
|
7766
|
-
return summarizeAdaptedSession(result);
|
|
7767
8039
|
default:
|
|
7768
8040
|
return JSON.stringify(result, null, 2);
|
|
7769
8041
|
}
|
|
@@ -7888,6 +8160,8 @@ async function runAuthCommand(context) {
|
|
|
7888
8160
|
throw new Error(`Unsupported auth subcommand: ${subcommand}.`);
|
|
7889
8161
|
}
|
|
7890
8162
|
async function runCommand(context) {
|
|
8163
|
+
const extended = await runToolCommand(context.args, context.client, context.authState !== void 0, context.stravaLimitedAccess ?? false);
|
|
8164
|
+
if (extended) return writeResult(context, "tool", extended.result);
|
|
7891
8165
|
const top = shiftRequired(context.args, "command");
|
|
7892
8166
|
if (top === "health") {
|
|
7893
8167
|
return writeResult(context, "health", await context.client.health());
|
|
@@ -7957,52 +8231,28 @@ async function runCommand(context) {
|
|
|
7957
8231
|
context: contextPayload
|
|
7958
8232
|
}));
|
|
7959
8233
|
}
|
|
7960
|
-
if (top === "
|
|
7961
|
-
const subcommand = shiftRequired(context.args, "
|
|
7962
|
-
|
|
7963
|
-
|
|
7964
|
-
|
|
8234
|
+
if (top === "strava") {
|
|
8235
|
+
const subcommand = shiftRequired(context.args, "strava subcommand");
|
|
8236
|
+
requireSignedInAuth(context);
|
|
8237
|
+
if (subcommand === "routes") {
|
|
8238
|
+
const limit = parseIntegerOption(takeOption(context.args, "--limit"), "--limit");
|
|
8239
|
+
return writeResult(context, "strava_routes", await context.client.listMyStravaRoutes({ ...limit === void 0 ? {} : { limit } }));
|
|
7965
8240
|
}
|
|
7966
|
-
if (subcommand === "
|
|
7967
|
-
const
|
|
7968
|
-
|
|
7969
|
-
const workoutVersion = parseIntegerOption(takeOption(context.args, "--version"), "--version");
|
|
7970
|
-
const date = takeOption(context.args, "--date");
|
|
7971
|
-
if (!workoutId) {
|
|
7972
|
-
throw new Error("Missing required argument: --workout-id.");
|
|
7973
|
-
}
|
|
7974
|
-
if (workoutVersion === void 0) {
|
|
7975
|
-
throw new Error("Missing required argument: --version.");
|
|
7976
|
-
}
|
|
7977
|
-
if (!date) {
|
|
7978
|
-
throw new Error("Missing required argument: --date.");
|
|
7979
|
-
}
|
|
7980
|
-
return target.useMyRoutes ? writeResult(context, "plan_schedule", await context.client.scheduleMyWorkout({
|
|
7981
|
-
workoutId,
|
|
7982
|
-
workoutVersion,
|
|
7983
|
-
date
|
|
7984
|
-
})) : writeResult(context, "plan_schedule", await context.client.scheduleWorkout({
|
|
7985
|
-
athleteId: target.athleteId,
|
|
7986
|
-
workoutId,
|
|
7987
|
-
workoutVersion,
|
|
7988
|
-
date
|
|
7989
|
-
}));
|
|
8241
|
+
if (subcommand === "starred-segments") {
|
|
8242
|
+
const limit = parseIntegerOption(takeOption(context.args, "--limit"), "--limit");
|
|
8243
|
+
return writeResult(context, "strava_starred", await context.client.listMyStravaStarredSegments({ ...limit === void 0 ? {} : { limit } }));
|
|
7990
8244
|
}
|
|
7991
|
-
throw new Error(`Unsupported
|
|
8245
|
+
throw new Error(`Unsupported strava subcommand: ${subcommand}.`);
|
|
7992
8246
|
}
|
|
7993
|
-
if (top === "
|
|
7994
|
-
const subcommand = shiftRequired(context.args, "
|
|
7995
|
-
|
|
7996
|
-
|
|
8247
|
+
if (top === "course") {
|
|
8248
|
+
const subcommand = shiftRequired(context.args, "course subcommand");
|
|
8249
|
+
requireSignedInAuth(context);
|
|
8250
|
+
if (subcommand === "import-strava-route" || subcommand === "import-strava-segment") {
|
|
8251
|
+
const input = await readJsonInput(context.args);
|
|
8252
|
+
if (input === void 0) throw new Error(`course ${subcommand} requires --input-json or --input-file.`);
|
|
8253
|
+
return subcommand === "import-strava-route" ? writeResult(context, "course_import", await context.client.importMyStravaRoute(input)) : writeResult(context, "course_import", await context.client.importMyStravaSegment(input));
|
|
7997
8254
|
}
|
|
7998
|
-
|
|
7999
|
-
const date = takeOption(context.args, "--date");
|
|
8000
|
-
return target.useMyRoutes ? writeResult(context, "runtime_plan_today", await context.client.getMyTodayRuntimePlan({
|
|
8001
|
-
...date === void 0 ? {} : { date }
|
|
8002
|
-
})) : writeResult(context, "runtime_plan_today", await context.client.getTodayRuntimePlan({
|
|
8003
|
-
athleteId: target.athleteId,
|
|
8004
|
-
...date === void 0 ? {} : { date }
|
|
8005
|
-
}));
|
|
8255
|
+
throw new Error(`Unsupported course subcommand: ${subcommand}.`);
|
|
8006
8256
|
}
|
|
8007
8257
|
if (top === "workout") {
|
|
8008
8258
|
const subcommand = shiftRequired(context.args, "workout subcommand");
|
|
@@ -8093,7 +8343,22 @@ async function runCommand(context) {
|
|
|
8093
8343
|
structuredInput
|
|
8094
8344
|
));
|
|
8095
8345
|
}
|
|
8346
|
+
if (subcommand === "list") {
|
|
8347
|
+
const limit = parseIntegerOption(takeOption(context.args, "--limit"), "--limit");
|
|
8348
|
+
const target = resolveAthleteTarget(context);
|
|
8349
|
+
return target.useMyRoutes ? writeResult(context, "workout_list", await context.client.listMySavedWorkouts({ ...limit === void 0 ? {} : { limit } })) : writeResult(context, "workout_list", await context.client.listSavedWorkouts({ athleteId: target.athleteId, ...limit === void 0 ? {} : { limit } }));
|
|
8350
|
+
}
|
|
8096
8351
|
const workoutId = shiftRequired(context.args, "workoutId");
|
|
8352
|
+
if (subcommand === "schedule" || subcommand === "unschedule") {
|
|
8353
|
+
const date = takeOption(context.args, "--date");
|
|
8354
|
+
if (subcommand === "schedule" && !date) throw new Error("Missing required argument: --date.");
|
|
8355
|
+
if (subcommand === "unschedule" && date !== void 0) throw new Error("workout unschedule does not accept --date.");
|
|
8356
|
+
const input = { workoutId, scheduledDate: subcommand === "schedule" ? date : null };
|
|
8357
|
+
return context.authState ? writeResult(context, "workout_schedule", await context.client.scheduleMySavedWorkout(input)) : writeResult(context, "workout_schedule", await context.client.scheduleSavedWorkout(input));
|
|
8358
|
+
}
|
|
8359
|
+
if (subcommand === "retire") {
|
|
8360
|
+
return context.authState ? writeResult(context, "workout_retire", await context.client.retireMySavedWorkout({ workoutId })) : writeResult(context, "workout_retire", await context.client.retireSavedWorkout({ workoutId }));
|
|
8361
|
+
}
|
|
8097
8362
|
if (subcommand === "get") {
|
|
8098
8363
|
const workoutVersion = parseIntegerOption(takeOption(context.args, "--version"), "--version");
|
|
8099
8364
|
return context.authState ? writeResult(context, "workout_get", await context.client.getMySavedWorkout({
|
|
@@ -8124,99 +8389,7 @@ async function runCommand(context) {
|
|
|
8124
8389
|
}
|
|
8125
8390
|
if (top === "session") {
|
|
8126
8391
|
const subcommand = shiftRequired(context.args, "session subcommand");
|
|
8127
|
-
if (subcommand === "load-workout") {
|
|
8128
|
-
const athleteId = takeOption(context.args, "--athlete-id");
|
|
8129
|
-
const workoutId = takeOption(context.args, "--workout-id");
|
|
8130
|
-
const workoutVersion = parseIntegerOption(takeOption(context.args, "--version"), "--version");
|
|
8131
|
-
if (!workoutId) {
|
|
8132
|
-
throw new Error("Missing required argument: --workout-id.");
|
|
8133
|
-
}
|
|
8134
|
-
if (workoutVersion === void 0) {
|
|
8135
|
-
throw new Error("Missing required argument: --version.");
|
|
8136
|
-
}
|
|
8137
|
-
if (!athleteId && context.authState) {
|
|
8138
|
-
return writeResult(context, "session_load_workout", await context.client.sendMyWorkoutToActiveTrainerSession({
|
|
8139
|
-
workoutId,
|
|
8140
|
-
workoutVersion
|
|
8141
|
-
}));
|
|
8142
|
-
}
|
|
8143
|
-
if (!athleteId) {
|
|
8144
|
-
throw new Error("Missing required argument: --athlete-id.");
|
|
8145
|
-
}
|
|
8146
|
-
return writeResult(context, "session_load_workout", await context.client.sendToActiveTrainerSession({
|
|
8147
|
-
athleteId,
|
|
8148
|
-
workoutId,
|
|
8149
|
-
workoutVersion
|
|
8150
|
-
}));
|
|
8151
|
-
}
|
|
8152
8392
|
const sessionId = shiftRequired(context.args, "sessionId");
|
|
8153
|
-
if (subcommand === "complete") {
|
|
8154
|
-
const structuredInput = await readJsonInput(context.args);
|
|
8155
|
-
if (structuredInput !== void 0) {
|
|
8156
|
-
return context.authState ? writeResult(context, "session_complete", await context.client.ingestMyCompletedSession({
|
|
8157
|
-
sessionId,
|
|
8158
|
-
...structuredInput
|
|
8159
|
-
})) : writeResult(context, "session_complete", await context.client.ingestCompletedSession({
|
|
8160
|
-
sessionId,
|
|
8161
|
-
...structuredInput
|
|
8162
|
-
}));
|
|
8163
|
-
}
|
|
8164
|
-
const status = takeOption(context.args, "--status");
|
|
8165
|
-
const completionKind = takeOption(context.args, "--completion-kind");
|
|
8166
|
-
const startedAt = takeOption(context.args, "--started-at");
|
|
8167
|
-
const endedAt = takeOption(context.args, "--ended-at");
|
|
8168
|
-
const executionMode = takeOption(context.args, "--execution-mode");
|
|
8169
|
-
const elapsedSec = parseIntegerOption(takeOption(context.args, "--elapsed-sec"), "--elapsed-sec");
|
|
8170
|
-
const avgPower = parseNumberOption(takeOption(context.args, "--avg-power"), "--avg-power");
|
|
8171
|
-
const avgHrBpm = parseNumberOption(takeOption(context.args, "--avg-hr-bpm"), "--avg-hr-bpm");
|
|
8172
|
-
const maxHrBpm = parseNumberOption(takeOption(context.args, "--max-hr-bpm"), "--max-hr-bpm");
|
|
8173
|
-
const compliancePct = parseNumberOption(takeOption(context.args, "--compliance-pct"), "--compliance-pct");
|
|
8174
|
-
const completedDurationSec = parseNumberOption(takeOption(context.args, "--completed-duration-sec"), "--completed-duration-sec");
|
|
8175
|
-
const completedTss = parseNumberOption(takeOption(context.args, "--completed-tss"), "--completed-tss");
|
|
8176
|
-
const notes = takeOptions(context.args, "--note");
|
|
8177
|
-
if (!status) {
|
|
8178
|
-
throw new Error("session complete requires either --input-json/--input-file or at least --status.");
|
|
8179
|
-
}
|
|
8180
|
-
const completion = {
|
|
8181
|
-
sessionId,
|
|
8182
|
-
status,
|
|
8183
|
-
...completionKind === void 0 ? {} : { completionKind },
|
|
8184
|
-
...startedAt === void 0 ? {} : { startedAt },
|
|
8185
|
-
...endedAt === void 0 ? {} : { endedAt },
|
|
8186
|
-
...executionMode === void 0 ? {} : { executionMode },
|
|
8187
|
-
...elapsedSec === void 0 ? {} : { liveSummary: { elapsedSec } },
|
|
8188
|
-
...avgPower === void 0 && avgHrBpm === void 0 && maxHrBpm === void 0 && compliancePct === void 0 && completedDurationSec === void 0 && completedTss === void 0 ? {} : {
|
|
8189
|
-
actualMetrics: {
|
|
8190
|
-
...avgPower === void 0 ? {} : { avgPower },
|
|
8191
|
-
...avgHrBpm === void 0 ? {} : { avgHrBpm },
|
|
8192
|
-
...maxHrBpm === void 0 ? {} : { maxHrBpm },
|
|
8193
|
-
...compliancePct === void 0 ? {} : { compliancePct },
|
|
8194
|
-
...completedDurationSec === void 0 ? {} : { completedDurationSec },
|
|
8195
|
-
...completedTss === void 0 ? {} : { completedTss }
|
|
8196
|
-
}
|
|
8197
|
-
},
|
|
8198
|
-
...notes.length === 0 ? {} : { notes }
|
|
8199
|
-
};
|
|
8200
|
-
return context.authState ? writeResult(context, "session_complete", await context.client.ingestMyCompletedSession(completion)) : writeResult(context, "session_complete", await context.client.ingestCompletedSession(completion));
|
|
8201
|
-
}
|
|
8202
|
-
if (subcommand === "adapt") {
|
|
8203
|
-
const op = takeOption(context.args, "--op");
|
|
8204
|
-
const reason = takeOption(context.args, "--reason");
|
|
8205
|
-
const payload = await readJsonInput(context.args, "--payload-json", "--payload-file");
|
|
8206
|
-
if (!op) {
|
|
8207
|
-
throw new Error("Missing required argument: --op.");
|
|
8208
|
-
}
|
|
8209
|
-
if (!reason) {
|
|
8210
|
-
throw new Error("Missing required argument: --reason.");
|
|
8211
|
-
}
|
|
8212
|
-
const adaptation = {
|
|
8213
|
-
sessionId,
|
|
8214
|
-
op,
|
|
8215
|
-
reason,
|
|
8216
|
-
...payload === void 0 ? {} : { payload }
|
|
8217
|
-
};
|
|
8218
|
-
return context.authState ? writeResult(context, "session_adapt", await context.client.adaptMyActiveSession(adaptation)) : writeResult(context, "session_adapt", await context.client.adaptActiveSession(adaptation));
|
|
8219
|
-
}
|
|
8220
8393
|
if (subcommand === "summary") {
|
|
8221
8394
|
return context.authState ? writeResult(context, "session_summary", await context.client.summarizeMySession({ sessionId })) : writeResult(context, "session_summary", await context.client.summarizeSession({ sessionId }));
|
|
8222
8395
|
}
|
|
@@ -8284,7 +8457,8 @@ async function runPlannerCli(options) {
|
|
|
8284
8457
|
stdout,
|
|
8285
8458
|
stderr,
|
|
8286
8459
|
client,
|
|
8287
|
-
authState: await authController.loadState()
|
|
8460
|
+
authState: await authController.loadState(),
|
|
8461
|
+
stravaLimitedAccess: ["1", "true", "yes", "on"].includes((env.WATTSON_STRAVA_LIMITED_ACCESS ?? env.PLANNER_MCP_STRAVA_LIMITED_ACCESS ?? "").trim().toLowerCase())
|
|
8288
8462
|
});
|
|
8289
8463
|
} catch (error) {
|
|
8290
8464
|
writeError(stderr, error);
|