incremnt 0.1.9 → 0.1.11
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/package.json +1 -1
- package/src/contract.js +17 -0
- package/src/format.js +54 -0
- package/src/mcp.js +0 -0
- package/src/openrouter.js +34 -7
- package/src/queries.js +115 -2
package/package.json
CHANGED
package/src/contract.js
CHANGED
|
@@ -98,6 +98,23 @@ export const commandSchema = [
|
|
|
98
98
|
options: [
|
|
99
99
|
{ name: 'id', type: 'string', required: true, description: 'Plan ID' }
|
|
100
100
|
]
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
command: 'cycles list',
|
|
104
|
+
id: 'cycle-summary-list',
|
|
105
|
+
description: 'List cycle summaries',
|
|
106
|
+
options: [
|
|
107
|
+
{ name: 'program-id', type: 'string', required: false, description: 'Filter by program ID' }
|
|
108
|
+
]
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
command: 'cycles show',
|
|
112
|
+
id: 'cycle-summary-show',
|
|
113
|
+
description: 'Show cycle summary details',
|
|
114
|
+
usage: 'cycles show --id <summary-id>',
|
|
115
|
+
options: [
|
|
116
|
+
{ name: 'id', type: 'string', required: true, description: 'Cycle summary ID' }
|
|
117
|
+
]
|
|
101
118
|
}
|
|
102
119
|
];
|
|
103
120
|
|
package/src/format.js
CHANGED
|
@@ -403,6 +403,58 @@ function formatProposalsList(payload) {
|
|
|
403
403
|
return lines.join('\n');
|
|
404
404
|
}
|
|
405
405
|
|
|
406
|
+
function formatCycleSummaryList(payload) {
|
|
407
|
+
if (!Array.isArray(payload) || payload.length === 0) {
|
|
408
|
+
return 'No cycle summaries found.';
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const lines = [header('CYCLE SUMMARIES'), ''];
|
|
412
|
+
|
|
413
|
+
for (const summary of payload) {
|
|
414
|
+
const date = formatShortDate(summary.completedDate);
|
|
415
|
+
const sets = `${summary.totalSetsCompleted}/${summary.totalSetsPlanned} sets`;
|
|
416
|
+
const progressions = summary.progressionCount > 0 ? `${summary.progressionCount} progressions` : '';
|
|
417
|
+
const ai = summary.hasAISummary ? chalk.green('AI') : '';
|
|
418
|
+
const parts = [chalk.dim(sets), progressions ? chalk.dim(progressions) : '', ai].filter(Boolean);
|
|
419
|
+
|
|
420
|
+
lines.push(` ${chalk.bold(date)} ${summary.programName}${dimDot()}${parts.join(dimDot())}`);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
return lines.join('\n');
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function formatCycleSummaryShow(payload) {
|
|
427
|
+
if (!payload) {
|
|
428
|
+
return 'Cycle summary not found.';
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const date = formatDayAndDate(payload.completedDate);
|
|
432
|
+
const lines = [` ${chalk.bold('CYCLE SUMMARY')}${dimDot()}${date}`, ''];
|
|
433
|
+
|
|
434
|
+
lines.push(keyValue('Program', payload.programName));
|
|
435
|
+
lines.push(keyValue('Sets', `${payload.totalSetsCompleted}/${payload.totalSetsPlanned}`));
|
|
436
|
+
|
|
437
|
+
if (payload.progressionUpdates.length > 0) {
|
|
438
|
+
lines.push('');
|
|
439
|
+
lines.push(` ${chalk.bold('Progressions')}`);
|
|
440
|
+
|
|
441
|
+
for (const update of payload.progressionUpdates) {
|
|
442
|
+
const icon = update.kind === 'deload' ? chalk.yellow('\u2193') : chalk.green('\u2191');
|
|
443
|
+
lines.push(` ${icon} ${update.exerciseName.padEnd(24)} ${chalk.dim(update.change)}`);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
if (payload.aiSummary) {
|
|
448
|
+
lines.push('');
|
|
449
|
+
lines.push(` ${chalk.bold('AI Summary')}`);
|
|
450
|
+
for (const line of payload.aiSummary.split('\n')) {
|
|
451
|
+
lines.push(` ${line}`);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
return lines.join('\n');
|
|
456
|
+
}
|
|
457
|
+
|
|
406
458
|
function formatProposalDismissed(payload) {
|
|
407
459
|
if (!payload) return 'Proposal not found.';
|
|
408
460
|
return ` Proposal ${chalk.bold(payload.id)} dismissed.`;
|
|
@@ -459,6 +511,8 @@ export function formatPretty(command, payload) {
|
|
|
459
511
|
'program-detail': formatProgramDetail,
|
|
460
512
|
'planned-vs-actual': formatPlannedVsActual,
|
|
461
513
|
'why-did-this-change': formatWhyDidThisChange,
|
|
514
|
+
'cycle-summary-list': formatCycleSummaryList,
|
|
515
|
+
'cycle-summary-show': formatCycleSummaryShow,
|
|
462
516
|
'programs-propose': formatProposalCreated,
|
|
463
517
|
'programs-proposals': formatProposalsList,
|
|
464
518
|
'proposal-dismiss': formatProposalDismissed
|
package/src/mcp.js
CHANGED
|
File without changes
|
package/src/openrouter.js
CHANGED
|
@@ -100,16 +100,24 @@ function formatCycleContext(ctx) {
|
|
|
100
100
|
return lines.join('\n');
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
-
export const WORKOUT_COACH_PROMPT = `You are
|
|
103
|
+
export const WORKOUT_COACH_PROMPT = `You are reviewing a training session log. Write 2-3 short paragraphs separated by blank lines.
|
|
104
|
+
|
|
105
|
+
Your job is to surface things the user wouldn't notice from glancing at their workout summary. The app already shows them PRs, total volume, effort score, and exercise breakdown — do NOT repeat any of that.
|
|
106
|
+
|
|
107
|
+
Focus on:
|
|
108
|
+
- Plan deviations: exercises swapped, skipped, or added vs the plan. Ask why if something looks unusual.
|
|
109
|
+
- Set completion: if they did fewer sets than planned on an exercise, note it and ask about it.
|
|
110
|
+
- Cross-session patterns: trends across recent sessions (volume direction on specific lifts, consistent cutoffs, same weight for weeks).
|
|
111
|
+
- Ask 1-2 genuine questions about choices that look interesting or unusual.
|
|
104
112
|
|
|
105
113
|
Rules:
|
|
106
|
-
-
|
|
107
|
-
-
|
|
108
|
-
- Compare volume or effort to recent sessions if the data is there. Just state what changed.
|
|
109
|
-
- End with one concrete suggestion for next session. Be specific about which exercise and what to try.
|
|
110
|
-
- Write like a real person texting a training partner — short sentences, no filler, no cheerleading.
|
|
111
|
-
- Never use words like: impressive, crushed, solid, demonstrating, significant, incredible, fantastic, highlighting.
|
|
114
|
+
- Only state what the data shows. Never claim how something "felt" — you have numbers, not feelings.
|
|
115
|
+
- Never use words like: impressive, crushed, solid, demonstrating, significant, incredible, fantastic, smooth, controlled.
|
|
112
116
|
- Never use -ing clauses that add fake depth ("indicating progressive overload", "demonstrating strength gains").
|
|
117
|
+
- Never say things "felt smooth", "felt controlled", "feels about average" — you cannot know this.
|
|
118
|
+
- Never restate PRs, total volume, or effort score — the user already sees these in the app.
|
|
119
|
+
- Write like a training partner looking at a logbook, not a motivational coach.
|
|
120
|
+
- Short sentences, no filler, no cheerleading. Questions are good.
|
|
113
121
|
- No bullet points or lists.`;
|
|
114
122
|
|
|
115
123
|
export async function generateWorkoutCoachingSummary(workoutContext, { apiKey, model, timeoutMs } = {}) {
|
|
@@ -189,5 +197,24 @@ export function formatWorkoutContext(ctx) {
|
|
|
189
197
|
}
|
|
190
198
|
}
|
|
191
199
|
|
|
200
|
+
if (ctx.planComparison) {
|
|
201
|
+
const planLines = [];
|
|
202
|
+
if (ctx.planComparison.skipped.length > 0) {
|
|
203
|
+
planLines.push(` Skipped: ${ctx.planComparison.skipped.join(', ')}`);
|
|
204
|
+
}
|
|
205
|
+
if (ctx.planComparison.added.length > 0) {
|
|
206
|
+
planLines.push(` Added: ${ctx.planComparison.added.join(', ')}`);
|
|
207
|
+
}
|
|
208
|
+
for (const sc of ctx.planComparison.setsComparison) {
|
|
209
|
+
if (sc.completed !== sc.planned) {
|
|
210
|
+
planLines.push(` ${sc.exercise}: ${sc.completed}/${sc.planned} sets`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
if (planLines.length > 0) {
|
|
214
|
+
lines.push('Plan comparison:');
|
|
215
|
+
lines.push(...planLines);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
192
219
|
return lines.join('\n');
|
|
193
220
|
}
|
package/src/queries.js
CHANGED
|
@@ -2,6 +2,44 @@ function completionDateForSession(session) {
|
|
|
2
2
|
return session.completedAt ?? session.summary?.date ?? session.date;
|
|
3
3
|
}
|
|
4
4
|
|
|
5
|
+
function buildPlanComparison(session, performedExercises, plannedExercises) {
|
|
6
|
+
if (!Array.isArray(plannedExercises) || plannedExercises.length === 0) {
|
|
7
|
+
return undefined;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const plannedNames = plannedExercises.map((exercise) =>
|
|
11
|
+
normalizeExerciseName(exercise.name ?? exercise.exerciseName)
|
|
12
|
+
);
|
|
13
|
+
const performedNames = performedExercises.map((exercise) =>
|
|
14
|
+
normalizeExerciseName(exercise.exerciseName)
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
const skipped = plannedExercises
|
|
18
|
+
.filter((exercise) => !performedNames.includes(normalizeExerciseName(exercise.name ?? exercise.exerciseName)))
|
|
19
|
+
.map((exercise) => exercise.name ?? exercise.exerciseName);
|
|
20
|
+
|
|
21
|
+
const added = (session.exercises ?? [])
|
|
22
|
+
.filter((exercise) => !plannedNames.includes(normalizeExerciseName(exercise.name)))
|
|
23
|
+
.map((exercise) => exercise.name);
|
|
24
|
+
|
|
25
|
+
const setsComparison = plannedExercises
|
|
26
|
+
.filter((exercise) => performedNames.includes(normalizeExerciseName(exercise.name ?? exercise.exerciseName)))
|
|
27
|
+
.map((planned) => {
|
|
28
|
+
const plannedName = planned.name ?? planned.exerciseName;
|
|
29
|
+
const performed = (session.exercises ?? []).find(
|
|
30
|
+
(exercise) => normalizeExerciseName(exercise.name) === normalizeExerciseName(plannedName)
|
|
31
|
+
);
|
|
32
|
+
const completedSets = (performed?.sets ?? []).filter((set) => set.isComplete).length;
|
|
33
|
+
return {
|
|
34
|
+
exercise: plannedName,
|
|
35
|
+
planned: Array.isArray(planned.sets) ? planned.sets.length : (planned.targetSets ?? []).length,
|
|
36
|
+
completed: completedSets
|
|
37
|
+
};
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
return { skipped, added, setsComparison };
|
|
41
|
+
}
|
|
42
|
+
|
|
5
43
|
function sessionSummary(session) {
|
|
6
44
|
return {
|
|
7
45
|
sessionId: session.id,
|
|
@@ -19,7 +57,8 @@ function sessionSummary(session) {
|
|
|
19
57
|
exerciseCount: (session.exercises ?? []).length,
|
|
20
58
|
recommendations: session.recommendations ?? {},
|
|
21
59
|
historicalContext: session.historicalContext ?? null,
|
|
22
|
-
prescriptionSnapshot: session.prescriptionSnapshot ?? null
|
|
60
|
+
prescriptionSnapshot: session.prescriptionSnapshot ?? null,
|
|
61
|
+
aiCoachNotes: session.aiCoachNotes ?? null
|
|
23
62
|
};
|
|
24
63
|
}
|
|
25
64
|
|
|
@@ -207,6 +246,7 @@ export function sessionDetails(snapshot, sessionId) {
|
|
|
207
246
|
summary.exercises = (session.exercises ?? []).map((exercise) => ({
|
|
208
247
|
name: exercise.name,
|
|
209
248
|
muscleGroup: exercise.muscleGroup ?? null,
|
|
249
|
+
swappedFrom: exercise.swappedFrom ?? null,
|
|
210
250
|
sets: (exercise.sets ?? []).filter((s) => s.isComplete).map((s) => ({
|
|
211
251
|
weight: s.weight ?? null,
|
|
212
252
|
reps: s.reps ?? null,
|
|
@@ -235,6 +275,7 @@ export function plannedVsActual(snapshot, sessionId) {
|
|
|
235
275
|
return {
|
|
236
276
|
exerciseName: exercise.name,
|
|
237
277
|
muscleGroup: exercise.muscleGroup,
|
|
278
|
+
swappedFrom: exercise.swappedFrom ?? null,
|
|
238
279
|
plannedSets: planned?.targetSets ?? [],
|
|
239
280
|
actualSets: (exercise.sets ?? []).filter((set) => set.isComplete),
|
|
240
281
|
plannedRir: planned?.rir ?? null,
|
|
@@ -347,6 +388,43 @@ export function goalDetail(snapshot, planId) {
|
|
|
347
388
|
};
|
|
348
389
|
}
|
|
349
390
|
|
|
391
|
+
export function cycleSummaryList(snapshot, programId) {
|
|
392
|
+
const summaries = snapshot.cycleSummaries ?? [];
|
|
393
|
+
const filtered = programId
|
|
394
|
+
? summaries.filter((s) => s.programId === programId)
|
|
395
|
+
: summaries;
|
|
396
|
+
|
|
397
|
+
return [...filtered]
|
|
398
|
+
.sort((a, b) => String(b.completedDate).localeCompare(String(a.completedDate)))
|
|
399
|
+
.map((s) => ({
|
|
400
|
+
id: s.id,
|
|
401
|
+
programId: s.programId,
|
|
402
|
+
programName: s.programName,
|
|
403
|
+
completedDate: s.completedDate,
|
|
404
|
+
totalSetsCompleted: s.totalSetsCompleted ?? 0,
|
|
405
|
+
totalSetsPlanned: s.totalSetsPlanned ?? 0,
|
|
406
|
+
progressionCount: (s.progressionUpdates ?? []).length,
|
|
407
|
+
hasAISummary: !!s.aiSummary
|
|
408
|
+
}));
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
export function cycleSummaryShow(snapshot, summaryId) {
|
|
412
|
+
const summaries = snapshot.cycleSummaries ?? [];
|
|
413
|
+
const summary = summaries.find((s) => s.id === summaryId);
|
|
414
|
+
if (!summary) return null;
|
|
415
|
+
|
|
416
|
+
return {
|
|
417
|
+
id: summary.id,
|
|
418
|
+
programId: summary.programId,
|
|
419
|
+
programName: summary.programName,
|
|
420
|
+
completedDate: summary.completedDate,
|
|
421
|
+
totalSetsCompleted: summary.totalSetsCompleted ?? 0,
|
|
422
|
+
totalSetsPlanned: summary.totalSetsPlanned ?? 0,
|
|
423
|
+
progressionUpdates: summary.progressionUpdates ?? [],
|
|
424
|
+
aiSummary: summary.aiSummary ?? null
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
|
|
350
428
|
export function cycleSummaryContext(snapshot, programId) {
|
|
351
429
|
const programs = snapshot.programs ?? [];
|
|
352
430
|
const program = programId
|
|
@@ -561,7 +639,21 @@ export function workoutSummaryContext(snapshot, sessionId) {
|
|
|
561
639
|
}
|
|
562
640
|
}
|
|
563
641
|
|
|
564
|
-
|
|
642
|
+
// Plan comparison — prefer the logged point-in-time prescription snapshot.
|
|
643
|
+
let planComparison;
|
|
644
|
+
if (session.prescriptionSnapshot?.exercises?.length > 0) {
|
|
645
|
+
planComparison = buildPlanComparison(session, exercises, session.prescriptionSnapshot.exercises);
|
|
646
|
+
} else if (session.programId) {
|
|
647
|
+
const program = (snapshot.programs ?? []).find(p => p.id === session.programId);
|
|
648
|
+
const matchingDay = Number.isInteger(session.programDayIndex)
|
|
649
|
+
? program?.days?.[session.programDayIndex]
|
|
650
|
+
: program?.days?.find(d => d.title === dayName);
|
|
651
|
+
if (matchingDay) {
|
|
652
|
+
planComparison = buildPlanComparison(session, exercises, matchingDay.exercises ?? []);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
const result = {
|
|
565
657
|
sessionDate,
|
|
566
658
|
dayName,
|
|
567
659
|
totalVolume: session.summary?.totalVolume ?? session.volume ?? 0,
|
|
@@ -570,6 +662,8 @@ export function workoutSummaryContext(snapshot, sessionId) {
|
|
|
570
662
|
recentComparisons,
|
|
571
663
|
prs
|
|
572
664
|
};
|
|
665
|
+
if (planComparison) result.planComparison = planComparison;
|
|
666
|
+
return result;
|
|
573
667
|
}
|
|
574
668
|
|
|
575
669
|
function requiredOption(options, primaryKey, legacyKey = null) {
|
|
@@ -674,5 +768,24 @@ export function executeReadCommand(snapshot, normalizedCommand, options = {}) {
|
|
|
674
768
|
return { ok: true, payload };
|
|
675
769
|
}
|
|
676
770
|
|
|
771
|
+
if (normalizedCommand === 'cycle-summary-list') {
|
|
772
|
+
const programId = requiredOption(options, 'program-id');
|
|
773
|
+
return { ok: true, payload: cycleSummaryList(snapshot, programId ?? null) };
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
if (normalizedCommand === 'cycle-summary-show') {
|
|
777
|
+
const summaryId = requiredOption(options, 'id');
|
|
778
|
+
if (!summaryId) {
|
|
779
|
+
return { ok: false, error: '--id is required' };
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
const payload = cycleSummaryShow(snapshot, summaryId);
|
|
783
|
+
if (!payload) {
|
|
784
|
+
return { ok: false, error: `Cycle summary not found: ${summaryId}` };
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
return { ok: true, payload };
|
|
788
|
+
}
|
|
789
|
+
|
|
677
790
|
return { ok: false, error: `Unknown read command: ${normalizedCommand}` };
|
|
678
791
|
}
|