incremnt 0.5.0 → 0.6.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/README.md CHANGED
@@ -59,14 +59,12 @@ incremnt login --session-file ~/Downloads/session.json
59
59
  | `programs current` | Active program state |
60
60
  | `programs list` | All programs |
61
61
  | `programs show --id <id>` | Full program detail |
62
- | `cycles list [--program-id <id>]` | Completed cycle summaries |
63
- | `cycles show --id <id>` | Details for a completed cycle summary |
64
62
  | `exercises history --name <name>` | Set-by-set history for an exercise |
65
63
  | `records` | Personal records (best e1RM per exercise) |
66
- | `goals list` / `goals show --id <id>` | Strength plan goals |
67
64
  | `health summary` / `health ai` | Health metrics and AI summary |
68
65
  | `training load` | ATL/CTL/TSB and workload context |
69
66
  | `ask history` / `ask show --id <id>` | Coach conversation history |
67
+ | `increment-score current` | Latest Increment Score summary with components, drivers, trend, and data-quality flags |
70
68
  | `increment-score history` | Historical Increment Score snapshots |
71
69
  | `increment-score upload --file <file>` | Upload Increment Score snapshots |
72
70
  | `programs propose --file <file>` | Submit a program proposal |
@@ -121,7 +119,7 @@ incremnt login --session-file ~/Downloads/session.json
121
119
 
122
120
  ## MCP Server
123
121
 
124
- The package includes an [MCP](https://modelcontextprotocol.io) server that exposes the same read queries and program proposal commands as tools for AI assistants like Claude.
122
+ The package includes an [MCP](https://modelcontextprotocol.io) server for AI assistants like Claude and Codex.
125
123
 
126
124
  Run `incremnt mcp install` to auto-register the server with Claude Desktop, Claude Code, and Codex CLI (see [Setup](#setup) above).
127
125
 
@@ -142,7 +140,12 @@ The MCP server uses the same auth session as the CLI — run `incremnt login` fi
142
140
 
143
141
  ### MCP tool surface
144
142
 
145
- The MCP server exposes the same read/write contract as the CLI command surface, including sessions, programs, cycles, goals, health, training load, ask history/show, Increment Score history/upload, program proposal and share workflows, plus typed coach read tools (e.g. `get_increment_score`).
143
+ The MCP server exposes two tool families:
144
+
145
+ - Command-shaped tools from the public CLI contract, including sessions, programs, cycles, goals, health, training load, ask history/show, `increment-score-current`, `increment-score-history`, `increment-score-upload`, program proposals, and program shares.
146
+ - Typed coach read tools for agent-native context retrieval, including `get_increment_score`, `get_recent_sessions`, `get_exercise_history`, `get_next_session`, `get_readiness_snapshot`, `get_body_weight_snapshot`, `get_goal_status`, and `get_records`.
147
+
148
+ `get_increment_score` returns the same privacy-safe score summary as `increment-score current`: score, snapshot timestamp, formula version, data tier, component scores, positive/negative drivers, day-over-day delta, recent trend, and data-quality flags. It does not expose raw HealthKit values.
146
149
 
147
150
  You can inspect the exact machine-readable contract at any time:
148
151
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "incremnt",
3
- "version": "0.5.0",
3
+ "version": "0.6.1",
4
4
  "description": "Command-line tool for querying your incremnt strength training data",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/auth.js CHANGED
@@ -275,7 +275,11 @@ async function fetchRemoteContract(baseUrl, token) {
275
275
 
276
276
  const payload = await response.json();
277
277
  if (payload.contractVersion !== contractVersion) {
278
- const error = new Error(`Remote incremnt sync service is incompatible with this CLI. Expected contract v${contractVersion}, got v${payload.contractVersion}.`);
278
+ const cliBehind = typeof payload.contractVersion === 'number' && payload.contractVersion > contractVersion;
279
+ const hint = cliBehind
280
+ ? ' Your CLI is out of date — run `npm install -g incremnt@latest` to upgrade.'
281
+ : ' The sync service is older than this CLI — wait for the service to redeploy, or use an older CLI version.';
282
+ const error = new Error(`Remote incremnt sync service is incompatible with this CLI. Expected contract v${contractVersion}, got v${payload.contractVersion}.${hint}`);
279
283
  error.code = 'REMOTE_CONTRACT_MISMATCH';
280
284
  throw error;
281
285
  }
package/src/contract.js CHANGED
@@ -1,4 +1,4 @@
1
- export const contractVersion = 10;
1
+ export const contractVersion = 11;
2
2
 
3
3
  export const capabilities = {
4
4
  readOnly: false,
@@ -87,13 +87,15 @@ export const commandSchema = [
87
87
  {
88
88
  command: 'goals list',
89
89
  id: 'goals-list',
90
- description: 'List strength plans and lift goals',
90
+ description: 'Legacy read-only strength plan and lift goal data',
91
+ hidden: true,
91
92
  options: []
92
93
  },
93
94
  {
94
95
  command: 'goals show',
95
96
  id: 'goals-show',
96
- description: 'Show strength plan goal details',
97
+ description: 'Legacy read-only strength plan goal details',
98
+ hidden: true,
97
99
  usage: 'goals show --id <plan-id>',
98
100
  options: [
99
101
  { name: 'id', type: 'string', required: true, description: 'Plan ID' }
@@ -102,7 +104,8 @@ export const commandSchema = [
102
104
  {
103
105
  command: 'cycles list',
104
106
  id: 'cycle-summary-list',
105
- description: 'List cycle summaries',
107
+ description: 'Legacy read-only cycle summaries',
108
+ hidden: true,
106
109
  options: [
107
110
  { name: 'program-id', type: 'string', required: false, description: 'Filter by program ID' }
108
111
  ]
@@ -110,7 +113,8 @@ export const commandSchema = [
110
113
  {
111
114
  command: 'cycles show',
112
115
  id: 'cycle-summary-show',
113
- description: 'Show cycle summary details',
116
+ description: 'Legacy read-only cycle summary details',
117
+ hidden: true,
114
118
  usage: 'cycles show --id <summary-id>',
115
119
  options: [
116
120
  { name: 'id', type: 'string', required: true, description: 'Cycle summary ID' }
@@ -162,6 +166,14 @@ export const commandSchema = [
162
166
  { name: 'token', type: 'string', required: true, description: 'Program share token' }
163
167
  ]
164
168
  },
169
+ {
170
+ command: 'increment-score current',
171
+ id: 'increment-score-current',
172
+ description: 'Show the latest Increment Score snapshot summary',
173
+ options: [
174
+ { name: 'historyDays', type: 'number', required: false, description: 'Recent score history window (default 14, max 60)' }
175
+ ]
176
+ },
165
177
  {
166
178
  command: 'increment-score history',
167
179
  id: 'increment-score-history',
package/src/format.js CHANGED
@@ -689,6 +689,50 @@ function formatIncrementScoreHistory(payload) {
689
689
  return lines.join('\n');
690
690
  }
691
691
 
692
+ function formatIncrementScoreCurrent(payload) {
693
+ if (!payload?.available) {
694
+ return 'No Increment Score snapshots found.';
695
+ }
696
+
697
+ const lines = [
698
+ ` ${chalk.bold('INCREMENT SCORE')}${dimDot()}${chalk.bold(String(payload.score))}${dimDot()}${chalk.dim(payload.dataTier ?? '')}`,
699
+ ''
700
+ ];
701
+
702
+ if (payload.snapshotAt) lines.push(` Snapshot ${chalk.dim(formatShortDate(payload.snapshotAt))}`);
703
+ if (payload.formulaVersion) lines.push(` Formula ${chalk.dim(payload.formulaVersion)}`);
704
+ if (payload.dayOverDayDelta != null) {
705
+ const sign = payload.dayOverDayDelta > 0 ? '+' : '';
706
+ lines.push(` Delta ${chalk.dim(`${sign}${payload.dayOverDayDelta}`)}`);
707
+ }
708
+ if (lines.at(-1) !== '') lines.push('');
709
+
710
+ if (payload.components && typeof payload.components === 'object' && Object.keys(payload.components).length > 0) {
711
+ lines.push(` ${chalk.bold('Components')}`);
712
+ for (const [name, value] of Object.entries(payload.components)) {
713
+ lines.push(` ${name.padEnd(12)} ${chalk.dim(String(value))}`);
714
+ }
715
+ lines.push('');
716
+ }
717
+
718
+ const drivers = (label, list) => {
719
+ if (!Array.isArray(list) || list.length === 0) return;
720
+ lines.push(` ${chalk.bold(label)}`);
721
+ for (const text of list.slice(0, 3)) lines.push(` ${text}`);
722
+ lines.push('');
723
+ };
724
+ drivers('Top positive drivers', payload.topPositiveDrivers);
725
+ drivers('Top negative drivers', payload.topNegativeDrivers);
726
+
727
+ if (Array.isArray(payload.dataQualityNotes) && payload.dataQualityNotes.length > 0) {
728
+ lines.push(` ${chalk.bold('Data quality')}`);
729
+ for (const note of payload.dataQualityNotes) lines.push(` ${note}`);
730
+ }
731
+
732
+ while (lines.at(-1) === '') lines.pop();
733
+ return lines.join('\n');
734
+ }
735
+
692
736
  function formatIncrementScoreUpload(payload) {
693
737
  const inserted = payload?.inserted ?? payload?.count ?? 0;
694
738
  return ` Uploaded ${chalk.bold(String(inserted))} Increment Score snapshot${inserted === 1 ? '' : 's'}.`;
@@ -711,7 +755,7 @@ export function formatHelp(opts = {}) {
711
755
  ` incremnt <command> [options]`,
712
756
  '',
713
757
  header('COMMANDS'),
714
- ...commandSchema.map((c) => cmd(c.usage ?? c.command, c.description)),
758
+ ...commandSchema.filter((c) => !c.hidden).map((c) => cmd(c.usage ?? c.command, c.description)),
715
759
  '',
716
760
  header('WRITE COMMANDS'),
717
761
  ...writeCommandSchema.map((c) => cmd(c.usage ?? c.command, c.description)),
@@ -763,6 +807,7 @@ export function formatPretty(command, payload) {
763
807
  'programs-propose': formatProposalCreated,
764
808
  'programs-proposals': formatProposalsList,
765
809
  'proposal-dismiss': formatProposalDismissed,
810
+ 'increment-score-current': formatIncrementScoreCurrent,
766
811
  'increment-score-history': formatIncrementScoreHistory,
767
812
  'increment-score-upload': formatIncrementScoreUpload
768
813
  }[command];
package/src/lib.js CHANGED
@@ -73,7 +73,15 @@ export async function runCli(argv, stdout, stderr) {
73
73
  })[command] ?? command;
74
74
 
75
75
  const sessionState = await readSessionState();
76
- const isAuthenticated = Boolean(sessionState?.session && !isSessionExpired(sessionState.session));
76
+ const hasTransport = Boolean(
77
+ sessionState?.session?.transport?.baseUrl
78
+ || sessionState?.session?.transport?.fixturePath
79
+ );
80
+ const isAuthenticated = Boolean(
81
+ sessionState?.session
82
+ && !isSessionExpired(sessionState.session)
83
+ && hasTransport
84
+ );
77
85
 
78
86
  if (!command || options.help) {
79
87
  await printLogo(stdout);
package/src/mcp.js CHANGED
@@ -8,6 +8,7 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
8
8
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
9
9
  import { z } from 'zod';
10
10
  import { commandSchema, writeCommands, writeCommandSchema } from './contract.js';
11
+ import { listCoachReadTools } from './queries.js';
11
12
  import { readSessionState } from './state.js';
12
13
  import { createTransport } from './transport.js';
13
14
 
@@ -26,6 +27,32 @@ function commandShape(cmd) {
26
27
  return shape;
27
28
  }
28
29
 
30
+ function coachToolShape(tool) {
31
+ const shape = {};
32
+ const props = tool.inputSchema?.properties ?? {};
33
+ const required = new Set(tool.inputSchema?.required ?? []);
34
+
35
+ for (const [name, schema] of Object.entries(props)) {
36
+ let field;
37
+ if (schema.type === 'integer' || schema.type === 'number') {
38
+ field = z.number();
39
+ } else if (schema.type === 'boolean') {
40
+ field = z.boolean();
41
+ } else if (schema.type === 'array') {
42
+ field = z.array(z.any());
43
+ } else if (schema.type === 'object') {
44
+ field = z.record(z.string(), z.any());
45
+ } else {
46
+ field = z.string();
47
+ }
48
+ if (!required.has(name)) field = field.optional();
49
+ if (schema.description) field = field.describe(schema.description);
50
+ shape[name] = field;
51
+ }
52
+
53
+ return shape;
54
+ }
55
+
29
56
  export function registerMcpTools(server, {
30
57
  readSessionStateFn = readSessionState,
31
58
  createTransportFn = createTransport
@@ -68,6 +95,41 @@ export function registerMcpTools(server, {
68
95
  });
69
96
  }
70
97
 
98
+ for (const tool of listCoachReadTools()) {
99
+ server.tool(tool.name, tool.description, coachToolShape(tool), async (args) => {
100
+ try {
101
+ const sessionState = await readSessionStateFn();
102
+ const transport = await createTransportFn({}, sessionState);
103
+
104
+ if (transport.expired) {
105
+ return {
106
+ content: [{ type: 'text', text: 'Session expired. Run `incremnt login` to re-authenticate.' }],
107
+ isError: true
108
+ };
109
+ }
110
+
111
+ const result = await transport.executeCoachReadTool(tool.name, args);
112
+ return {
113
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }]
114
+ };
115
+ } catch (error) {
116
+ const message = error && error.message ? error.message : String(error);
117
+
118
+ if (error && error.code === 'SNAPSHOT_NOT_FOUND') {
119
+ return {
120
+ content: [{ type: 'text', text: 'Not logged in. Run `incremnt login` first.' }],
121
+ isError: true
122
+ };
123
+ }
124
+
125
+ return {
126
+ content: [{ type: 'text', text: message }],
127
+ isError: true
128
+ };
129
+ }
130
+ });
131
+ }
132
+
71
133
  return server;
72
134
  }
73
135
 
@@ -88,6 +150,11 @@ export function createSandboxServer() {
88
150
  sandbox: true,
89
151
  ok: true
90
152
  }),
153
+ executeCoachReadTool: async (toolName) => ({
154
+ toolName,
155
+ sandbox: true,
156
+ ok: true
157
+ }),
91
158
  executeWriteCommand: async (commandId) => ({
92
159
  commandId,
93
160
  sandbox: true,
package/src/openrouter.js CHANGED
@@ -24,7 +24,7 @@ const TRACE_DETAIL_METADATA = 'metadata';
24
24
  const TRACE_DETAIL_RAW_INTERNAL = 'raw_internal';
25
25
 
26
26
  export const AI_PROMPT_VERSIONS = Object.freeze({
27
- workout: 'workout_v2026_04_24_2',
27
+ workout: 'workout_v2026_05_06_1',
28
28
  cycle: 'cycle_v2026_04_18_1',
29
29
  vitals: 'vitals_v2026_04_16_1',
30
30
  checkpoint: 'checkpoint_v2026_04_16_1',
@@ -70,8 +70,7 @@ function exerciseNamesFromContext(source) {
70
70
  source.exercises?.map((exercise) => exercise.exerciseName ?? exercise.name),
71
71
  source.sessions?.flatMap((session) => session.exercises?.map((exercise) => exercise.exerciseName ?? exercise.name) ?? []),
72
72
  source.prsThisWeek?.map((pr) => pr.exerciseName),
73
- source.stalledExercises?.map((exercise) => exercise.exerciseName),
74
- source.goalProgress?.map((goal) => goal.exerciseName)
73
+ source.stalledExercises?.map((exercise) => exercise.exerciseName)
75
74
  ]);
76
75
  }
77
76
 
@@ -108,8 +107,7 @@ function includedSectionsForSurface(surface, source) {
108
107
  return [
109
108
  'week',
110
109
  hasItems(source.prsThisWeek) ? 'prs' : null,
111
- hasItems(source.stalledExercises) ? 'stalled_exercises' : null,
112
- hasItems(source.goalProgress) ? 'goal_progress' : null
110
+ hasItems(source.stalledExercises) ? 'stalled_exercises' : null
113
111
  ].filter(Boolean);
114
112
  default:
115
113
  return [];
@@ -713,12 +711,6 @@ export function formatCycleContext(ctx) {
713
711
  `Program: ${ctx.programName}, Week ${ctx.cycleNumber}${intentLabel}, ${ctx.totalSessions} session(s).`
714
712
  ];
715
713
 
716
- const phaseLines = formatProgramPhaseContext(ctx.programPhase);
717
- if (phaseLines.length > 0) {
718
- lines.push('');
719
- lines.push(...phaseLines);
720
- }
721
-
722
714
  if (ctx.prioritySignals?.length > 0) {
723
715
  lines.push('');
724
716
  lines.push('Priority signals (ranked):');
@@ -798,14 +790,6 @@ export function formatCycleContext(ctx) {
798
790
  }
799
791
  }
800
792
 
801
- if (ctx.goalProgress) {
802
- lines.push('');
803
- lines.push('Goal progress:');
804
- for (const g of ctx.goalProgress) {
805
- lines.push(` ${g.exerciseName}: ${g.progressPercent ?? '?'}% toward ${g.targetE1RM} e1RM`);
806
- }
807
- }
808
-
809
793
  if (ctx.previousCycles?.length > 0) {
810
794
  lines.push('');
811
795
  lines.push('Previous cycles:');
@@ -892,7 +876,7 @@ The app already shows PRs, total volume, effort score, exercise breakdown, and p
892
876
  Rules:
893
877
  - No bullet points, no questions.
894
878
  - Be specific — use exact exercise names from the session data. Do not shorten or generalize.
895
- - Only mention exercises that appear in the current session, the next session list, or the recorded PR list. Never reference skipped or absent exercises by name.
879
+ - Only mention exercises that appear in the current session, the next session list, the recorded PR list, or the plan comparison block. You may name a skipped exercise from plan comparison if it adds insight (e.g. context for the day's shape), but at most one such mention, and never speculate on why it was skipped unless the context states a reason.
896
880
  - Do not summarize PRs with a count in workout notes. Name the specific lift or lifts instead.
897
881
  - Never use the phrase "rep PR" in a workout note.
898
882
  - Do not state a percentage change unless the exact percentage is directly supported by the comparison block.
@@ -963,10 +947,6 @@ export function formatWorkoutContext(ctx) {
963
947
  const timeOfDay = hour < 12 ? 'morning' : hour < 17 ? 'afternoon' : 'evening';
964
948
  lines.push(`Completed: ${dayNames[d.getUTCDay()]}, ${timeOfDay}.`);
965
949
  }
966
- if (ctx.programWeekNumber) {
967
- const phase = ctx.programProgressionType ? ` (${ctx.programProgressionType})` : '';
968
- lines.push(`Program week: ${ctx.programWeekNumber}${phase}.`);
969
- }
970
950
  if (ctx.sessionsThisWeek) {
971
951
  lines.push(`Sessions this week: ${ctx.sessionsThisWeek}.`);
972
952
  }
@@ -1201,12 +1181,6 @@ export function formatCheckpointContext(ctx) {
1201
1181
  `Program: ${ctx.programName}, Checkpoint at week ${ctx.checkpointWeek} of ${ctx.totalWeeks}.`
1202
1182
  ];
1203
1183
 
1204
- const phaseLines = formatProgramPhaseContext(ctx.programPhase);
1205
- if (phaseLines.length > 0) {
1206
- lines.push('');
1207
- lines.push(...phaseLines);
1208
- }
1209
-
1210
1184
  lines.push('');
1211
1185
  lines.push('Exercise targets:');
1212
1186
  for (const ex of ctx.exercises) {
@@ -1419,10 +1393,6 @@ function formatWeeklyCheckinContext(context) {
1419
1393
  const lines = [];
1420
1394
  lines.push(`Today: ${context.todayIso}`);
1421
1395
  lines.push(`Week range: ${context.weekRangeIso?.start} to ${context.weekRangeIso?.end}`);
1422
- const phaseLines = formatProgramPhaseContext(context.programPhase);
1423
- if (phaseLines.length > 0) {
1424
- lines.push(...phaseLines);
1425
- }
1426
1396
  lines.push(`Sessions this week: ${context.sessionCount}`);
1427
1397
  if (context.adherencePct != null) {
1428
1398
  lines.push(`Adherence: ${context.completedSets}/${context.plannedSets} sets (${context.adherencePct}%)`);
@@ -1446,47 +1416,9 @@ function formatWeeklyCheckinContext(context) {
1446
1416
  const sign = context.bodyweightDeltaKg >= 0 ? '+' : '';
1447
1417
  lines.push(`Bodyweight 7d delta: ${sign}${context.bodyweightDeltaKg}kg`);
1448
1418
  }
1449
- if (Array.isArray(context.goalProgress) && context.goalProgress.length > 0) {
1450
- lines.push('Goal progress:');
1451
- for (const g of context.goalProgress) {
1452
- const deadline = g.finishDate ? ` (finish ${g.finishDate})` : '';
1453
- lines.push(` - ${g.exerciseName}: ${g.progressPercent}% toward ${g.targetE1RM}kg${deadline}`);
1454
- }
1455
- }
1456
1419
  return lines.join('\n');
1457
1420
  }
1458
1421
 
1459
- export function formatProgramPhaseContext(programPhase) {
1460
- if (!programPhase || typeof programPhase !== 'object') return [];
1461
- const current = programPhase.current;
1462
- if (!current?.phase || typeof current.displayWeek !== 'number') return [];
1463
-
1464
- const describe = (phase) => {
1465
- if (!phase?.phase) return null;
1466
- const week = typeof phase.displayWeek === 'number' ? `week ${phase.displayWeek}` : 'week ?';
1467
- return `${week} ${phase.phase}${phase.isDeload ? ' (deload)' : ''}`;
1468
- };
1469
- const describeList = (phases) => {
1470
- if (!Array.isArray(phases) || phases.length === 0) return null;
1471
- return phases.map(describe).filter(Boolean).join(', ');
1472
- };
1473
-
1474
- const lines = ['Program phase:'];
1475
- lines.push(` Current: ${describe(current)}`);
1476
- const previous = describe(programPhase.previousWeek);
1477
- if (previous) lines.push(` Previous: ${previous}`);
1478
- const next = describe(programPhase.nextWeek);
1479
- if (next) lines.push(` Next: ${next}`);
1480
- if (programPhase.isPostDeloadReturn === true) {
1481
- lines.push(' Post-deload return: yes');
1482
- }
1483
- const range = describeList(programPhase.phasesInRange);
1484
- if (range) lines.push(` Range phases: ${range}`);
1485
- const previousRange = describeList(programPhase.previousRangePhases);
1486
- if (previousRange) lines.push(` Previous range phases: ${previousRange}`);
1487
- return lines;
1488
- }
1489
-
1490
1422
  export async function generateWeeklyCheckinRecap(context, { apiKey, model, timeoutMs, priorCommitment, user, sessionId, contextMetadata } = {}) {
1491
1423
  const contextText = formatWeeklyCheckinContext(context);
1492
1424
  const userLines = [fenceContent('training_data', contextText)];