incremnt 0.6.1 → 0.7.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 +4 -1
- package/SKILL.md +60 -0
- package/package.json +4 -2
- package/src/auth.js +1 -5
- package/src/browse.js +7 -1
- package/src/contract.js +84 -17
- package/src/fields.js +41 -0
- package/src/format.js +41 -2
- package/src/lib.js +117 -14
- package/src/mcp.js +61 -5
- package/src/queries.js +87 -6
- package/src/remote.js +208 -15
- package/src/sync-service.js +154 -2
- package/src/validate.js +152 -0
package/src/lib.js
CHANGED
|
@@ -16,6 +16,21 @@ import { createTransport } from './transport.js';
|
|
|
16
16
|
import { formatPretty, formatHelp } from './format.js';
|
|
17
17
|
import { printLogo } from './logo.js';
|
|
18
18
|
import { runBrowseCli } from './browse.js';
|
|
19
|
+
import { ValidationError, validateOptions } from './validate.js';
|
|
20
|
+
import { projectFields } from './fields.js';
|
|
21
|
+
import { buildWriteRequest } from './remote.js';
|
|
22
|
+
|
|
23
|
+
const schemaById = new Map([
|
|
24
|
+
...commandSchema.map((c) => [c.id, c]),
|
|
25
|
+
...writeCommandSchema.map((c) => [c.id, c])
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
function pageAllRecords(payload, commandEntry) {
|
|
29
|
+
if (Array.isArray(payload)) return payload;
|
|
30
|
+
const key = commandEntry?.pageAllKey;
|
|
31
|
+
if (key && Array.isArray(payload?.[key])) return payload[key];
|
|
32
|
+
return [payload];
|
|
33
|
+
}
|
|
19
34
|
|
|
20
35
|
function parseArgs(argv) {
|
|
21
36
|
const commandTokens = [];
|
|
@@ -73,15 +88,7 @@ export async function runCli(argv, stdout, stderr) {
|
|
|
73
88
|
})[command] ?? command;
|
|
74
89
|
|
|
75
90
|
const sessionState = await readSessionState();
|
|
76
|
-
const
|
|
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
|
-
);
|
|
91
|
+
const isAuthenticated = Boolean(sessionState?.session && !isSessionExpired(sessionState.session));
|
|
85
92
|
|
|
86
93
|
if (!command || options.help) {
|
|
87
94
|
await printLogo(stdout);
|
|
@@ -353,9 +360,59 @@ export async function runCli(argv, stdout, stderr) {
|
|
|
353
360
|
const wantJson = options.json || !(stdout.isTTY ?? false);
|
|
354
361
|
const explicitJson = Boolean(options.json);
|
|
355
362
|
|
|
363
|
+
// Reject --dry-run on any command that doesn't declare dryRun support
|
|
364
|
+
// (reads, unknown commands, and writes without dryRun: true). Without this
|
|
365
|
+
// guard, --dry-run is silently dropped and the underlying command runs.
|
|
366
|
+
if (options['dry-run'] && !schemaById.get(normalizedCommand)?.dryRun) {
|
|
367
|
+
const cmdLabel = schemaById.get(normalizedCommand)?.command ?? command ?? normalizedCommand;
|
|
368
|
+
const message = `--dry-run is not supported for ${cmdLabel}.`;
|
|
369
|
+
if (explicitJson) {
|
|
370
|
+
stdout.write(`${JSON.stringify({ error: message, code: 'UNSUPPORTED_DRY_RUN' }, null, 2)}\n`);
|
|
371
|
+
} else {
|
|
372
|
+
stderr.write(`${message}\n`);
|
|
373
|
+
}
|
|
374
|
+
return 1;
|
|
375
|
+
}
|
|
376
|
+
|
|
356
377
|
if (writeCommands.has(normalizedCommand)) {
|
|
378
|
+
let validated;
|
|
379
|
+
try {
|
|
380
|
+
validated = validateOptions(schemaById.get(normalizedCommand), options);
|
|
381
|
+
} catch (error) {
|
|
382
|
+
if (error instanceof ValidationError) {
|
|
383
|
+
if (explicitJson) {
|
|
384
|
+
stdout.write(`${JSON.stringify({ error: error.message, code: error.code, option: error.option }, null, 2)}\n`);
|
|
385
|
+
} else {
|
|
386
|
+
stderr.write(`${error.message}\n`);
|
|
387
|
+
}
|
|
388
|
+
return 1;
|
|
389
|
+
}
|
|
390
|
+
throw error;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const cmdEntry = schemaById.get(normalizedCommand);
|
|
394
|
+
if (options['dry-run']) {
|
|
395
|
+
// Outer guard already verified cmdEntry.dryRun is true here.
|
|
396
|
+
try {
|
|
397
|
+
const request = await buildWriteRequest(normalizedCommand, validated, sessionState);
|
|
398
|
+
stdout.write(`${JSON.stringify({
|
|
399
|
+
dryRun: true,
|
|
400
|
+
command: cmdEntry.command,
|
|
401
|
+
request
|
|
402
|
+
}, null, 2)}\n`);
|
|
403
|
+
return 0;
|
|
404
|
+
} catch (error) {
|
|
405
|
+
if (explicitJson) {
|
|
406
|
+
stdout.write(`${JSON.stringify({ error: error.message, code: error.code ?? null }, null, 2)}\n`);
|
|
407
|
+
} else {
|
|
408
|
+
stderr.write(`${error.message}\n`);
|
|
409
|
+
}
|
|
410
|
+
return 1;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
357
414
|
try {
|
|
358
|
-
const payload = await transport.executeWriteCommand(normalizedCommand,
|
|
415
|
+
const payload = await transport.executeWriteCommand(normalizedCommand, validated);
|
|
359
416
|
if (wantJson) {
|
|
360
417
|
stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
|
|
361
418
|
} else {
|
|
@@ -386,13 +443,59 @@ export async function runCli(argv, stdout, stderr) {
|
|
|
386
443
|
return 1;
|
|
387
444
|
}
|
|
388
445
|
|
|
446
|
+
let validatedRead;
|
|
389
447
|
try {
|
|
390
|
-
|
|
448
|
+
validatedRead = validateOptions(schemaById.get(normalizedCommand), options);
|
|
449
|
+
} catch (error) {
|
|
450
|
+
if (error instanceof ValidationError) {
|
|
451
|
+
if (explicitJson) {
|
|
452
|
+
stdout.write(`${JSON.stringify({ error: error.message, code: error.code, option: error.option }, null, 2)}\n`);
|
|
453
|
+
} else {
|
|
454
|
+
stderr.write(`${error.message}\n`);
|
|
455
|
+
}
|
|
456
|
+
return 1;
|
|
457
|
+
}
|
|
458
|
+
throw error;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const readCmdEntry = schemaById.get(normalizedCommand);
|
|
462
|
+
|
|
463
|
+
if (options.fields !== undefined && !readCmdEntry?.supportsFields) {
|
|
464
|
+
const message = `--fields is not supported for ${readCmdEntry?.command ?? normalizedCommand}.`;
|
|
465
|
+
if (explicitJson) {
|
|
466
|
+
stdout.write(`${JSON.stringify({ error: message, code: 'UNSUPPORTED_FIELDS' }, null, 2)}\n`);
|
|
467
|
+
} else {
|
|
468
|
+
stderr.write(`${message}\n`);
|
|
469
|
+
}
|
|
470
|
+
return 1;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
if (options['page-all'] && !readCmdEntry?.supportsPageAll) {
|
|
474
|
+
const message = `--page-all is not supported for ${readCmdEntry?.command ?? normalizedCommand}.`;
|
|
475
|
+
if (explicitJson) {
|
|
476
|
+
stdout.write(`${JSON.stringify({ error: message, code: 'UNSUPPORTED_PAGE_ALL' }, null, 2)}\n`);
|
|
477
|
+
} else {
|
|
478
|
+
stderr.write(`${message}\n`);
|
|
479
|
+
}
|
|
480
|
+
return 1;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
try {
|
|
484
|
+
const payload = await transport.executeReadCommand(normalizedCommand, validatedRead);
|
|
485
|
+
const projected = options.fields !== undefined ? projectFields(payload, options.fields) : payload;
|
|
486
|
+
|
|
487
|
+
if (options['page-all'] && readCmdEntry?.supportsPageAll) {
|
|
488
|
+
for (const record of pageAllRecords(projected, readCmdEntry)) {
|
|
489
|
+
stdout.write(`${JSON.stringify(record)}\n`);
|
|
490
|
+
}
|
|
491
|
+
return 0;
|
|
492
|
+
}
|
|
493
|
+
|
|
391
494
|
if (wantJson) {
|
|
392
|
-
stdout.write(`${JSON.stringify(
|
|
495
|
+
stdout.write(`${JSON.stringify(projected, null, 2)}\n`);
|
|
393
496
|
} else {
|
|
394
|
-
const pretty = formatPretty(normalizedCommand,
|
|
395
|
-
stdout.write(`${pretty ?? JSON.stringify(
|
|
497
|
+
const pretty = formatPretty(normalizedCommand, projected);
|
|
498
|
+
stdout.write(`${pretty ?? JSON.stringify(projected, null, 2)}\n`);
|
|
396
499
|
}
|
|
397
500
|
|
|
398
501
|
return 0;
|
package/src/mcp.js
CHANGED
|
@@ -11,6 +11,9 @@ import { commandSchema, writeCommands, writeCommandSchema } from './contract.js'
|
|
|
11
11
|
import { listCoachReadTools } from './queries.js';
|
|
12
12
|
import { readSessionState } from './state.js';
|
|
13
13
|
import { createTransport } from './transport.js';
|
|
14
|
+
import { ValidationError, validateOptions } from './validate.js';
|
|
15
|
+
import { projectFields } from './fields.js';
|
|
16
|
+
import { buildWriteRequest } from './remote.js';
|
|
14
17
|
|
|
15
18
|
const globalScope = Function('return this')();
|
|
16
19
|
|
|
@@ -24,6 +27,14 @@ function commandShape(cmd) {
|
|
|
24
27
|
shape[opt.name] = field;
|
|
25
28
|
}
|
|
26
29
|
|
|
30
|
+
if (cmd.supportsFields) {
|
|
31
|
+
shape.fields = z.string().optional().describe('Comma-separated top-level keys to keep in the response. Cuts payload size for context-bound agents.');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (cmd.dryRun) {
|
|
35
|
+
shape['dry-run'] = z.boolean().optional().describe('Preview the request without sending. Returns { dryRun: true, command, request }.');
|
|
36
|
+
}
|
|
37
|
+
|
|
27
38
|
return shape;
|
|
28
39
|
}
|
|
29
40
|
|
|
@@ -57,9 +68,43 @@ export function registerMcpTools(server, {
|
|
|
57
68
|
readSessionStateFn = readSessionState,
|
|
58
69
|
createTransportFn = createTransport
|
|
59
70
|
} = {}) {
|
|
60
|
-
for (const cmd of [...commandSchema, ...writeCommandSchema]) {
|
|
61
|
-
|
|
71
|
+
for (const cmd of [...commandSchema, ...writeCommandSchema].filter((entry) => entry.mcp !== false)) {
|
|
72
|
+
const description = cmd.agentNotes
|
|
73
|
+
? `${cmd.description}\n\nNotes for agents:\n${cmd.agentNotes}`
|
|
74
|
+
: cmd.description;
|
|
75
|
+
server.tool(cmd.id, description, commandShape(cmd), async (args, extra) => {
|
|
62
76
|
try {
|
|
77
|
+
// Reject --dry-run on tools that don't support it. Zod strips unknown
|
|
78
|
+
// fields silently, so without this guard a caller passing dry-run on
|
|
79
|
+
// an unsupported tool would execute the real mutation. Check the raw
|
|
80
|
+
// request when available; fall back to args.
|
|
81
|
+
const rawDryRun = extra?.request?.params?.arguments?.['dry-run'] ?? args?.['dry-run'];
|
|
82
|
+
if (rawDryRun && !cmd.dryRun) {
|
|
83
|
+
return {
|
|
84
|
+
content: [{
|
|
85
|
+
type: 'text',
|
|
86
|
+
text: JSON.stringify({ error: `--dry-run is not supported for ${cmd.command}.`, code: 'UNSUPPORTED_DRY_RUN' }, null, 2)
|
|
87
|
+
}],
|
|
88
|
+
isError: true
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
let validated;
|
|
93
|
+
try {
|
|
94
|
+
validated = validateOptions(cmd, args);
|
|
95
|
+
} catch (error) {
|
|
96
|
+
if (error instanceof ValidationError) {
|
|
97
|
+
return {
|
|
98
|
+
content: [{
|
|
99
|
+
type: 'text',
|
|
100
|
+
text: JSON.stringify({ error: error.message, code: error.code, option: error.option }, null, 2)
|
|
101
|
+
}],
|
|
102
|
+
isError: true
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
throw error;
|
|
106
|
+
}
|
|
107
|
+
|
|
63
108
|
const sessionState = await readSessionStateFn();
|
|
64
109
|
const transport = await createTransportFn({}, sessionState);
|
|
65
110
|
|
|
@@ -70,12 +115,23 @@ export function registerMcpTools(server, {
|
|
|
70
115
|
};
|
|
71
116
|
}
|
|
72
117
|
|
|
118
|
+
if (cmd.dryRun && validated['dry-run']) {
|
|
119
|
+
const request = await buildWriteRequest(cmd.id, validated, sessionState);
|
|
120
|
+
return {
|
|
121
|
+
content: [{ type: 'text', text: JSON.stringify({ dryRun: true, command: cmd.command, request }, null, 2) }]
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
73
125
|
const result = writeCommands.has(cmd.id)
|
|
74
|
-
? await transport.executeWriteCommand(cmd.id,
|
|
75
|
-
: await transport.executeReadCommand(cmd.id,
|
|
126
|
+
? await transport.executeWriteCommand(cmd.id, validated)
|
|
127
|
+
: await transport.executeReadCommand(cmd.id, validated);
|
|
128
|
+
|
|
129
|
+
const projected = cmd.supportsFields && validated.fields !== undefined
|
|
130
|
+
? projectFields(result, validated.fields)
|
|
131
|
+
: result;
|
|
76
132
|
|
|
77
133
|
return {
|
|
78
|
-
content: [{ type: 'text', text: JSON.stringify(
|
|
134
|
+
content: [{ type: 'text', text: JSON.stringify(projected, null, 2) }]
|
|
79
135
|
};
|
|
80
136
|
} catch (error) {
|
|
81
137
|
const message = error && error.message ? error.message : String(error);
|
package/src/queries.js
CHANGED
|
@@ -3610,7 +3610,42 @@ function askToolMetadata(tools = [], provenance = []) {
|
|
|
3610
3610
|
};
|
|
3611
3611
|
}
|
|
3612
3612
|
|
|
3613
|
-
|
|
3613
|
+
function appendCoachObservationsContextBeforeExcludeNote(lines, observations, exclude = new Set()) {
|
|
3614
|
+
if (exclude.has('coach_observations')) return [];
|
|
3615
|
+
const usable = (Array.isArray(observations) ? observations : [])
|
|
3616
|
+
.filter((observation) => observation?.id && observation?.summary)
|
|
3617
|
+
.slice(0, 3);
|
|
3618
|
+
if (usable.length === 0) return [];
|
|
3619
|
+
|
|
3620
|
+
const note = buildExcludeNote(exclude);
|
|
3621
|
+
const noteAtEnd = note && lines.at(-1) === note;
|
|
3622
|
+
if (noteAtEnd) {
|
|
3623
|
+
lines.pop();
|
|
3624
|
+
if (lines.at(-1) === '') lines.pop();
|
|
3625
|
+
}
|
|
3626
|
+
const section = [
|
|
3627
|
+
'',
|
|
3628
|
+
'Coach observations (derived from training data, not user-stated facts):'
|
|
3629
|
+
];
|
|
3630
|
+
for (const observation of usable) {
|
|
3631
|
+
const parts = [
|
|
3632
|
+
`[${observation.kind ?? 'observation'}] ${observation.summary}`,
|
|
3633
|
+
observation.actionText ? `Action: ${observation.actionText}` : null,
|
|
3634
|
+
observation.sourceComponent ? `component=${observation.sourceComponent}` : null,
|
|
3635
|
+
`confidence=${Number(observation.confidence ?? 0).toFixed(2)}`,
|
|
3636
|
+
`observation-id=${observation.id}`
|
|
3637
|
+
].filter(Boolean);
|
|
3638
|
+
section.push(` ${parts.join(' ')}`);
|
|
3639
|
+
}
|
|
3640
|
+
lines.push(...section);
|
|
3641
|
+
if (noteAtEnd) {
|
|
3642
|
+
lines.push('');
|
|
3643
|
+
lines.push(note);
|
|
3644
|
+
}
|
|
3645
|
+
return usable.map((observation) => observation.id);
|
|
3646
|
+
}
|
|
3647
|
+
|
|
3648
|
+
export function askRoutedContext(snapshot, question, { exclude = new Set(), coachFacts = null, coachObservations = null } = {}) {
|
|
3614
3649
|
const { route, namedExercises } = routeAskQuestion(snapshot, question);
|
|
3615
3650
|
let effectiveRoute = route;
|
|
3616
3651
|
let fallbackRoute = null;
|
|
@@ -3657,6 +3692,7 @@ export function askRoutedContext(snapshot, question, { exclude = new Set(), coac
|
|
|
3657
3692
|
const factLines = built.context.split('\n');
|
|
3658
3693
|
const includedFacts = rankedCoachFactsForAsk(snapshot, question, effectiveRoute, { facts: coachFacts });
|
|
3659
3694
|
const includedCoachFactIds = appendCoachFactsContextBeforeExcludeNote(factLines, includedFacts, exclude);
|
|
3695
|
+
const includedCoachObservationIds = appendCoachObservationsContextBeforeExcludeNote(factLines, coachObservations, exclude);
|
|
3660
3696
|
const includedCoachFactKinds = uniqueArray(includedFacts.map((fact) => fact.kind));
|
|
3661
3697
|
const includedCoachFactSources = uniqueArray(includedFacts.map((fact) => {
|
|
3662
3698
|
const sourceSessionId = String(fact.sourceSessionId ?? '');
|
|
@@ -3666,7 +3702,11 @@ export function askRoutedContext(snapshot, question, { exclude = new Set(), coac
|
|
|
3666
3702
|
}).filter(Boolean));
|
|
3667
3703
|
built = {
|
|
3668
3704
|
context: factLines.join('\n'),
|
|
3669
|
-
sections:
|
|
3705
|
+
sections: [
|
|
3706
|
+
...built.sections,
|
|
3707
|
+
...(includedFacts.length > 0 ? ['coach_facts'] : []),
|
|
3708
|
+
...(includedCoachObservationIds.length > 0 ? ['coach_observations'] : [])
|
|
3709
|
+
]
|
|
3670
3710
|
};
|
|
3671
3711
|
|
|
3672
3712
|
return {
|
|
@@ -3683,6 +3723,8 @@ export function askRoutedContext(snapshot, question, { exclude = new Set(), coac
|
|
|
3683
3723
|
coachFactIds: includedCoachFactIds,
|
|
3684
3724
|
coachFactKinds: includedCoachFactKinds,
|
|
3685
3725
|
coachFactSources: includedCoachFactSources,
|
|
3726
|
+
includedCoachObservationIds,
|
|
3727
|
+
coachObservationIds: includedCoachObservationIds,
|
|
3686
3728
|
contextCharCount: built.context.length,
|
|
3687
3729
|
...toolMetadata
|
|
3688
3730
|
}
|
|
@@ -4071,12 +4113,51 @@ export function healthSummary(snapshot, days = 14) {
|
|
|
4071
4113
|
nights: recentSleep.length
|
|
4072
4114
|
},
|
|
4073
4115
|
bodyWeight: (() => {
|
|
4074
|
-
const
|
|
4075
|
-
|
|
4116
|
+
const profileWeightKg = Number(snapshot.user?.weightKg);
|
|
4117
|
+
const resolvedProfileWeightKg = Number.isFinite(profileWeightKg) && profileWeightKg > 0
|
|
4118
|
+
? Math.round(profileWeightKg * 10) / 10
|
|
4119
|
+
: null;
|
|
4120
|
+
const rows = (metrics.bodyWeight ?? [])
|
|
4121
|
+
.filter((m) => m?.date && Number.isFinite(Number(m.value ?? m.weight)))
|
|
4122
|
+
.map((m) => ({
|
|
4123
|
+
date: String(m.date).slice(0, 10),
|
|
4124
|
+
value: Number(m.value ?? m.weight)
|
|
4125
|
+
}))
|
|
4126
|
+
.sort((a, b) => a.date.localeCompare(b.date));
|
|
4127
|
+
const recent = rows.filter((m) => m.date >= cutoff);
|
|
4128
|
+
if (recent.length === 0 && resolvedProfileWeightKg != null) {
|
|
4129
|
+
return {
|
|
4130
|
+
latest: { value: resolvedProfileWeightKg, date: null, source: 'profile', stale: false },
|
|
4131
|
+
trend: null,
|
|
4132
|
+
readings: 0,
|
|
4133
|
+
totalReadings: rows.length
|
|
4134
|
+
};
|
|
4135
|
+
}
|
|
4136
|
+
if (recent.length === 0) {
|
|
4137
|
+
const latestKnown = rows.at(-1);
|
|
4138
|
+
if (!latestKnown) return { latest: null, readings: 0, totalReadings: 0 };
|
|
4139
|
+
return {
|
|
4140
|
+
latest: {
|
|
4141
|
+
value: Math.round(latestKnown.value * 10) / 10,
|
|
4142
|
+
date: latestKnown.date,
|
|
4143
|
+
source: 'healthkit',
|
|
4144
|
+
stale: true
|
|
4145
|
+
},
|
|
4146
|
+
trend: null,
|
|
4147
|
+
readings: 0,
|
|
4148
|
+
totalReadings: rows.length
|
|
4149
|
+
};
|
|
4150
|
+
}
|
|
4076
4151
|
return {
|
|
4077
|
-
latest: {
|
|
4152
|
+
latest: {
|
|
4153
|
+
value: Math.round(recent.at(-1).value * 10) / 10,
|
|
4154
|
+
date: recent.at(-1).date,
|
|
4155
|
+
source: 'healthkit',
|
|
4156
|
+
stale: false
|
|
4157
|
+
},
|
|
4078
4158
|
trend: Math.round((recent.at(-1).value - recent[0].value) * 10) / 10,
|
|
4079
|
-
readings: recent.length
|
|
4159
|
+
readings: recent.length,
|
|
4160
|
+
totalReadings: rows.length
|
|
4080
4161
|
};
|
|
4081
4162
|
})(),
|
|
4082
4163
|
respiratoryRate: (() => {
|