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/README.md +63 -9
- package/package.json +3 -1
- package/src/browse.js +1103 -0
- package/src/contract.js +3 -1
- package/src/format.js +132 -0
- package/src/lib.js +13 -1
- package/src/logo.js +49 -33
- package/src/mcp.js +37 -4
- package/src/openrouter.js +241 -68
- package/src/prompt-security.js +70 -0
- package/src/queries.js +396 -142
- package/src/sync-service.js +2163 -56
package/src/contract.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export const contractVersion =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
const hasBg = (bgY >= 0 && bgY < 5 && bgX >= 0 && bgX < rawLines[0].length && rawLines[bgY][bgX] === '#');
|
|
81
|
+
return lines;
|
|
82
|
+
}
|
|
50
83
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
|
54
|
+
const message = error && error.message ? error.message : String(error);
|
|
52
55
|
|
|
53
|
-
if (error
|
|
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
|
-
|
|
87
|
-
|
|
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
|
}
|