incremnt 0.8.8 → 0.8.9

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 CHANGED
@@ -110,7 +110,7 @@ incremnt login --session-file ~/Downloads/session.json
110
110
  | `coach observations list` | Current persisted coach observations |
111
111
  | `coach observations seen --id <id>` | Mark a coach observation as seen |
112
112
  | `coach observations dismiss --id <id>` | Dismiss an observation and suppress matching follow-ups |
113
- | `programs create --file <file>` | Create an inactive saved program from generated-plan JSON |
113
+ | `programs create --file <file>` | Create an inactive saved program from generated-plan JSON, including day-local supersets |
114
114
  | `programs propose --file <file>` | Submit a program proposal |
115
115
  | `programs proposals` | List proposals |
116
116
  | `programs proposal dismiss --id <id>` | Dismiss a proposal |
package/SKILL.md CHANGED
@@ -26,7 +26,7 @@ Append `--dry-run` to any mutating command to preview the HTTP request as `{ dry
26
26
 
27
27
  | Command | Reversibility | Notes |
28
28
  |---|---|---|
29
- | `programs create --file <f>` | Reversible in iOS (created inactive) | Creates a saved inactive program from Coach-compatible generated-plan JSON. It does not activate or replace the current plan. Use `records` or `exercises history` first for canonical names. |
29
+ | `programs create --file <f>` | Reversible in iOS (created inactive) | Creates a saved inactive program from Coach-compatible generated-plan JSON, including day-local supersets via `supersetKey`/`supersetOrder`. It does not activate or replace the current plan. Use `records` or `exercises history` first for canonical names. |
30
30
  | `programs propose --file <f>` | Reversible (proposal stays pending) | Weights omitted; iOS computes from history. Names must come from `records` or `exercises history`. |
31
31
  | `programs proposals` | Read-only lookup | Use before dismissing a proposal. |
32
32
  | `programs proposal dismiss --id <id>` | Irreversible | IDs come from `programs proposals`. |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "incremnt",
3
- "version": "0.8.8",
3
+ "version": "0.8.9",
4
4
  "description": "Command-line tool for querying your incremnt strength training data",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/contract.js CHANGED
@@ -1,4 +1,4 @@
1
- export const contractVersion = 24;
1
+ export const contractVersion = 25;
2
2
 
3
3
  export const capabilities = {
4
4
  readOnly: false,
@@ -281,7 +281,7 @@ export const writeCommandSchema = [
281
281
  description: 'Create a saved inactive program from a generated plan file',
282
282
  usage: 'programs create --file <file>',
283
283
  dryRun: true,
284
- agentNotes: 'Mutation. Creates a brand-new inactive saved program from Coach-compatible generated-plan JSON; it does not activate or replace the current plan. External assistants should draft the JSON file, then run programs create --file <path>. Use records or exercises history before drafting exercise names; do not invent unusual exercise names.',
284
+ agentNotes: 'Mutation. Creates a brand-new inactive saved program from Coach-compatible generated-plan JSON; it does not activate or replace the current plan. External assistants should draft the JSON file, then run programs create --file <path>. Use records or exercises history before drafting exercise names; do not invent unusual exercise names. Supersets are supported with day-local supersetKey plus supersetOrder; the service converts them to native iOS supersetGroupId fields.',
285
285
  options: [
286
286
  { name: 'file', type: 'string', format: 'path', required: true, description: 'Path to Coach-compatible generated-plan JSON file' }
287
287
  ]
@@ -446,31 +446,48 @@ for (const command of writeCommandSchema) {
446
446
  }
447
447
 
448
448
  export const programDraftSchema = {
449
- description: 'JSON structure for programs create --file and programs propose --file. Weights are omitted iOS calculates from history.',
449
+ description: 'JSON structure for programs create --file and programs propose --file. This mirrors the current iOS program shape: concrete set reps, optional warmups/notes/RIR, and day-local supersetKey groups. Rep ranges and rest overrides are not supported.',
450
450
  required: ['name', 'daysPerWeek', 'equipmentTier', 'days'],
451
451
  properties: {
452
452
  name: { type: 'string', description: 'Program name' },
453
453
  equipmentTier: { type: 'string', enum: ['fullGym', 'benchDumbbells', 'dumbbellsOnly', 'bodyweightOnly'] },
454
454
  daysPerWeek: { type: 'integer', minimum: 1, maximum: 7, description: 'Training days per week' },
455
+ volumeLevel: { type: 'string', enum: ['minimum', 'moderate', 'high'], description: 'Program volume label. Optional, defaults to moderate.' },
456
+ currentDayIndex: { type: 'integer', minimum: 0, description: 'Initial current day index. Optional, defaults to 0.' },
455
457
  trainingWeekdays: { type: 'array', items: { type: 'integer', minimum: 0, maximum: 6 }, description: 'Days of week (0=Sun). Optional.' },
456
- rationale: { type: 'string', description: 'Why this program was suggested. Optional.' },
457
458
  days: {
458
459
  type: 'array',
459
460
  minItems: 1,
460
461
  items: {
461
- required: ['title', 'exercises'],
462
+ required: ['dayLabel', 'title', 'exercises'],
462
463
  properties: {
464
+ dayLabel: { type: 'string', description: 'Short day label, e.g. "Day 1".' },
463
465
  title: { type: 'string', description: 'Day name, e.g. "Upper A"' },
466
+ subtitle: { type: 'string', description: 'Optional day subtitle.' },
464
467
  exercises: {
465
468
  type: 'array',
466
469
  minItems: 1,
467
470
  items: {
468
- required: ['name', 'sets', 'reps'],
471
+ required: ['name', 'muscleGroup', 'sets'],
469
472
  properties: {
470
473
  name: { type: 'string', description: 'Exercise name (use canonical names from records or exercises history)' },
471
- muscleGroup: { type: 'string', description: 'e.g. "Chest", "Back". Optional.' },
472
- sets: { type: 'integer', minimum: 1 },
473
- reps: { type: 'integer', minimum: 1 }
474
+ muscleGroup: { type: 'string', description: 'e.g. "Chest", "Back".' },
475
+ sets: {
476
+ type: 'array',
477
+ minItems: 1,
478
+ items: {
479
+ required: ['weight', 'reps'],
480
+ properties: {
481
+ weight: { type: 'number', minimum: 0, description: 'Planned load. Use 0 for bodyweight or app-computed/manual adjustment later.' },
482
+ reps: { type: 'integer', minimum: 1, maximum: 30 },
483
+ isWarmup: { type: 'boolean', description: 'Optional warmup marker.' }
484
+ }
485
+ }
486
+ },
487
+ note: { type: 'string', description: 'Optional prescription note.' },
488
+ rir: { type: 'integer', minimum: 0, maximum: 5, description: 'Optional app-supported RIR field.' },
489
+ supersetKey: { type: 'string', description: 'Optional day-local label shared by 2+ exercises to create a native iOS superset.' },
490
+ supersetOrder: { type: 'integer', minimum: 0, maximum: 24, description: 'Required when supersetKey is present; order within that superset group.' }
474
491
  }
475
492
  }
476
493
  }
@@ -18,12 +18,15 @@ export const PROGRAM_DRAFT_LIMITS = {
18
18
  dayTitleMaxLen: 120,
19
19
  daySubtitleMaxLen: 120,
20
20
  noteMaxLen: 1000,
21
+ supersetKeyMaxLen: 64,
21
22
  minWeight: 0,
22
23
  maxWeight: 600,
23
24
  minReps: 1,
24
25
  maxReps: 30,
25
26
  minRir: 0,
26
27
  maxRir: 5,
28
+ minSupersetOrder: 0,
29
+ maxSupersetOrder: 24,
27
30
  minSetsPerExercise: 1,
28
31
  maxSetsPerExercise: 12,
29
32
  minExercisesPerDay: 1,
@@ -81,7 +84,7 @@ function normalizeProgramDraftSet(set) {
81
84
  }
82
85
 
83
86
  function normalizeProgramDraftExercise(exercise, canonicalizeExerciseName, strict = false) {
84
- if (!hasOnlyAllowedKeys(exercise, new Set(['name', 'muscleGroup', 'sets', 'rir', 'note']))) return null;
87
+ if (!hasOnlyAllowedKeys(exercise, new Set(['name', 'muscleGroup', 'sets', 'rir', 'note', 'supersetKey', 'supersetOrder']))) return null;
85
88
 
86
89
  const name = normalizedExerciseDisplayName(exercise?.name, canonicalizeExerciseName);
87
90
  const muscleGroup = String(exercise?.muscleGroup ?? '').trim();
@@ -109,6 +112,20 @@ function normalizeProgramDraftExercise(exercise, canonicalizeExerciseName, stric
109
112
  const note = exercise?.note == null ? null : String(exercise.note);
110
113
  if (note && note.length > PROGRAM_DRAFT_LIMITS.noteMaxLen) return null;
111
114
 
115
+ const supersetKey = exercise?.supersetKey == null ? null : String(exercise.supersetKey).trim();
116
+ if (supersetKey != null && (
117
+ !supersetKey ||
118
+ supersetKey.length > PROGRAM_DRAFT_LIMITS.supersetKeyMaxLen
119
+ )) return null;
120
+
121
+ const supersetOrder = exercise?.supersetOrder == null ? null : Number(exercise.supersetOrder);
122
+ if (supersetKey == null && supersetOrder != null) return null;
123
+ if (supersetKey != null && (
124
+ !Number.isInteger(supersetOrder) ||
125
+ supersetOrder < PROGRAM_DRAFT_LIMITS.minSupersetOrder ||
126
+ supersetOrder > PROGRAM_DRAFT_LIMITS.maxSupersetOrder
127
+ )) return null;
128
+
112
129
  return {
113
130
  name,
114
131
  muscleGroup,
@@ -116,10 +133,33 @@ function normalizeProgramDraftExercise(exercise, canonicalizeExerciseName, stric
116
133
  nextSuggestion: '',
117
134
  sets,
118
135
  ...(note ? { note } : {}),
119
- ...(rir != null ? { rir } : {})
136
+ ...(rir != null ? { rir } : {}),
137
+ ...(supersetKey != null ? { supersetKey, supersetOrder } : {})
120
138
  };
121
139
  }
122
140
 
141
+ function hasValidDaySupersets(exercises) {
142
+ const grouped = new Map();
143
+ for (const exercise of exercises) {
144
+ if (!exercise.supersetKey) continue;
145
+ const existing = grouped.get(exercise.supersetKey) ?? [];
146
+ existing.push(exercise);
147
+ grouped.set(exercise.supersetKey, existing);
148
+ }
149
+
150
+ for (const group of grouped.values()) {
151
+ if (group.length < 2) return false;
152
+ const orders = new Set();
153
+ for (const exercise of group) {
154
+ if (!Number.isInteger(exercise.supersetOrder)) return false;
155
+ if (orders.has(exercise.supersetOrder)) return false;
156
+ orders.add(exercise.supersetOrder);
157
+ }
158
+ }
159
+
160
+ return true;
161
+ }
162
+
123
163
  function normalizeProgramDraftDay(day, canonicalizeExerciseName, strict = false) {
124
164
  if (!hasOnlyAllowedKeys(day, new Set(['dayLabel', 'title', 'subtitle', 'exercises']))) return null;
125
165
 
@@ -139,6 +179,7 @@ function normalizeProgramDraftDay(day, canonicalizeExerciseName, strict = false)
139
179
  exercises.length < PROGRAM_DRAFT_LIMITS.minExercisesPerDay ||
140
180
  exercises.length > PROGRAM_DRAFT_LIMITS.maxExercisesPerDay
141
181
  ) return null;
182
+ if (!hasValidDaySupersets(exercises)) return null;
142
183
 
143
184
  return { dayLabel, title, subtitle, exercises };
144
185
  }
@@ -219,10 +260,15 @@ function defaultChildIdFactory() {
219
260
  return randomUUID();
220
261
  }
221
262
 
263
+ function defaultSupersetIdFactory() {
264
+ return randomUUID();
265
+ }
266
+
222
267
  export function buildProgramFromDraft(rawProgram, {
223
268
  now = new Date(),
224
269
  idFactory = defaultProgramIdFactory,
225
- childIdFactory = defaultChildIdFactory
270
+ childIdFactory = defaultChildIdFactory,
271
+ supersetIdFactory = defaultSupersetIdFactory
226
272
  } = {}) {
227
273
  const draft = normalizeProgramDraft(rawProgram, { strict: true });
228
274
  if (!draft) {
@@ -230,28 +276,44 @@ export function buildProgramFromDraft(rawProgram, {
230
276
  }
231
277
 
232
278
  const createdAt = now instanceof Date ? now.toISOString() : new Date(now).toISOString();
233
- const days = draft.days.map((day) => ({
234
- id: childIdFactory('day'),
235
- dayLabel: day.dayLabel,
236
- title: day.title,
237
- subtitle: day.subtitle,
238
- exercises: day.exercises.map((exercise) => ({
239
- id: childIdFactory('exercise'),
240
- name: exercise.name,
241
- muscleGroup: exercise.muscleGroup,
242
- lastSuggestion: exercise.lastSuggestion,
243
- nextSuggestion: exercise.nextSuggestion,
244
- sets: exercise.sets.map((set) => ({
245
- id: childIdFactory('set'),
246
- weight: set.weight,
247
- reps: set.reps,
248
- isComplete: false,
249
- isWarmup: set.isWarmup === true
250
- })),
251
- ...(exercise.note ? { note: exercise.note } : {}),
252
- ...(exercise.rir != null ? { rir: exercise.rir } : {})
253
- }))
254
- }));
279
+ const days = draft.days.map((day) => {
280
+ const supersetGroupIds = new Map();
281
+ const groupIdForKey = (key) => {
282
+ if (!supersetGroupIds.has(key)) {
283
+ supersetGroupIds.set(key, supersetIdFactory('superset'));
284
+ }
285
+ return supersetGroupIds.get(key);
286
+ };
287
+
288
+ return {
289
+ id: childIdFactory('day'),
290
+ dayLabel: day.dayLabel,
291
+ title: day.title,
292
+ subtitle: day.subtitle,
293
+ exercises: day.exercises.map((exercise) => ({
294
+ id: childIdFactory('exercise'),
295
+ name: exercise.name,
296
+ muscleGroup: exercise.muscleGroup,
297
+ lastSuggestion: exercise.lastSuggestion,
298
+ nextSuggestion: exercise.nextSuggestion,
299
+ sets: exercise.sets.map((set) => ({
300
+ id: childIdFactory('set'),
301
+ weight: set.weight,
302
+ reps: set.reps,
303
+ isComplete: false,
304
+ isWarmup: set.isWarmup === true
305
+ })),
306
+ ...(exercise.note ? { note: exercise.note } : {}),
307
+ ...(exercise.rir != null ? { rir: exercise.rir } : {}),
308
+ ...(exercise.supersetKey
309
+ ? {
310
+ supersetGroupId: groupIdForKey(exercise.supersetKey),
311
+ supersetOrder: exercise.supersetOrder
312
+ }
313
+ : {})
314
+ }))
315
+ };
316
+ });
255
317
 
256
318
  return {
257
319
  id: idFactory('program'),