incremnt 0.1.13 → 0.1.16
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 +11 -9
- package/package.json +3 -2
- package/src/contract.js +25 -0
- package/src/lib.js +57 -1
- package/src/mcp.js +57 -30
- package/src/openrouter.js +206 -119
- package/src/queries.js +293 -10
- package/src/remote.js +39 -1
- package/src/sync-service.js +259 -18
package/src/queries.js
CHANGED
|
@@ -58,7 +58,8 @@ function sessionSummary(session) {
|
|
|
58
58
|
recommendations: session.recommendations ?? {},
|
|
59
59
|
historicalContext: session.historicalContext ?? null,
|
|
60
60
|
prescriptionSnapshot: session.prescriptionSnapshot ?? null,
|
|
61
|
-
aiCoachNotes: session.aiCoachNotes ?? null
|
|
61
|
+
aiCoachNotes: session.summary?.aiCoachNotes ?? null,
|
|
62
|
+
aiCoachModel: session.summary?.aiCoachModel ?? null
|
|
62
63
|
};
|
|
63
64
|
}
|
|
64
65
|
|
|
@@ -648,6 +649,39 @@ export function cycleSummaryContext(snapshot, programId) {
|
|
|
648
649
|
? { completed: matchingSummary.totalSetsCompleted ?? 0, planned: matchingSummary.totalSetsPlanned ?? 0 }
|
|
649
650
|
: null;
|
|
650
651
|
|
|
652
|
+
// Health metrics spanning the cycle
|
|
653
|
+
const cycleStart = String(cycleSessions[0] ? completionDateForSession(cycleSessions[0]) : '');
|
|
654
|
+
const cycleEnd = String(cycleSessions[cycleSessions.length - 1]
|
|
655
|
+
? completionDateForSession(cycleSessions[cycleSessions.length - 1]) : '');
|
|
656
|
+
const cycleCardio = (snapshot.healthMetrics?.otherWorkouts ?? [])
|
|
657
|
+
.filter((w) => w.date >= cycleStart && w.date <= cycleEnd);
|
|
658
|
+
const cycleRestingHR = (snapshot.healthMetrics?.restingHR ?? [])
|
|
659
|
+
.filter((m) => m.date >= cycleStart && m.date <= cycleEnd);
|
|
660
|
+
const cycleHRV = (snapshot.healthMetrics?.hrv ?? [])
|
|
661
|
+
.filter((m) => m.date >= cycleStart && m.date <= cycleEnd);
|
|
662
|
+
const cycleVO2Max = (snapshot.healthMetrics?.vo2Max ?? [])
|
|
663
|
+
.filter((m) => m.date >= cycleStart && m.date <= cycleEnd);
|
|
664
|
+
const cycleSleep = (snapshot.healthMetrics?.sleep ?? [])
|
|
665
|
+
.filter((m) => m.date >= cycleStart && m.date <= cycleEnd);
|
|
666
|
+
|
|
667
|
+
const avgRestingHR = cycleRestingHR.length > 0
|
|
668
|
+
? Math.round(cycleRestingHR.reduce((s, m) => s + m.value, 0) / cycleRestingHR.length)
|
|
669
|
+
: null;
|
|
670
|
+
const avgHRV = cycleHRV.length > 0
|
|
671
|
+
? Math.round(cycleHRV.reduce((s, m) => s + m.value, 0) / cycleHRV.length)
|
|
672
|
+
: null;
|
|
673
|
+
const latestVO2Max = cycleVO2Max.length > 0
|
|
674
|
+
? Math.round(cycleVO2Max.at(-1).value * 10) / 10
|
|
675
|
+
: null;
|
|
676
|
+
const avgSleepMins = cycleSleep.length > 0
|
|
677
|
+
? Math.round(cycleSleep.reduce((s, m) => s + m.durationMins, 0) / cycleSleep.length)
|
|
678
|
+
: null;
|
|
679
|
+
const cycleBodyWeight = (snapshot.healthMetrics?.bodyWeight ?? [])
|
|
680
|
+
.filter((m) => m.date >= cycleStart && m.date <= cycleEnd);
|
|
681
|
+
const latestBodyWeightKg = cycleBodyWeight.length > 0
|
|
682
|
+
? Math.round(cycleBodyWeight.at(-1).value * 10) / 10
|
|
683
|
+
: null;
|
|
684
|
+
|
|
651
685
|
return {
|
|
652
686
|
programName: program.name,
|
|
653
687
|
cycleNumber: cycleWeekNumber,
|
|
@@ -661,7 +695,13 @@ export function cycleSummaryContext(snapshot, programId) {
|
|
|
661
695
|
cycleIntent,
|
|
662
696
|
adaptationNote,
|
|
663
697
|
previousCycles,
|
|
664
|
-
exerciseTrends
|
|
698
|
+
exerciseTrends,
|
|
699
|
+
cycleCardio: cycleCardio.length > 0 ? cycleCardio : null,
|
|
700
|
+
avgRestingHR,
|
|
701
|
+
avgHRV,
|
|
702
|
+
latestVO2Max,
|
|
703
|
+
avgSleepMins,
|
|
704
|
+
latestBodyWeightKg
|
|
665
705
|
};
|
|
666
706
|
}
|
|
667
707
|
|
|
@@ -692,24 +732,26 @@ export function workoutSummaryContext(snapshot, sessionId) {
|
|
|
692
732
|
};
|
|
693
733
|
});
|
|
694
734
|
|
|
695
|
-
// Find recent sessions with same dayName for comparison (up to 3, excluding current)
|
|
735
|
+
// Find recent sessions with same dayName for comparison (up to 3, excluding current).
|
|
736
|
+
// Match across programs so context survives program switches.
|
|
696
737
|
const recentComparisons = sessions
|
|
697
738
|
.filter(
|
|
698
739
|
(s) =>
|
|
699
740
|
s.id !== sessionId &&
|
|
700
|
-
s.dayName === dayName
|
|
701
|
-
s.programId === session.programId
|
|
741
|
+
s.dayName === dayName
|
|
702
742
|
)
|
|
703
743
|
.sort((a, b) => String(completionDateForSession(b)).localeCompare(String(completionDateForSession(a))))
|
|
704
744
|
.slice(0, 3)
|
|
705
745
|
.map((s) => ({
|
|
706
746
|
date: completionDateForSession(s),
|
|
707
747
|
totalVolume: s.summary?.totalVolume ?? s.volume ?? 0,
|
|
708
|
-
effortScore: s.summary?.effortScore ?? null
|
|
748
|
+
effortScore: s.summary?.effortScore ?? null,
|
|
749
|
+
programName: s.programName ?? null
|
|
709
750
|
}));
|
|
710
751
|
|
|
711
|
-
//
|
|
752
|
+
// Count prior sessions per exercise and track best e1RM scores
|
|
712
753
|
const priorBests = new Map();
|
|
754
|
+
const exerciseSessionCounts = new Map();
|
|
713
755
|
for (const s of sessions) {
|
|
714
756
|
if (s.id === sessionId) continue;
|
|
715
757
|
const sDate = String(completionDateForSession(s));
|
|
@@ -717,9 +759,10 @@ export function workoutSummaryContext(snapshot, sessionId) {
|
|
|
717
759
|
if (sDate >= currentDate) continue;
|
|
718
760
|
|
|
719
761
|
for (const exercise of s.exercises ?? []) {
|
|
762
|
+
const key = normalizeExerciseName(exercise.name);
|
|
763
|
+
exerciseSessionCounts.set(key, (exerciseSessionCounts.get(key) ?? 0) + 1);
|
|
720
764
|
for (const set of exercise.sets ?? []) {
|
|
721
765
|
if (!set.isComplete) continue;
|
|
722
|
-
const key = normalizeExerciseName(exercise.name);
|
|
723
766
|
const score = Number(set.weight) * (1 + Number(set.reps) / 30);
|
|
724
767
|
const current = priorBests.get(key);
|
|
725
768
|
if (!current || score > current) priorBests.set(key, score);
|
|
@@ -727,14 +770,22 @@ export function workoutSummaryContext(snapshot, sessionId) {
|
|
|
727
770
|
}
|
|
728
771
|
}
|
|
729
772
|
|
|
773
|
+
// Attach prior session count to each exercise
|
|
774
|
+
for (const ex of exercises) {
|
|
775
|
+
const key = normalizeExerciseName(ex.exerciseName);
|
|
776
|
+
ex.priorSessions = exerciseSessionCounts.get(key) ?? 0;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
// Detect PRs — skip first-time exercises (every set is trivially a "PR")
|
|
730
780
|
const prs = [];
|
|
731
781
|
for (const exercise of session.exercises ?? []) {
|
|
732
782
|
const key = normalizeExerciseName(exercise.name);
|
|
783
|
+
if (!priorBests.has(key)) continue;
|
|
733
784
|
for (const set of exercise.sets ?? []) {
|
|
734
785
|
if (!set.isComplete) continue;
|
|
735
786
|
const score = Number(set.weight) * (1 + Number(set.reps) / 30);
|
|
736
787
|
const prior = priorBests.get(key);
|
|
737
|
-
if (
|
|
788
|
+
if (score > prior) {
|
|
738
789
|
prs.push({
|
|
739
790
|
exerciseName: exercise.name,
|
|
740
791
|
weight: set.weight,
|
|
@@ -760,19 +811,246 @@ export function workoutSummaryContext(snapshot, sessionId) {
|
|
|
760
811
|
}
|
|
761
812
|
}
|
|
762
813
|
|
|
814
|
+
const isAdhoc = !session.programId;
|
|
815
|
+
|
|
816
|
+
// Include other workouts from the 7 days before this session for coach context
|
|
817
|
+
const sessionDateStr = String(sessionDate);
|
|
818
|
+
const weekBefore = new Date(new Date(sessionDateStr).getTime() - 7 * 24 * 60 * 60 * 1000)
|
|
819
|
+
.toISOString().slice(0, 10);
|
|
820
|
+
const nearbyCardio = (snapshot.healthMetrics?.otherWorkouts ?? [])
|
|
821
|
+
.filter((w) => w.date >= weekBefore && w.date <= sessionDateStr);
|
|
822
|
+
|
|
823
|
+
const restingHROnDay = (snapshot.healthMetrics?.restingHR ?? [])
|
|
824
|
+
.find((m) => m.date === sessionDateStr);
|
|
825
|
+
const hrvOnDay = (snapshot.healthMetrics?.hrv ?? [])
|
|
826
|
+
.find((m) => m.date === sessionDateStr);
|
|
827
|
+
const sleepNight = (snapshot.healthMetrics?.sleep ?? [])
|
|
828
|
+
.find((m) => m.date === sessionDateStr);
|
|
829
|
+
const vo2MaxRecent = (snapshot.healthMetrics?.vo2Max ?? [])
|
|
830
|
+
.filter((m) => m.date >= weekBefore && m.date <= sessionDateStr);
|
|
831
|
+
const vo2MaxLatest = vo2MaxRecent.length > 0
|
|
832
|
+
? Math.round(vo2MaxRecent.at(-1).value * 10) / 10
|
|
833
|
+
: null;
|
|
834
|
+
const bodyWeightOnDay = (snapshot.healthMetrics?.bodyWeight ?? [])
|
|
835
|
+
.find((m) => m.date === sessionDateStr);
|
|
836
|
+
const bodyWeightKg = bodyWeightOnDay
|
|
837
|
+
? Math.round(bodyWeightOnDay.value * 10) / 10
|
|
838
|
+
: null;
|
|
839
|
+
|
|
763
840
|
const result = {
|
|
764
841
|
sessionDate,
|
|
765
842
|
dayName,
|
|
843
|
+
programName: session.programName ?? null,
|
|
844
|
+
isAdhoc,
|
|
766
845
|
totalVolume: session.summary?.totalVolume ?? session.volume ?? 0,
|
|
767
846
|
effortScore: session.summary?.effortScore ?? null,
|
|
768
847
|
exercises,
|
|
769
848
|
recentComparisons,
|
|
770
|
-
prs
|
|
849
|
+
prs,
|
|
850
|
+
nearbyCardio: nearbyCardio.length > 0 ? nearbyCardio : null,
|
|
851
|
+
restingHROnDay: restingHROnDay?.value ?? null,
|
|
852
|
+
hrvOnDay: hrvOnDay?.value ?? null,
|
|
853
|
+
vo2MaxLatest,
|
|
854
|
+
sleepNight: sleepNight ?? null,
|
|
855
|
+
bodyWeightKg
|
|
771
856
|
};
|
|
772
857
|
if (planComparison) result.planComparison = planComparison;
|
|
773
858
|
return result;
|
|
774
859
|
}
|
|
775
860
|
|
|
861
|
+
export function askContext(snapshot) {
|
|
862
|
+
const sessions = snapshot.sessions ?? [];
|
|
863
|
+
const lines = [];
|
|
864
|
+
|
|
865
|
+
lines.push(`Training overview: ${sessions.length} total workouts logged.`);
|
|
866
|
+
|
|
867
|
+
// Training frequency (last 4 weeks)
|
|
868
|
+
const fourWeeksAgo = new Date(Date.now() - 28 * 24 * 60 * 60 * 1000).toISOString();
|
|
869
|
+
const recentCount = sessions.filter((s) => String(completionDateForSession(s)) >= fourWeeksAgo).length;
|
|
870
|
+
if (recentCount > 0) {
|
|
871
|
+
const perWeek = (recentCount / 4).toFixed(1);
|
|
872
|
+
lines.push(`Recent frequency: ${perWeek} sessions/week (last 4 weeks).`);
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
// Current program
|
|
876
|
+
const program = activeProgram(snapshot);
|
|
877
|
+
if (program) {
|
|
878
|
+
lines.push(`Current program: ${program.name}, ${program.daysPerWeek} days/week, equipment: ${program.equipmentTier ?? 'unknown'}.`);
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
// Best e1RM records (top 15)
|
|
882
|
+
const bestByExercise = new Map();
|
|
883
|
+
for (const session of sessions) {
|
|
884
|
+
for (const exercise of session.exercises ?? []) {
|
|
885
|
+
const key = normalizeExerciseName(exercise.name);
|
|
886
|
+
for (const set of exercise.sets ?? []) {
|
|
887
|
+
if (!set.isComplete) continue;
|
|
888
|
+
const e1rm = Number(set.weight) * (1 + Number(set.reps) / 30);
|
|
889
|
+
const current = bestByExercise.get(key);
|
|
890
|
+
if (!current || e1rm > current.e1rm) {
|
|
891
|
+
bestByExercise.set(key, { name: exercise.name, e1rm });
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
const records = [...bestByExercise.values()]
|
|
898
|
+
.filter((r) => r.e1rm > 0)
|
|
899
|
+
.sort((a, b) => b.e1rm - a.e1rm)
|
|
900
|
+
.slice(0, 15);
|
|
901
|
+
|
|
902
|
+
if (records.length > 0) {
|
|
903
|
+
lines.push('');
|
|
904
|
+
lines.push('Best estimated 1RM records:');
|
|
905
|
+
for (const r of records) {
|
|
906
|
+
lines.push(` ${r.name}: ${r.e1rm.toFixed(1)} kg`);
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
// Recent sessions (last 10)
|
|
911
|
+
const recentSessions = sessions.slice(-10);
|
|
912
|
+
if (recentSessions.length > 0) {
|
|
913
|
+
lines.push('');
|
|
914
|
+
lines.push('Recent sessions (newest last):');
|
|
915
|
+
for (const session of recentSessions) {
|
|
916
|
+
const dateStr = completionDateForSession(session);
|
|
917
|
+
const dayLabel = session.dayName ?? session.programName ?? 'Workout';
|
|
918
|
+
const exerciseNames = (session.exercises ?? []).map((e) => e.name).join(', ');
|
|
919
|
+
const volume = session.summary?.totalVolume ?? session.volume ?? 0;
|
|
920
|
+
let line = ` ${dateStr} - ${dayLabel}: ${exerciseNames}`;
|
|
921
|
+
if (volume > 0) line += ` (${volume} kg volume)`;
|
|
922
|
+
lines.push(line);
|
|
923
|
+
|
|
924
|
+
for (const exercise of session.exercises ?? []) {
|
|
925
|
+
const completedSets = (exercise.sets ?? []).filter((s) => s.isComplete);
|
|
926
|
+
if (completedSets.length === 0) continue;
|
|
927
|
+
const topSet = completedSets.reduce((best, s) => {
|
|
928
|
+
const score = Number(s.weight) * Number(s.reps);
|
|
929
|
+
const bestScore = Number(best.weight) * Number(best.reps);
|
|
930
|
+
return score > bestScore ? s : best;
|
|
931
|
+
});
|
|
932
|
+
lines.push(` ${exercise.name}: ${completedSets.length} sets, top ${Number(topSet.weight).toFixed(1)}x${topSet.reps}`);
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
appendHealthMetricsContext(lines, snapshot.healthMetrics, { recentDays: 14 });
|
|
938
|
+
|
|
939
|
+
return lines.join('\n');
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
function appendHealthMetricsContext(lines, metrics, { recentDays = 14 } = {}) {
|
|
943
|
+
if (!metrics) return;
|
|
944
|
+
|
|
945
|
+
const cutoff = new Date(Date.now() - recentDays * 24 * 60 * 60 * 1000).toISOString().slice(0, 10);
|
|
946
|
+
|
|
947
|
+
const recentWorkouts = (metrics.otherWorkouts ?? []).filter((w) => w.date >= cutoff);
|
|
948
|
+
if (recentWorkouts.length > 0) {
|
|
949
|
+
lines.push('');
|
|
950
|
+
lines.push(`Other workouts (last ${recentDays} days):`);
|
|
951
|
+
for (const w of recentWorkouts) {
|
|
952
|
+
const parts = [`${w.durationSecs ? Math.round(w.durationSecs / 60) : '?'} min`];
|
|
953
|
+
if (w.distanceKm) parts.push(`${w.distanceKm.toFixed(1)} km`);
|
|
954
|
+
if (w.avgHR) parts.push(`avg HR ${w.avgHR} bpm`);
|
|
955
|
+
if (w.calories) parts.push(`${w.calories} kcal`);
|
|
956
|
+
if (w.effortScore) parts.push(`effort ${w.effortScore}/10`);
|
|
957
|
+
lines.push(` ${w.date} ${w.workoutType}: ${parts.join(', ')}`);
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
const recentRestingHR = (metrics.restingHR ?? []).filter((m) => m.date >= cutoff);
|
|
962
|
+
if (recentRestingHR.length > 0) {
|
|
963
|
+
const avg = Math.round(recentRestingHR.reduce((s, m) => s + m.value, 0) / recentRestingHR.length);
|
|
964
|
+
const latest = recentRestingHR[recentRestingHR.length - 1];
|
|
965
|
+
lines.push('');
|
|
966
|
+
lines.push(`Resting HR (last ${recentDays} days): avg ${avg} bpm, latest ${Math.round(latest.value)} bpm (${latest.date})`);
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
const recentHRV = (metrics.hrv ?? []).filter((m) => m.date >= cutoff);
|
|
970
|
+
if (recentHRV.length > 0) {
|
|
971
|
+
const avg = Math.round(recentHRV.reduce((s, m) => s + m.value, 0) / recentHRV.length);
|
|
972
|
+
const latest = recentHRV[recentHRV.length - 1];
|
|
973
|
+
lines.push(`HRV (last ${recentDays} days): avg ${avg} ms, latest ${Math.round(latest.value)} ms (${latest.date})`);
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
const recentVO2Max = (metrics.vo2Max ?? []).filter((m) => m.date >= cutoff);
|
|
977
|
+
if (recentVO2Max.length > 0) {
|
|
978
|
+
const latest = recentVO2Max[recentVO2Max.length - 1];
|
|
979
|
+
lines.push(`VO2 Max: ${Math.round(latest.value * 10) / 10} ml/kg/min (${latest.date})`);
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
const recentSleep = (metrics.sleep ?? []).filter((m) => m.date >= cutoff);
|
|
983
|
+
if (recentSleep.length > 0) {
|
|
984
|
+
const avgMins = Math.round(recentSleep.reduce((s, m) => s + m.durationMins, 0) / recentSleep.length);
|
|
985
|
+
const avgHours = (avgMins / 60).toFixed(1);
|
|
986
|
+
lines.push(`Sleep (last ${recentDays} days): avg ${avgHours}h/night`);
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
const recentBodyWeight = (metrics.bodyWeight ?? []).filter((m) => m.date >= cutoff);
|
|
990
|
+
if (recentBodyWeight.length > 0) {
|
|
991
|
+
const latest = recentBodyWeight[recentBodyWeight.length - 1];
|
|
992
|
+
const earliest = recentBodyWeight[0];
|
|
993
|
+
const delta = (latest.value - earliest.value).toFixed(1);
|
|
994
|
+
const trend = delta > 0 ? `+${delta}` : delta;
|
|
995
|
+
lines.push(`Body weight (last ${recentDays} days): latest ${latest.value.toFixed(1)} kg (${latest.date}), ${recentBodyWeight.length} readings, trend ${trend} kg`);
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
function healthSummary(snapshot, days = 14) {
|
|
1000
|
+
const metrics = snapshot.healthMetrics;
|
|
1001
|
+
if (!metrics) return { available: false };
|
|
1002
|
+
|
|
1003
|
+
const cutoff = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString().slice(0, 10);
|
|
1004
|
+
|
|
1005
|
+
const recentWorkouts = (metrics.otherWorkouts ?? []).filter((w) => w.date >= cutoff);
|
|
1006
|
+
const recentRestingHR = (metrics.restingHR ?? []).filter((m) => m.date >= cutoff);
|
|
1007
|
+
const recentHRV = (metrics.hrv ?? []).filter((m) => m.date >= cutoff);
|
|
1008
|
+
const recentVO2Max = (metrics.vo2Max ?? []).filter((m) => m.date >= cutoff);
|
|
1009
|
+
const recentSleep = (metrics.sleep ?? []).filter((m) => m.date >= cutoff);
|
|
1010
|
+
|
|
1011
|
+
const avg = (arr) => arr.length > 0 ? arr.reduce((s, m) => s + m.value, 0) / arr.length : null;
|
|
1012
|
+
|
|
1013
|
+
return {
|
|
1014
|
+
available: true,
|
|
1015
|
+
days,
|
|
1016
|
+
cardio: recentWorkouts.map((w) => ({
|
|
1017
|
+
date: w.date,
|
|
1018
|
+
workoutType: w.workoutType,
|
|
1019
|
+
durationMins: w.durationSecs ? Math.round(w.durationSecs / 60) : null,
|
|
1020
|
+
distanceKm: w.distanceKm ?? null,
|
|
1021
|
+
avgHR: w.avgHR ?? null,
|
|
1022
|
+
calories: w.calories ?? null
|
|
1023
|
+
})),
|
|
1024
|
+
restingHR: {
|
|
1025
|
+
avg: recentRestingHR.length > 0 ? Math.round(avg(recentRestingHR)) : null,
|
|
1026
|
+
latest: recentRestingHR.length > 0 ? { value: Math.round(recentRestingHR.at(-1).value), date: recentRestingHR.at(-1).date } : null,
|
|
1027
|
+
readings: recentRestingHR.length
|
|
1028
|
+
},
|
|
1029
|
+
hrv: {
|
|
1030
|
+
avg: recentHRV.length > 0 ? Math.round(avg(recentHRV)) : null,
|
|
1031
|
+
latest: recentHRV.length > 0 ? { value: Math.round(recentHRV.at(-1).value), date: recentHRV.at(-1).date } : null,
|
|
1032
|
+
readings: recentHRV.length
|
|
1033
|
+
},
|
|
1034
|
+
vo2Max: {
|
|
1035
|
+
latest: recentVO2Max.length > 0 ? { value: Math.round(recentVO2Max.at(-1).value * 10) / 10, date: recentVO2Max.at(-1).date } : null,
|
|
1036
|
+
readings: recentVO2Max.length
|
|
1037
|
+
},
|
|
1038
|
+
sleep: {
|
|
1039
|
+
avgHours: recentSleep.length > 0 ? Math.round(recentSleep.reduce((s, m) => s + m.durationMins, 0) / recentSleep.length / 60 * 10) / 10 : null,
|
|
1040
|
+
nights: recentSleep.length
|
|
1041
|
+
},
|
|
1042
|
+
bodyWeight: (() => {
|
|
1043
|
+
const recent = (metrics.bodyWeight ?? []).filter((m) => m.date >= cutoff);
|
|
1044
|
+
if (recent.length === 0) return { latest: null, readings: 0 };
|
|
1045
|
+
return {
|
|
1046
|
+
latest: { value: Math.round(recent.at(-1).value * 10) / 10, date: recent.at(-1).date },
|
|
1047
|
+
trend: Math.round((recent.at(-1).value - recent[0].value) * 10) / 10,
|
|
1048
|
+
readings: recent.length
|
|
1049
|
+
};
|
|
1050
|
+
})()
|
|
1051
|
+
};
|
|
1052
|
+
}
|
|
1053
|
+
|
|
776
1054
|
function requiredOption(options, primaryKey, legacyKey = null) {
|
|
777
1055
|
return options[primaryKey] ?? (legacyKey ? options[legacyKey] : undefined);
|
|
778
1056
|
}
|
|
@@ -894,5 +1172,10 @@ export function executeReadCommand(snapshot, normalizedCommand, options = {}) {
|
|
|
894
1172
|
return { ok: true, payload };
|
|
895
1173
|
}
|
|
896
1174
|
|
|
1175
|
+
if (normalizedCommand === 'health-summary') {
|
|
1176
|
+
const days = Number.parseInt(options.days ?? '14', 10);
|
|
1177
|
+
return { ok: true, payload: healthSummary(snapshot, Number.isNaN(days) ? 14 : days) };
|
|
1178
|
+
}
|
|
1179
|
+
|
|
897
1180
|
return { ok: false, error: `Unknown read command: ${normalizedCommand}` };
|
|
898
1181
|
}
|
package/src/remote.js
CHANGED
|
@@ -29,10 +29,15 @@ const remoteCommandHandlers = {
|
|
|
29
29
|
'program-list': executeRemoteRead,
|
|
30
30
|
'program-summary': executeRemoteRead,
|
|
31
31
|
'program-detail': executeRemoteRead,
|
|
32
|
+
'cycle-summary-list': executeRemoteRead,
|
|
33
|
+
'cycle-summary-show': executeRemoteRead,
|
|
32
34
|
'planned-vs-actual': executeRemoteRead,
|
|
33
35
|
'why-did-this-change': executeRemoteRead,
|
|
34
36
|
'goals-list': executeRemoteRead,
|
|
35
|
-
'goals-show': executeRemoteRead
|
|
37
|
+
'goals-show': executeRemoteRead,
|
|
38
|
+
'health-summary': executeRemoteRead,
|
|
39
|
+
'ask-history': executeRemoteRead,
|
|
40
|
+
'ask-show': executeRemoteRead
|
|
36
41
|
};
|
|
37
42
|
|
|
38
43
|
async function executeRemoteRead(options, sessionState, normalizedCommand) {
|
|
@@ -111,6 +116,15 @@ function endpointForCommand(baseUrl, normalizedCommand, options) {
|
|
|
111
116
|
return resolveServiceUrl(baseUrl, '/cli/programs/current');
|
|
112
117
|
case 'program-detail':
|
|
113
118
|
return resolveServiceUrl(baseUrl, options.id ? `/cli/programs/${options.id}` : '/cli/programs/active');
|
|
119
|
+
case 'cycle-summary-list': {
|
|
120
|
+
const cyclesUrl = resolveServiceUrl(baseUrl, '/cli/cycles');
|
|
121
|
+
if (options['program-id']) {
|
|
122
|
+
cyclesUrl.searchParams.set('program-id', options['program-id']);
|
|
123
|
+
}
|
|
124
|
+
return cyclesUrl;
|
|
125
|
+
}
|
|
126
|
+
case 'cycle-summary-show':
|
|
127
|
+
return resolveServiceUrl(baseUrl, `/cli/cycles/${options.id}`);
|
|
114
128
|
case 'exercise-history': {
|
|
115
129
|
const historyUrl = resolveServiceUrl(baseUrl, '/cli/exercises/history');
|
|
116
130
|
historyUrl.searchParams.set('name', options.name ?? options.exercise);
|
|
@@ -122,6 +136,22 @@ function endpointForCommand(baseUrl, normalizedCommand, options) {
|
|
|
122
136
|
return resolveServiceUrl(baseUrl, '/cli/goals');
|
|
123
137
|
case 'goals-show':
|
|
124
138
|
return resolveServiceUrl(baseUrl, options.id ? `/cli/goals/${options.id}` : '/cli/goals');
|
|
139
|
+
case 'health-summary': {
|
|
140
|
+
const healthUrl = resolveServiceUrl(baseUrl, '/cli/health/summary');
|
|
141
|
+
if (options.days) {
|
|
142
|
+
healthUrl.searchParams.set('days', options.days);
|
|
143
|
+
}
|
|
144
|
+
return healthUrl;
|
|
145
|
+
}
|
|
146
|
+
case 'ask-history': {
|
|
147
|
+
const askUrl = resolveServiceUrl(baseUrl, '/cli/ask/history');
|
|
148
|
+
if (options.limit) {
|
|
149
|
+
askUrl.searchParams.set('limit', options.limit);
|
|
150
|
+
}
|
|
151
|
+
return askUrl;
|
|
152
|
+
}
|
|
153
|
+
case 'ask-show':
|
|
154
|
+
return resolveServiceUrl(baseUrl, `/cli/ask/history/${options.id}`);
|
|
125
155
|
default:
|
|
126
156
|
return resolveServiceUrl(baseUrl, '/');
|
|
127
157
|
}
|
|
@@ -136,6 +166,14 @@ function resourceNotFoundMessage(normalizedCommand, options) {
|
|
|
136
166
|
return `Session not found: ${options['session-id']}`;
|
|
137
167
|
}
|
|
138
168
|
|
|
169
|
+
if (normalizedCommand === 'cycle-summary-show') {
|
|
170
|
+
return `Cycle summary not found: ${options.id}`;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (normalizedCommand === 'ask-show') {
|
|
174
|
+
return `Conversation not found: ${options.id}`;
|
|
175
|
+
}
|
|
176
|
+
|
|
139
177
|
return 'Requested resource was not found.';
|
|
140
178
|
}
|
|
141
179
|
|