incremnt 0.1.18 → 0.2.0

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/src/contract.js CHANGED
@@ -1,4 +1,4 @@
1
- export const contractVersion = 4;
1
+ export const contractVersion = 5;
2
2
 
3
3
  export const capabilities = {
4
4
  readOnly: false,
@@ -225,6 +225,8 @@ export const officialCommands = [
225
225
  ...writeCommandSchema.map((c) => c.usage ?? c.command),
226
226
  'status',
227
227
  'contract',
228
+ 'browse',
229
+ 'tui',
228
230
  'login',
229
231
  'login --base-url <base-url>',
230
232
  'login --snapshot <snapshot-file>',
package/src/format.js CHANGED
@@ -221,6 +221,133 @@ function formatProgramList(payload) {
221
221
  return lines.join('\n');
222
222
  }
223
223
 
224
+ function formatGoalsShow(payload) {
225
+ if (!payload) {
226
+ return 'Goal plan not found.';
227
+ }
228
+
229
+ const lines = [` ${chalk.bold('GOAL PLAN')}${dimDot()}${payload.status ?? 'unknown'}`, ''];
230
+
231
+ lines.push(keyValue('Plan', payload.planId));
232
+ if (payload.programId) {
233
+ lines.push(keyValue('Program', payload.programId));
234
+ }
235
+ lines.push(keyValue('Level', String(payload.strengthLevel ?? 'unknown')));
236
+ lines.push(keyValue('Duration', `${payload.durationWeeks ?? '?'} weeks`));
237
+
238
+ if (payload.startDate) {
239
+ lines.push(keyValue('Start', formatShortDate(payload.startDate)));
240
+ }
241
+
242
+ if (payload.finishDate) {
243
+ lines.push(keyValue('Finish', formatShortDate(payload.finishDate)));
244
+ }
245
+
246
+ lines.push('');
247
+ lines.push(` ${chalk.bold('Lift goals')}`);
248
+
249
+ for (const goal of payload.liftGoals ?? []) {
250
+ const progress = goal.progressPercent != null ? ` ${chalk.dim(`(${goal.progressPercent}%)`)}` : '';
251
+ const current = goal.currentBestE1RM != null ? `${goal.currentBestE1RM} kg` : 'n/a';
252
+ const target = goal.targetE1RM != null ? `${goal.targetE1RM} kg` : 'n/a';
253
+ const weight = goal.currentBestWeight != null ? `${goal.currentBestWeight} kg` : 'n/a';
254
+ const reps = goal.currentBestReps != null ? `${goal.currentBestReps} reps` : 'n/a';
255
+ const estimated = `best ${weight} x ${reps} → ${current}`;
256
+ lines.push(` ${chalk.bold(goal.exerciseName)}${progress}`);
257
+ lines.push(` ${chalk.dim(estimated)}${dimDot()}${chalk.dim(`target ${target}`)}`);
258
+ }
259
+
260
+ if ((payload.liftGoals ?? []).length === 0) {
261
+ lines.push(` ${chalk.dim('No lift goals found.')}`);
262
+ }
263
+
264
+ return lines.join('\n');
265
+ }
266
+
267
+ function formatTrainingLoad(payload) {
268
+ if (!payload?.available) {
269
+ return 'No training load data found.';
270
+ }
271
+
272
+ const lines = [` ${chalk.bold('TRAINING LOAD')}${dimDot()}${payload.status}`, ''];
273
+
274
+ lines.push(keyValue('Coverage', `${payload.coverageDays} days`));
275
+ lines.push(keyValue('Baseline', payload.baselineEstablished ? 'established' : 'building'));
276
+ lines.push(keyValue('7-day avg', String(payload.last7Days?.avgPerDay ?? 0)));
277
+ lines.push(keyValue('28-day avg', String(payload.last28Days?.avgPerDay ?? 0)));
278
+
279
+ if (payload.readiness) {
280
+ lines.push(keyValue('Readiness', `ATL ${payload.readiness.atl} / CTL ${payload.readiness.ctl} / TSB ${payload.readiness.tsb}`));
281
+ }
282
+
283
+ lines.push('');
284
+ lines.push(` ${payload.statusDescription ?? ''}`.trimEnd());
285
+
286
+ if (payload.byType && Object.keys(payload.byType).length > 0) {
287
+ lines.push('');
288
+ lines.push(` ${chalk.bold('By type')}`);
289
+ for (const [type, stats] of Object.entries(payload.byType)) {
290
+ lines.push(` ${type.padEnd(12)} ${chalk.dim(`${stats.count} workouts, ${Math.round(stats.totalEffort)} load, ${stats.totalDurationMins} min`)}`);
291
+ }
292
+ }
293
+
294
+ if (payload.recentWorkouts?.length > 0) {
295
+ lines.push('');
296
+ lines.push(` ${chalk.bold('Recent workouts')}`);
297
+ for (const workout of payload.recentWorkouts.slice(0, 6)) {
298
+ const duration = workout.durationMins != null ? ` ${workout.durationMins} min` : '';
299
+ const hr = workout.avgHR != null ? ` ${workout.avgHR} bpm` : '';
300
+ lines.push(` ${formatShortDate(workout.date).padEnd(8)} ${chalk.bold(workout.type)}${dimDot()}${chalk.dim(`effort ${workout.estimatedEffort ?? '?'}`)}${duration}${hr}`);
301
+ }
302
+ }
303
+
304
+ return lines.join('\n');
305
+ }
306
+
307
+ function formatHealthSummary(payload) {
308
+ if (!payload?.available) {
309
+ return 'No health data found.';
310
+ }
311
+
312
+ const lines = [` ${chalk.bold('HEALTH SUMMARY')}${dimDot()}last ${payload.days ?? 14} days`, ''];
313
+
314
+ if (payload.restingHR?.avg != null) {
315
+ lines.push(keyValue('Resting HR', `${payload.restingHR.avg} bpm`));
316
+ }
317
+
318
+ if (payload.hrv?.avg != null) {
319
+ lines.push(keyValue('HRV', `${payload.hrv.avg} ms`));
320
+ }
321
+
322
+ if (payload.vo2Max?.latest?.value != null) {
323
+ lines.push(keyValue('VO2 max', `${payload.vo2Max.latest.value}`));
324
+ }
325
+
326
+ if (payload.sleep?.avgHours != null) {
327
+ lines.push(keyValue('Sleep', `${payload.sleep.avgHours} h`));
328
+ }
329
+
330
+ if (payload.bodyWeight?.latest?.value != null) {
331
+ lines.push(keyValue('Body weight', `${payload.bodyWeight.latest.value} kg`));
332
+ }
333
+
334
+ if (payload.respiratoryRate?.avg != null) {
335
+ lines.push(keyValue('Resp. rate', `${payload.respiratoryRate.avg}`));
336
+ }
337
+
338
+ if (payload.bodyTemperature?.avg != null) {
339
+ lines.push(keyValue('Temp', `${payload.bodyTemperature.avg}`));
340
+ }
341
+
342
+ if (payload.trainingLoad?.available) {
343
+ lines.push('');
344
+ lines.push(` ${chalk.bold('Training load')}`);
345
+ lines.push(` ${chalk.dim(payload.trainingLoad.statusDescription ?? 'No status available.')}`);
346
+ }
347
+
348
+ return lines.join('\n');
349
+ }
350
+
224
351
  function formatProgramDetail(payload) {
225
352
  if (!payload) {
226
353
  return 'Program not found.';
@@ -538,6 +665,8 @@ export function formatHelp(opts = {}) {
538
665
  cmd('logout', 'Clear session'),
539
666
  '',
540
667
  header('OTHER'),
668
+ cmd('browse', 'Interactive Ink browser'),
669
+ cmd('tui', 'Alias for browse'),
541
670
  cmd('status', 'Connection & auth info'),
542
671
  cmd('contract', 'API contract info'),
543
672
  cmd('--json', 'Force JSON output'),
@@ -561,8 +690,11 @@ export function formatPretty(command, payload) {
561
690
  'program-summary': formatProgramSummary,
562
691
  'program-list': formatProgramList,
563
692
  'program-detail': formatProgramDetail,
693
+ 'goals-show': formatGoalsShow,
564
694
  'planned-vs-actual': formatPlannedVsActual,
565
695
  'why-did-this-change': formatWhyDidThisChange,
696
+ 'training-load': formatTrainingLoad,
697
+ 'health-summary': formatHealthSummary,
566
698
  'cycle-summary-list': formatCycleSummaryList,
567
699
  'cycle-summary-show': formatCycleSummaryShow,
568
700
  'ask-history': formatAskHistory,
package/src/lib.js CHANGED
@@ -15,6 +15,7 @@ import { clearSessionState, isSessionExpired, readSessionState, resolveConfigDir
15
15
  import { createTransport } from './transport.js';
16
16
  import { formatPretty, formatHelp } from './format.js';
17
17
  import { printLogo } from './logo.js';
18
+ import { runBrowseCli } from './browse.js';
18
19
 
19
20
  function parseArgs(argv) {
20
21
  const commandTokens = [];
@@ -66,7 +67,9 @@ export async function runCli(argv, stdout, stderr) {
66
67
  explain: 'why-did-this-change',
67
68
  propose: 'programs-propose',
68
69
  proposals: 'programs-proposals',
69
- dismiss: 'proposal-dismiss'
70
+ dismiss: 'proposal-dismiss',
71
+ browse: 'browse',
72
+ tui: 'browse'
70
73
  })[command] ?? command;
71
74
 
72
75
  const sessionState = await readSessionState();
@@ -325,6 +328,15 @@ export async function runCli(argv, stdout, stderr) {
325
328
  return 1;
326
329
  }
327
330
 
331
+ if (normalizedCommand === 'browse') {
332
+ if (!(stdout.isTTY ?? false) || !(process.stdin.isTTY ?? false)) {
333
+ stderr.write('browse requires an interactive TTY.\n');
334
+ return 1;
335
+ }
336
+
337
+ return runBrowseCli({ transport, stdout, stderr, options });
338
+ }
339
+
328
340
  const wantJson = options.json || !(stdout.isTTY ?? false);
329
341
  const explicitJson = Boolean(options.json);
330
342
 
package/src/logo.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import chalk from 'chalk';
2
+ import process from 'node:process';
2
3
 
3
4
  const startColor = { r: 0, g: 255, b: 163 }; // #00ffa3
4
5
  const endColor = { r: 59, g: 130, b: 246 }; // #3b82f6
@@ -30,46 +31,61 @@ const lineLength = rawLines[0].length + SHADOW_OFFSET_X;
30
31
 
31
32
  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
32
33
 
33
- export async function printLogo(stdout = process.stdout) {
34
+ function buildLogoLine(row, gradientStartIdx) {
35
+ let lineStr = '';
36
+
37
+ for (let c = 0; c < lineLength; c++) {
38
+ const fgY = row;
39
+ const fgX = c;
40
+ const hasFg = (fgY >= 0 && fgY < 5 && fgX >= 0 && fgX < rawLines[0].length && rawLines[fgY][fgX] === '#');
41
+
42
+ const bgY = row - SHADOW_OFFSET_Y;
43
+ const bgX = c - SHADOW_OFFSET_X;
44
+ const hasBg = (bgY >= 0 && bgY < 5 && bgX >= 0 && bgX < rawLines[0].length && rawLines[bgY][bgX] === '#');
45
+
46
+ if (hasFg) {
47
+ if (fgX < gradientStartIdx) {
48
+ lineStr += chalk.white('█');
49
+ } else {
50
+ const factor = Math.max(0, Math.min(1, (fgX - gradientStartIdx) / (rawLines[0].length - gradientStartIdx - 2)));
51
+ const col = interpolateColor(startColor, endColor, factor);
52
+ lineStr += chalk.rgb(col.r, col.g, col.b)('█');
53
+ }
54
+ } else if (hasBg) {
55
+ const shadowFactor = 0.35;
56
+ if (bgX < gradientStartIdx) {
57
+ lineStr += chalk.rgb(Math.round(255 * shadowFactor), Math.round(255 * shadowFactor), Math.round(255 * shadowFactor))('█');
58
+ } else {
59
+ const factor = Math.max(0, Math.min(1, (bgX - gradientStartIdx) / (rawLines[0].length - gradientStartIdx - 2)));
60
+ const col = interpolateColor(startColor, endColor, factor);
61
+ lineStr += chalk.rgb(Math.round(col.r * shadowFactor), Math.round(col.g * shadowFactor), Math.round(col.b * shadowFactor))('█');
62
+ }
63
+ } else {
64
+ lineStr += ' ';
65
+ }
66
+ }
67
+
68
+ return lineStr;
69
+ }
70
+
71
+ export function getLogoLines() {
34
72
  // Gradient starts at the 'M' character position
35
73
  // I: 0, N: 3, C: 14, R: 21, E: 29, M: 37
36
74
  const gradientStartIdx = 37;
75
+ const lines = [];
37
76
 
38
- stdout.write('\n');
39
77
  for (let r = 0; r < totalLines; r++) {
40
- let lineStr = '';
41
-
42
- for (let c = 0; c < lineLength; c++) {
43
- const fgY = r;
44
- const fgX = c;
45
- const hasFg = (fgY >= 0 && fgY < 5 && fgX >= 0 && fgX < rawLines[0].length && rawLines[fgY][fgX] === '#');
78
+ lines.push(buildLogoLine(r, gradientStartIdx));
79
+ }
46
80
 
47
- const bgY = r - SHADOW_OFFSET_Y;
48
- const bgX = c - SHADOW_OFFSET_X;
49
- const hasBg = (bgY >= 0 && bgY < 5 && bgX >= 0 && bgX < rawLines[0].length && rawLines[bgY][bgX] === '#');
81
+ return lines;
82
+ }
50
83
 
51
- if (hasFg) {
52
- if (fgX < gradientStartIdx) {
53
- lineStr += chalk.white('');
54
- } else {
55
- const factor = Math.max(0, Math.min(1, (fgX - gradientStartIdx) / (rawLines[0].length - gradientStartIdx - 2)));
56
- const col = interpolateColor(startColor, endColor, factor);
57
- lineStr += chalk.rgb(col.r, col.g, col.b)('█');
58
- }
59
- } else if (hasBg) {
60
- const shadowFactor = 0.35;
61
- if (bgX < gradientStartIdx) {
62
- lineStr += chalk.rgb(Math.round(255 * shadowFactor), Math.round(255 * shadowFactor), Math.round(255 * shadowFactor))('█');
63
- } else {
64
- const factor = Math.max(0, Math.min(1, (bgX - gradientStartIdx) / (rawLines[0].length - gradientStartIdx - 2)));
65
- const col = interpolateColor(startColor, endColor, factor);
66
- lineStr += chalk.rgb(Math.round(col.r * shadowFactor), Math.round(col.g * shadowFactor), Math.round(col.b * shadowFactor))('█');
67
- }
68
- } else {
69
- lineStr += ' ';
70
- }
71
- }
72
- stdout.write(lineStr + '\n');
84
+ export async function printLogo(stdout = process.stdout) {
85
+ const lines = getLogoLines();
86
+ stdout.write('\n');
87
+ for (const line of lines) {
88
+ stdout.write(`${line}\n`);
73
89
  await sleep(80);
74
90
  }
75
91
  stdout.write('\n');
package/src/mcp.js CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  import fs from 'node:fs';
4
4
  import path from 'node:path';
5
+ import process from 'node:process';
5
6
  import { fileURLToPath } from 'node:url';
6
7
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
7
8
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
@@ -10,6 +11,8 @@ import { commandSchema, writeCommands, writeCommandSchema } from './contract.js'
10
11
  import { readSessionState } from './state.js';
11
12
  import { createTransport } from './transport.js';
12
13
 
14
+ const globalScope = Function('return this')();
15
+
13
16
  function commandShape(cmd) {
14
17
  const shape = {};
15
18
 
@@ -48,9 +51,9 @@ export function registerMcpTools(server, {
48
51
  content: [{ type: 'text', text: JSON.stringify(result, null, 2) }]
49
52
  };
50
53
  } catch (error) {
51
- const message = error?.message ?? String(error);
54
+ const message = error && error.message ? error.message : String(error);
52
55
 
53
- if (error?.code === 'SNAPSHOT_NOT_FOUND') {
56
+ if (error && error.code === 'SNAPSHOT_NOT_FOUND') {
54
57
  return {
55
58
  content: [{ type: 'text', text: 'Not logged in. Run `incremnt login` first.' }],
56
59
  isError: true
@@ -77,12 +80,42 @@ export function createMcpServer(deps) {
77
80
  return registerMcpTools(server, deps);
78
81
  }
79
82
 
83
+ export function createSandboxServer() {
84
+ const sandboxTransport = {
85
+ expired: false,
86
+ executeReadCommand: async (commandId) => ({
87
+ commandId,
88
+ sandbox: true,
89
+ ok: true
90
+ }),
91
+ executeWriteCommand: async (commandId) => ({
92
+ commandId,
93
+ sandbox: true,
94
+ ok: true
95
+ })
96
+ };
97
+
98
+ return createMcpServer({
99
+ readSessionStateFn: async () => ({}),
100
+ createTransportFn: async () => sandboxTransport
101
+ });
102
+ }
103
+
80
104
  async function main() {
81
105
  const transport = new StdioServerTransport();
82
106
  const server = createMcpServer();
83
107
  await server.connect(transport);
84
108
  }
85
109
 
86
- if (process.argv[1] && fs.realpathSync(path.resolve(process.argv[1])) === fileURLToPath(import.meta.url)) {
87
- await main();
110
+ const isDirectInvocation = process.argv[1]
111
+ && (
112
+ (typeof globalScope.__filename !== 'undefined' && fs.realpathSync(path.resolve(process.argv[1])) === globalScope.__filename)
113
+ || (typeof globalScope.__filename === 'undefined' && fs.realpathSync(path.resolve(process.argv[1])) === fileURLToPath(import.meta.url))
114
+ );
115
+
116
+ if (isDirectInvocation) {
117
+ void main().catch((error) => {
118
+ console.error(error);
119
+ process.exit(1);
120
+ });
88
121
  }