incremnt 0.8.2 → 0.8.4
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/package.json +1 -1
- package/src/ask-coach.js +364 -56
- package/src/openrouter.js +6 -4
- package/src/prompt-changelog.js +18 -0
- package/src/queries.js +429 -3
- package/src/sync-service.js +190 -50
package/src/sync-service.js
CHANGED
|
@@ -28,6 +28,7 @@ import { extractPlanChangeset } from './plan-changeset.js';
|
|
|
28
28
|
|
|
29
29
|
const MAX_BODY_BYTES = 10 * 1024 * 1024; // 10 MB
|
|
30
30
|
const DEFAULT_RATE_LIMIT_WINDOW_MS = 60_000;
|
|
31
|
+
const COACH_OBSERVATION_DAILY_REFRESH_COOLDOWN_MS = 20 * 60 * 60 * 1000;
|
|
31
32
|
const WEEKLY_CHECKIN_COMPLETION_USER_TURNS = 3;
|
|
32
33
|
const WEEKLY_CHECKIN_RECAP_LOCAL_HOUR = 16;
|
|
33
34
|
const DEFAULT_RATE_LIMIT_RULES = {
|
|
@@ -70,6 +71,8 @@ const DEFAULT_RATE_LIMIT_RULES = {
|
|
|
70
71
|
'program-share-list': 60,
|
|
71
72
|
'program-share-public': 120,
|
|
72
73
|
'program-share-revoke': 30,
|
|
74
|
+
'workout-share-create': 30,
|
|
75
|
+
'workout-share-public': 120,
|
|
73
76
|
'mobile-sync-bootstrap': 60,
|
|
74
77
|
'mobile-sync-pull': 120,
|
|
75
78
|
'mobile-sync-push': 60,
|
|
@@ -208,10 +211,28 @@ function mergeAgenticToolProvenance(routingMetadata, toolInvocations = []) {
|
|
|
208
211
|
}
|
|
209
212
|
}
|
|
210
213
|
|
|
214
|
+
function sanitizeBodyWeightEvidenceFactsForStorage(value) {
|
|
215
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
|
|
216
|
+
const facts = { ...value };
|
|
217
|
+
if (Array.isArray(facts.rows)) {
|
|
218
|
+
facts.rows = facts.rows
|
|
219
|
+
.filter((row) => row?.date && Number.isFinite(Number(row.weightKg)))
|
|
220
|
+
.slice(-90)
|
|
221
|
+
.map((row) => ({
|
|
222
|
+
date: askStorageString(String(row.date).slice(0, 10), { maxLength: 10 }),
|
|
223
|
+
weightKg: Math.round(Number(row.weightKg) * 10) / 10
|
|
224
|
+
}))
|
|
225
|
+
.filter((row) => row.date);
|
|
226
|
+
}
|
|
227
|
+
const serialized = JSON.stringify(facts);
|
|
228
|
+
if (serialized.length > ASK_STRUCTURED_MAX_JSON_LENGTH) return null;
|
|
229
|
+
return JSON.parse(serialized);
|
|
230
|
+
}
|
|
231
|
+
|
|
211
232
|
function sanitizeAskEvidenceForStorage(item) {
|
|
212
233
|
if (!item || typeof item !== 'object' || Array.isArray(item)) return null;
|
|
213
234
|
const sanitized = {};
|
|
214
|
-
for (const key of ['label', 'section', 'toolName', 'sourceTimestamp']) {
|
|
235
|
+
for (const key of ['label', 'section', 'toolName', 'sourceTimestamp', 'kind', 'presentation']) {
|
|
215
236
|
const value = askStorageString(item[key], { maxLength: 240 });
|
|
216
237
|
if (value) sanitized[key] = value;
|
|
217
238
|
}
|
|
@@ -219,6 +240,13 @@ function sanitizeAskEvidenceForStorage(item) {
|
|
|
219
240
|
const values = askStorageStringArray(item[key], { maxItems: ASK_STRUCTURED_MAX_ITEMS, maxLength: 160 });
|
|
220
241
|
if (values.length > 0) sanitized[key] = values;
|
|
221
242
|
}
|
|
243
|
+
const isBodyWeightEvidence = sanitized.toolName === 'get_body_weight_snapshot'
|
|
244
|
+
|| sanitized.kind === 'body_weight_trend'
|
|
245
|
+
|| sanitized.presentation === 'body_weight_trend';
|
|
246
|
+
if (isBodyWeightEvidence) {
|
|
247
|
+
const facts = sanitizeBodyWeightEvidenceFactsForStorage(item.facts);
|
|
248
|
+
if (facts) sanitized.facts = facts;
|
|
249
|
+
}
|
|
222
250
|
return Object.keys(sanitized).length > 0 ? sanitized : null;
|
|
223
251
|
}
|
|
224
252
|
|
|
@@ -243,7 +271,7 @@ function sanitizeAskProgramDraftForStorage(value) {
|
|
|
243
271
|
return JSON.parse(serialized);
|
|
244
272
|
}
|
|
245
273
|
|
|
246
|
-
function sanitizeAskStructuredResponseForStorage(structured) {
|
|
274
|
+
export function sanitizeAskStructuredResponseForStorage(structured) {
|
|
247
275
|
if (!structured || typeof structured !== 'object' || Array.isArray(structured)) return null;
|
|
248
276
|
const confidence = askStorageString(structured.confidence, { maxLength: 40 });
|
|
249
277
|
const answer = askStorageString(structured.answer);
|
|
@@ -922,6 +950,38 @@ function coachObservationGenerationMetadata(result, sourceTrigger) {
|
|
|
922
950
|
};
|
|
923
951
|
}
|
|
924
952
|
|
|
953
|
+
function skippedCoachObservationGenerationMetadata(skippedReason, sourceTrigger) {
|
|
954
|
+
return {
|
|
955
|
+
attempted: false,
|
|
956
|
+
generated: 0,
|
|
957
|
+
persisted: 0,
|
|
958
|
+
skippedReason,
|
|
959
|
+
sourceTrigger: sourceTrigger ?? null
|
|
960
|
+
};
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
function recentDailyRefreshObservation(observations, now = new Date()) {
|
|
964
|
+
const threshold = now.getTime() - COACH_OBSERVATION_DAILY_REFRESH_COOLDOWN_MS;
|
|
965
|
+
return (Array.isArray(observations) ? observations : []).some((observation) => {
|
|
966
|
+
if (observation?.sourceTrigger !== 'daily_refresh') return false;
|
|
967
|
+
const generatedAt = new Date(observation.generatedAt ?? observation.windowEnd ?? 0).getTime();
|
|
968
|
+
return Number.isFinite(generatedAt) && generatedAt >= threshold;
|
|
969
|
+
});
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
async function coachObservationGenerationSkipReason({
|
|
973
|
+
account,
|
|
974
|
+
uploadResult,
|
|
975
|
+
sourceTrigger,
|
|
976
|
+
listCurrentCoachObservationsForAccount
|
|
977
|
+
}) {
|
|
978
|
+
if (sourceTrigger !== 'daily_refresh') return null;
|
|
979
|
+
if (Number(uploadResult?.inserted ?? 0) <= 0) return 'no_score_snapshot_change';
|
|
980
|
+
if (typeof listCurrentCoachObservationsForAccount !== 'function') return null;
|
|
981
|
+
const observations = await listCurrentCoachObservationsForAccount(account, { limit: 20 });
|
|
982
|
+
return recentDailyRefreshObservation(observations) ? 'recent_daily_refresh' : null;
|
|
983
|
+
}
|
|
984
|
+
|
|
925
985
|
function normalizeAskCoachObservationFollowUp(value) {
|
|
926
986
|
if (!value || typeof value !== 'object') return null;
|
|
927
987
|
const id = String(value.id ?? '').trim();
|
|
@@ -1152,6 +1212,20 @@ function routeRequest(url, method) {
|
|
|
1152
1212
|
}
|
|
1153
1213
|
}
|
|
1154
1214
|
|
|
1215
|
+
if (pathname === '/cli/workout-share') {
|
|
1216
|
+
return { command: 'workout-share-create', options: {} };
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
{
|
|
1220
|
+
const workoutSharePublicMatch = pathname.match(/^\/workout-share\/([^/]+)$/);
|
|
1221
|
+
if (workoutSharePublicMatch) {
|
|
1222
|
+
return {
|
|
1223
|
+
command: 'workout-share-public',
|
|
1224
|
+
options: { token: decodeURIComponent(workoutSharePublicMatch[1]) }
|
|
1225
|
+
};
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1155
1229
|
{
|
|
1156
1230
|
const programShareRevokeMatch = pathname.match(/^\/cli\/program-share\/([^/]+)\/revoke$/);
|
|
1157
1231
|
if (programShareRevokeMatch) {
|
|
@@ -2291,6 +2365,8 @@ export function createSyncServiceRequestHandler({
|
|
|
2291
2365
|
listProgramSharesForAccount = null,
|
|
2292
2366
|
readPublicProgramShare = null,
|
|
2293
2367
|
revokeProgramShareForAccount = null,
|
|
2368
|
+
createWorkoutShareForAccount = null,
|
|
2369
|
+
readPublicWorkoutShare = null,
|
|
2294
2370
|
updateAnalysisConsentForAccount = null,
|
|
2295
2371
|
updateDisplayNameForAccount = null,
|
|
2296
2372
|
saveAskConversationForAccount = null,
|
|
@@ -2979,6 +3055,45 @@ export function createSyncServiceRequestHandler({
|
|
|
2979
3055
|
}
|
|
2980
3056
|
}
|
|
2981
3057
|
|
|
3058
|
+
if (route.command === 'workout-share-public') {
|
|
3059
|
+
if (request.method !== 'GET') {
|
|
3060
|
+
methodNotAllowed(response, 'Use GET for /workout-share/:token.');
|
|
3061
|
+
return;
|
|
3062
|
+
}
|
|
3063
|
+
if (!readPublicWorkoutShare) {
|
|
3064
|
+
methodNotAllowed(response, 'Workout sharing is not enabled for this service mode.');
|
|
3065
|
+
return;
|
|
3066
|
+
}
|
|
3067
|
+
try {
|
|
3068
|
+
const shared = await readPublicWorkoutShare(route.options.token);
|
|
3069
|
+
if (shared.status === 'not_found') {
|
|
3070
|
+
notFound(response, 'Workout share not found.');
|
|
3071
|
+
return;
|
|
3072
|
+
}
|
|
3073
|
+
if (shared.status === 'revoked' || shared.status === 'expired') {
|
|
3074
|
+
json(response, 410, { error: 'Workout share is no longer available.' });
|
|
3075
|
+
return;
|
|
3076
|
+
}
|
|
3077
|
+
json(response, 200, {
|
|
3078
|
+
ok: true,
|
|
3079
|
+
token: route.options.token,
|
|
3080
|
+
version: shared.share.version,
|
|
3081
|
+
workoutName: shared.share.workoutName,
|
|
3082
|
+
workoutPayload: shared.share.workoutPayload,
|
|
3083
|
+
createdAt: shared.share.createdAt,
|
|
3084
|
+
expiresAt: shared.share.expiresAt
|
|
3085
|
+
});
|
|
3086
|
+
return;
|
|
3087
|
+
} catch (error) {
|
|
3088
|
+
if (error?.message === 'Invalid workout share token.') {
|
|
3089
|
+
badRequest(response, error.message);
|
|
3090
|
+
return;
|
|
3091
|
+
}
|
|
3092
|
+
internalError(response, error, onError);
|
|
3093
|
+
return;
|
|
3094
|
+
}
|
|
3095
|
+
}
|
|
3096
|
+
|
|
2982
3097
|
if (route.command === 'google-mobile') {
|
|
2983
3098
|
if (request.method !== 'POST') {
|
|
2984
3099
|
methodNotAllowed(response, 'Use POST for /auth/google/mobile.');
|
|
@@ -3297,18 +3412,33 @@ export function createSyncServiceRequestHandler({
|
|
|
3297
3412
|
const result = await insertScoreSnapshotsForAccount(account, body.snapshots);
|
|
3298
3413
|
if (generateCoachObservationsForAccount) {
|
|
3299
3414
|
const sourceTrigger = coachObservationSourceTriggerForScoreSnapshots(body.snapshots);
|
|
3415
|
+
let skipReason = null;
|
|
3300
3416
|
try {
|
|
3301
|
-
|
|
3302
|
-
|
|
3417
|
+
skipReason = await coachObservationGenerationSkipReason({
|
|
3418
|
+
account,
|
|
3419
|
+
uploadResult: result,
|
|
3420
|
+
sourceTrigger,
|
|
3421
|
+
listCurrentCoachObservationsForAccount
|
|
3422
|
+
});
|
|
3303
3423
|
} catch (error) {
|
|
3304
|
-
console.error('Coach observation generation
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
sourceTrigger
|
|
3311
|
-
|
|
3424
|
+
console.error('Coach observation generation skip check failed:', error.message);
|
|
3425
|
+
}
|
|
3426
|
+
if (skipReason) {
|
|
3427
|
+
result.coachObservations = skippedCoachObservationGenerationMetadata(skipReason, sourceTrigger);
|
|
3428
|
+
} else {
|
|
3429
|
+
try {
|
|
3430
|
+
const generationResult = await generateCoachObservationsForAccount(account, { sourceTrigger });
|
|
3431
|
+
result.coachObservations = coachObservationGenerationMetadata(generationResult, sourceTrigger);
|
|
3432
|
+
} catch (error) {
|
|
3433
|
+
console.error('Coach observation generation after score upload failed:', error.message);
|
|
3434
|
+
result.coachObservations = {
|
|
3435
|
+
attempted: true,
|
|
3436
|
+
generated: 0,
|
|
3437
|
+
persisted: 0,
|
|
3438
|
+
skippedReason: 'generation_failed',
|
|
3439
|
+
sourceTrigger
|
|
3440
|
+
};
|
|
3441
|
+
}
|
|
3312
3442
|
}
|
|
3313
3443
|
}
|
|
3314
3444
|
json(response, 200, result);
|
|
@@ -3579,6 +3709,53 @@ export function createSyncServiceRequestHandler({
|
|
|
3579
3709
|
}
|
|
3580
3710
|
}
|
|
3581
3711
|
|
|
3712
|
+
if (route.command === 'workout-share-create') {
|
|
3713
|
+
if (request.method !== 'POST') {
|
|
3714
|
+
methodNotAllowed(response, 'Use POST for /cli/workout-share.');
|
|
3715
|
+
return;
|
|
3716
|
+
}
|
|
3717
|
+
if (!createWorkoutShareForAccount) {
|
|
3718
|
+
methodNotAllowed(response, 'Workout sharing is not enabled for this service mode.');
|
|
3719
|
+
return;
|
|
3720
|
+
}
|
|
3721
|
+
const account = connectedWriteAuthenticator
|
|
3722
|
+
? await connectedWriteAuthenticator(requestToken)
|
|
3723
|
+
: null;
|
|
3724
|
+
if (!account) {
|
|
3725
|
+
if (await rejectInsufficientScopeForReadToken(response, request, requestToken, readAuthenticator)) {
|
|
3726
|
+
return;
|
|
3727
|
+
}
|
|
3728
|
+
unauthorized(response, request);
|
|
3729
|
+
return;
|
|
3730
|
+
}
|
|
3731
|
+
try {
|
|
3732
|
+
const body = await readJsonBody(request);
|
|
3733
|
+
const workoutPayload = body.workoutPayload ?? body;
|
|
3734
|
+
const share = await createWorkoutShareForAccount(account, workoutPayload);
|
|
3735
|
+
json(response, 201, {
|
|
3736
|
+
ok: true,
|
|
3737
|
+
shareId: share.id,
|
|
3738
|
+
tokenHint: share.tokenHint,
|
|
3739
|
+
token: share.token,
|
|
3740
|
+
workoutName: share.workoutName,
|
|
3741
|
+
createdAt: share.createdAt,
|
|
3742
|
+
expiresAt: share.expiresAt,
|
|
3743
|
+
revokedAt: share.revokedAt,
|
|
3744
|
+
version: share.version,
|
|
3745
|
+
link: `${resolvedPublicOrigin}/workout-share/${share.token}`,
|
|
3746
|
+
deepLink: `incremnt://workout-share/${share.token}?base=${encodeURIComponent(resolvedPublicOrigin)}`
|
|
3747
|
+
});
|
|
3748
|
+
return;
|
|
3749
|
+
} catch (error) {
|
|
3750
|
+
if (error?.message === 'Workout share payload is malformed.') {
|
|
3751
|
+
badRequest(response, error.message);
|
|
3752
|
+
return;
|
|
3753
|
+
}
|
|
3754
|
+
internalError(response, error, onError);
|
|
3755
|
+
return;
|
|
3756
|
+
}
|
|
3757
|
+
}
|
|
3758
|
+
|
|
3582
3759
|
if (route.command === 'program-share-list') {
|
|
3583
3760
|
if (request.method !== 'GET') {
|
|
3584
3761
|
methodNotAllowed(response, 'Use GET for /cli/programs/:programId/shares.');
|
|
@@ -3999,45 +4176,8 @@ export function createSyncServiceRequestHandler({
|
|
|
3999
4176
|
const limit = parseLimit(route.options.limit, { defaultValue: 5, max: 20 });
|
|
4000
4177
|
let refresh = null;
|
|
4001
4178
|
if (route.options.refresh === 'morning_open') {
|
|
4002
|
-
const writeAccount = writeAuthenticator
|
|
4003
|
-
? await writeAuthenticator(requestToken)
|
|
4004
|
-
: requestToken === token
|
|
4005
|
-
? account
|
|
4006
|
-
: null;
|
|
4007
|
-
if (!writeAccount) {
|
|
4008
|
-
if (await rejectInsufficientScopeForReadToken(response, request, requestToken, readAuthenticator)) {
|
|
4009
|
-
return;
|
|
4010
|
-
}
|
|
4011
|
-
unauthorized(response, request);
|
|
4012
|
-
return;
|
|
4013
|
-
}
|
|
4014
4179
|
const sourceTrigger = 'daily_refresh';
|
|
4015
|
-
|
|
4016
|
-
try {
|
|
4017
|
-
const generationResult = await generateCoachObservationsForAccount(writeAccount, {
|
|
4018
|
-
sourceTrigger,
|
|
4019
|
-
refresh: route.options.refresh
|
|
4020
|
-
});
|
|
4021
|
-
refresh = coachObservationGenerationMetadata(generationResult, sourceTrigger);
|
|
4022
|
-
} catch (error) {
|
|
4023
|
-
console.error('Coach observation generation on morning open failed:', error.message);
|
|
4024
|
-
refresh = {
|
|
4025
|
-
attempted: true,
|
|
4026
|
-
generated: 0,
|
|
4027
|
-
persisted: 0,
|
|
4028
|
-
skippedReason: 'generation_failed',
|
|
4029
|
-
sourceTrigger
|
|
4030
|
-
};
|
|
4031
|
-
}
|
|
4032
|
-
} else {
|
|
4033
|
-
refresh = {
|
|
4034
|
-
attempted: false,
|
|
4035
|
-
generated: 0,
|
|
4036
|
-
persisted: 0,
|
|
4037
|
-
skippedReason: 'generation_unavailable',
|
|
4038
|
-
sourceTrigger
|
|
4039
|
-
};
|
|
4040
|
-
}
|
|
4180
|
+
refresh = skippedCoachObservationGenerationMetadata('morning_open_read_only', sourceTrigger);
|
|
4041
4181
|
}
|
|
4042
4182
|
const observations = await listCurrentCoachObservationsForAccount(account, {
|
|
4043
4183
|
limit
|