@murphai/murph 0.1.1 → 0.1.13
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/CHANGELOG.md +154 -0
- package/README.md +103 -60
- package/dist/.tsbuildinfo +1 -1
- package/dist/assistant/ui/ink.d.ts.map +1 -1
- package/dist/assistant/ui/ink.js +2 -3
- package/dist/assistant/ui/ink.js.map +1 -1
- package/dist/assistant-runtime.d.ts +0 -2
- package/dist/assistant-runtime.d.ts.map +1 -1
- package/dist/assistant-runtime.js +0 -1
- package/dist/assistant-runtime.js.map +1 -1
- package/dist/commands/device.js +1 -1
- package/dist/commands/device.js.map +1 -1
- package/dist/commands/export-intake-read-helpers.d.ts +1 -1
- package/dist/commands/knowledge.d.ts +3 -0
- package/dist/commands/knowledge.d.ts.map +1 -0
- package/dist/commands/knowledge.js +164 -0
- package/dist/commands/knowledge.js.map +1 -0
- package/dist/commands/wearables.d.ts +4985 -0
- package/dist/commands/wearables.d.ts.map +1 -0
- package/dist/commands/wearables.js +355 -0
- package/dist/commands/wearables.js.map +1 -0
- package/dist/commands/workout.d.ts.map +1 -1
- package/dist/commands/workout.js +330 -28
- package/dist/commands/workout.js.map +1 -1
- package/dist/incur-error-bridge.d.ts +2 -0
- package/dist/incur-error-bridge.d.ts.map +1 -0
- package/dist/incur-error-bridge.js +25 -0
- package/dist/incur-error-bridge.js.map +1 -0
- package/dist/incur.generated.d.ts +118 -1
- package/dist/incur.generated.d.ts.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/knowledge-cli-contracts.d.ts +179 -0
- package/dist/knowledge-cli-contracts.d.ts.map +1 -0
- package/dist/knowledge-cli-contracts.js +78 -0
- package/dist/knowledge-cli-contracts.js.map +1 -0
- package/dist/knowledge-documents.d.ts +44 -0
- package/dist/knowledge-documents.d.ts.map +1 -0
- package/dist/knowledge-documents.js +195 -0
- package/dist/knowledge-documents.js.map +1 -0
- package/dist/knowledge-lint.d.ts +11 -0
- package/dist/knowledge-lint.d.ts.map +1 -0
- package/dist/knowledge-lint.js +254 -0
- package/dist/knowledge-lint.js.map +1 -0
- package/dist/knowledge-runtime.d.ts +49 -0
- package/dist/knowledge-runtime.d.ts.map +1 -0
- package/dist/knowledge-runtime.js +227 -0
- package/dist/knowledge-runtime.js.map +1 -0
- package/dist/research-runtime.d.ts +3 -40
- package/dist/research-runtime.d.ts.map +1 -1
- package/dist/research-runtime.js +54 -253
- package/dist/research-runtime.js.map +1 -1
- package/dist/review-gpt-runtime.d.ts +85 -0
- package/dist/review-gpt-runtime.d.ts.map +1 -0
- package/dist/review-gpt-runtime.js +239 -0
- package/dist/review-gpt-runtime.js.map +1 -0
- package/dist/setup-assistant.d.ts +1 -0
- package/dist/setup-assistant.d.ts.map +1 -1
- package/dist/setup-assistant.js +2 -1
- package/dist/setup-assistant.js.map +1 -1
- package/dist/setup-cli.d.ts.map +1 -1
- package/dist/setup-cli.js +10 -1
- package/dist/setup-cli.js.map +1 -1
- package/dist/setup-wizard.d.ts.map +1 -1
- package/dist/setup-wizard.js +26 -7
- package/dist/setup-wizard.js.map +1 -1
- package/dist/usecases/workout-artifacts.d.ts +21 -0
- package/dist/usecases/workout-artifacts.d.ts.map +1 -0
- package/dist/usecases/workout-artifacts.js +149 -0
- package/dist/usecases/workout-artifacts.js.map +1 -0
- package/dist/usecases/workout-format.d.ts +92 -10
- package/dist/usecases/workout-format.d.ts.map +1 -1
- package/dist/usecases/workout-format.js +211 -391
- package/dist/usecases/workout-format.js.map +1 -1
- package/dist/usecases/workout-import.d.ts +36 -0
- package/dist/usecases/workout-import.d.ts.map +1 -0
- package/dist/usecases/workout-import.js +587 -0
- package/dist/usecases/workout-import.js.map +1 -0
- package/dist/usecases/workout-measurement.d.ts +66 -0
- package/dist/usecases/workout-measurement.d.ts.map +1 -0
- package/dist/usecases/workout-measurement.js +285 -0
- package/dist/usecases/workout-measurement.js.map +1 -0
- package/dist/usecases/workout-model.d.ts +15 -0
- package/dist/usecases/workout-model.d.ts.map +1 -0
- package/dist/usecases/workout-model.js +161 -0
- package/dist/usecases/workout-model.js.map +1 -0
- package/dist/usecases/workout.d.ts +57 -20
- package/dist/usecases/workout.d.ts.map +1 -1
- package/dist/usecases/workout.js +360 -214
- package/dist/usecases/workout.js.map +1 -1
- package/dist/vault-cli-command-manifest.d.ts +5179 -3
- package/dist/vault-cli-command-manifest.d.ts.map +1 -1
- package/dist/vault-cli-command-manifest.js +140 -3
- package/dist/vault-cli-command-manifest.js.map +1 -1
- package/dist/vault-cli.d.ts.map +1 -1
- package/dist/vault-cli.js +2 -0
- package/dist/vault-cli.js.map +1 -1
- package/package.json +22 -13
package/dist/usecases/workout.js
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
|
-
import {} from '@murphai/contracts';
|
|
1
|
+
import { workoutSessionSchema, } from '@murphai/contracts';
|
|
2
|
+
import { loadJsonInputObject } from '@murphai/assistant-core/json-input';
|
|
3
|
+
import { showWorkoutRecord } from '@murphai/assistant-core/usecases/workout-read';
|
|
2
4
|
import { VaultCliError } from '@murphai/assistant-core/vault-cli-errors';
|
|
3
|
-
import { inferDurationMinutes, validateDurationMinutes, } from './text-duration.js';
|
|
4
5
|
import { deleteEventRecord, editEventRecord, } from '@murphai/assistant-core/usecases/event-record-mutations';
|
|
5
|
-
import {
|
|
6
|
+
import { upsertEventRecord, } from '@murphai/assistant-core/usecases/provider-event';
|
|
6
7
|
import { compactObject, normalizeOptionalText, } from '@murphai/assistant-core/usecases/vault-usecase-helpers';
|
|
8
|
+
import { inferDurationMinutes, validateDurationMinutes, } from './text-duration.js';
|
|
9
|
+
import { buildWorkoutTitle, deriveDurationMinutesFromTimestamps, summarizeWorkoutSessionExercises, } from './workout-model.js';
|
|
10
|
+
import { cleanupStagedWorkoutMediaBatch, stageWorkoutMediaBatch, } from './workout-artifacts.js';
|
|
11
|
+
import { generateUlid } from '@murphai/runtime-state';
|
|
7
12
|
const MILES_TO_KM = 1.609344;
|
|
8
13
|
const knownWorkoutTypes = [
|
|
9
14
|
{
|
|
@@ -91,45 +96,285 @@ export function resolveWorkoutCapture(input) {
|
|
|
91
96
|
const strengthExercises = input.strengthExercises ?? inferStrengthExercises(note, activity.activityType);
|
|
92
97
|
return {
|
|
93
98
|
note,
|
|
94
|
-
title: buildWorkoutTitle(activity.
|
|
99
|
+
title: buildWorkoutTitle(activity.activityType, durationMinutes),
|
|
95
100
|
activityType: activity.activityType,
|
|
96
101
|
durationMinutes,
|
|
97
102
|
distanceKm: distanceKm ?? null,
|
|
98
103
|
strengthExercises: strengthExercises ?? null,
|
|
99
104
|
};
|
|
100
105
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
106
|
+
function asJsonObject(value) {
|
|
107
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
108
|
+
? value
|
|
109
|
+
: null;
|
|
110
|
+
}
|
|
111
|
+
function valueAsString(value) {
|
|
112
|
+
return typeof value === 'string' ? value : undefined;
|
|
113
|
+
}
|
|
114
|
+
function valueAsNumber(value) {
|
|
115
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
|
116
|
+
}
|
|
117
|
+
function stringArray(value) {
|
|
118
|
+
return Array.isArray(value)
|
|
119
|
+
? value.filter((entry) => typeof entry === 'string' && entry.trim().length > 0)
|
|
120
|
+
: [];
|
|
121
|
+
}
|
|
122
|
+
function mergeStoredMedia(existing, additions) {
|
|
123
|
+
const merged = new Map();
|
|
124
|
+
for (const entry of existing ?? []) {
|
|
125
|
+
merged.set(entry.relativePath, entry);
|
|
126
|
+
}
|
|
127
|
+
for (const entry of additions) {
|
|
128
|
+
merged.set(entry.relativePath, entry);
|
|
129
|
+
}
|
|
130
|
+
return [...merged.values()];
|
|
131
|
+
}
|
|
132
|
+
function ensureWorkoutEventId(payload) {
|
|
133
|
+
const explicitId = valueAsString(payload.id);
|
|
134
|
+
if (explicitId) {
|
|
135
|
+
return explicitId;
|
|
136
|
+
}
|
|
137
|
+
const eventId = `evt_${generateUlid()}`;
|
|
138
|
+
payload.id = eventId;
|
|
139
|
+
return eventId;
|
|
140
|
+
}
|
|
141
|
+
function applyStagedWorkoutMedia(input) {
|
|
142
|
+
if (input.media.length === 0 && input.rawRefs.length === 0) {
|
|
143
|
+
return input.payload;
|
|
144
|
+
}
|
|
145
|
+
const payload = { ...input.payload, id: input.eventId };
|
|
146
|
+
const existingWorkout = normalizeStructuredWorkout(payload.workout);
|
|
147
|
+
const mergedRawRefs = [...new Set([...stringArray(payload.rawRefs), ...input.rawRefs])];
|
|
148
|
+
const mergedWorkoutMedia = mergeStoredMedia(existingWorkout?.media, input.media);
|
|
149
|
+
const workout = existingWorkout
|
|
150
|
+
? {
|
|
151
|
+
...existingWorkout,
|
|
152
|
+
...(mergedWorkoutMedia.length > 0 ? { media: mergedWorkoutMedia } : {}),
|
|
153
|
+
}
|
|
154
|
+
: {
|
|
155
|
+
media: mergedWorkoutMedia,
|
|
156
|
+
exercises: [],
|
|
157
|
+
};
|
|
158
|
+
return {
|
|
159
|
+
...payload,
|
|
160
|
+
rawRefs: mergedRawRefs,
|
|
161
|
+
workout,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
function formatSchemaIssues(issues) {
|
|
165
|
+
return issues
|
|
166
|
+
.map((issue) => {
|
|
167
|
+
const path = issue.path.length > 0 ? issue.path.join('.') : 'value';
|
|
168
|
+
return `${path}: ${issue.message}`;
|
|
169
|
+
})
|
|
170
|
+
.join('; ');
|
|
171
|
+
}
|
|
172
|
+
function resolveStructuredDurationMinutes(input) {
|
|
173
|
+
const explicitDurationMinutes = typeof input.explicitDurationMinutes === 'number'
|
|
174
|
+
? validateDurationMinutes(input.explicitDurationMinutes)
|
|
175
|
+
: undefined;
|
|
176
|
+
if (explicitDurationMinutes !== undefined) {
|
|
177
|
+
return explicitDurationMinutes;
|
|
178
|
+
}
|
|
179
|
+
const payloadDurationMinutes = typeof input.payloadDurationMinutes === 'number'
|
|
180
|
+
? validateDurationMinutes(input.payloadDurationMinutes)
|
|
181
|
+
: undefined;
|
|
182
|
+
if (payloadDurationMinutes !== undefined) {
|
|
183
|
+
return payloadDurationMinutes;
|
|
184
|
+
}
|
|
185
|
+
const derivedDurationMinutes = deriveDurationMinutesFromTimestamps(input.structuredWorkout?.startedAt, input.structuredWorkout?.endedAt);
|
|
186
|
+
if (derivedDurationMinutes !== null) {
|
|
187
|
+
return derivedDurationMinutes;
|
|
188
|
+
}
|
|
189
|
+
if (input.fallbackText) {
|
|
190
|
+
return resolveDurationMinutes(input.fallbackText, undefined);
|
|
191
|
+
}
|
|
192
|
+
throw new VaultCliError('invalid_option', 'Workout duration is missing. Pass --duration <minutes> to record it explicitly.');
|
|
193
|
+
}
|
|
194
|
+
function normalizeStructuredWorkout(value, fieldName = 'workout') {
|
|
195
|
+
if (value === undefined || value === null) {
|
|
196
|
+
return undefined;
|
|
197
|
+
}
|
|
198
|
+
const parsed = workoutSessionSchema.safeParse(value);
|
|
199
|
+
if (!parsed.success) {
|
|
200
|
+
throw new VaultCliError('invalid_payload', `${fieldName} is not a valid workout session payload. ${formatSchemaIssues(parsed.error.issues)}`);
|
|
201
|
+
}
|
|
202
|
+
return parsed.data;
|
|
203
|
+
}
|
|
204
|
+
function pickPassthroughEventFields(payload) {
|
|
205
|
+
const keys = ['rawRefs', 'externalRef', 'relatedIds', 'tags', 'timeZone'];
|
|
206
|
+
const entries = keys.flatMap((key) => payload[key] !== undefined ? [[key, payload[key]]] : []);
|
|
207
|
+
return Object.fromEntries(entries);
|
|
208
|
+
}
|
|
209
|
+
export function buildStructuredWorkoutEventPayload(input) {
|
|
210
|
+
const sourcePayload = input.payload;
|
|
211
|
+
const structuredWorkout = normalizeStructuredWorkout(input.workout, 'workout')
|
|
212
|
+
?? (sourcePayload.workout !== undefined
|
|
213
|
+
? normalizeStructuredWorkout(sourcePayload.workout, 'payload.workout')
|
|
214
|
+
: undefined)
|
|
215
|
+
?? (Array.isArray(sourcePayload.exercises)
|
|
216
|
+
? normalizeStructuredWorkout(sourcePayload, 'payload')
|
|
217
|
+
: undefined);
|
|
218
|
+
const fallbackText = normalizeOptionalText(valueAsString(sourcePayload.note))
|
|
219
|
+
?? normalizeOptionalText(valueAsString(sourcePayload.text))
|
|
220
|
+
?? normalizeOptionalText(input.text)
|
|
221
|
+
?? normalizeOptionalText(structuredWorkout?.sessionNote)
|
|
222
|
+
?? normalizeOptionalText(structuredWorkout?.routineName);
|
|
223
|
+
const activityDescriptor = fallbackText
|
|
224
|
+
? resolveWorkoutActivityDescriptor(fallbackText, input.activityType ?? valueAsString(sourcePayload.activityType) ?? 'strength-training')
|
|
225
|
+
: resolveWorkoutActivityDescriptor(input.activityType ?? valueAsString(sourcePayload.activityType) ?? 'strength-training', input.activityType ?? valueAsString(sourcePayload.activityType) ?? 'strength-training');
|
|
226
|
+
const durationMinutes = resolveStructuredDurationMinutes({
|
|
227
|
+
explicitDurationMinutes: input.durationMinutes,
|
|
228
|
+
payloadDurationMinutes: valueAsNumber(sourcePayload.durationMinutes),
|
|
229
|
+
structuredWorkout,
|
|
230
|
+
fallbackText: fallbackText ?? undefined,
|
|
231
|
+
});
|
|
232
|
+
const distanceKm = typeof input.distanceKm === 'number'
|
|
233
|
+
? input.distanceKm
|
|
234
|
+
: typeof sourcePayload.distanceKm === 'number'
|
|
235
|
+
? sourcePayload.distanceKm
|
|
236
|
+
: resolveDistanceKm(fallbackText ?? '', undefined);
|
|
237
|
+
const strengthExercises = input.strengthExercises
|
|
238
|
+
?? (Array.isArray(sourcePayload.strengthExercises)
|
|
239
|
+
? sourcePayload.strengthExercises
|
|
240
|
+
: null)
|
|
241
|
+
?? summarizeWorkoutSessionExercises(structuredWorkout)
|
|
242
|
+
?? null;
|
|
243
|
+
const occurredAt = input.occurredAt
|
|
244
|
+
?? valueAsString(sourcePayload.occurredAt)
|
|
245
|
+
?? structuredWorkout?.startedAt
|
|
246
|
+
?? new Date().toISOString();
|
|
247
|
+
const title = normalizeOptionalText(input.title)
|
|
248
|
+
?? normalizeOptionalText(valueAsString(sourcePayload.title))
|
|
249
|
+
?? buildWorkoutTitle(activityDescriptor.activityType, durationMinutes);
|
|
250
|
+
const note = fallbackText ?? title;
|
|
251
|
+
return {
|
|
252
|
+
...pickPassthroughEventFields(sourcePayload),
|
|
105
253
|
kind: 'activity_session',
|
|
106
254
|
occurredAt,
|
|
107
|
-
source: input.source ?? 'manual',
|
|
108
|
-
title
|
|
109
|
-
activityType:
|
|
110
|
-
durationMinutes
|
|
111
|
-
...(typeof
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
? { strengthExercises: capture.strengthExercises }
|
|
116
|
-
: {}),
|
|
117
|
-
note: capture.note,
|
|
255
|
+
source: input.source ?? valueAsString(sourcePayload.source) ?? 'manual',
|
|
256
|
+
title,
|
|
257
|
+
activityType: activityDescriptor.activityType,
|
|
258
|
+
durationMinutes,
|
|
259
|
+
...(typeof distanceKm === 'number' ? { distanceKm } : {}),
|
|
260
|
+
...(strengthExercises ? { strengthExercises } : {}),
|
|
261
|
+
...(structuredWorkout ? { workout: structuredWorkout } : {}),
|
|
262
|
+
note,
|
|
118
263
|
};
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
264
|
+
}
|
|
265
|
+
async function loadStructuredWorkoutPayload(inputFile) {
|
|
266
|
+
return loadJsonInputObject(inputFile, 'workout payload');
|
|
267
|
+
}
|
|
268
|
+
export async function addWorkoutRecord(input) {
|
|
269
|
+
let payload;
|
|
270
|
+
if (typeof input.inputFile === 'string') {
|
|
271
|
+
payload = buildStructuredWorkoutEventPayload({
|
|
272
|
+
payload: await loadStructuredWorkoutPayload(input.inputFile),
|
|
273
|
+
occurredAt: input.occurredAt,
|
|
274
|
+
source: input.source,
|
|
275
|
+
durationMinutes: input.durationMinutes,
|
|
276
|
+
activityType: input.activityType,
|
|
277
|
+
distanceKm: input.distanceKm,
|
|
278
|
+
strengthExercises: input.strengthExercises,
|
|
279
|
+
workout: input.workout,
|
|
280
|
+
text: input.text,
|
|
281
|
+
title: input.title,
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
else if (input.workout) {
|
|
285
|
+
payload = buildStructuredWorkoutEventPayload({
|
|
286
|
+
payload: {},
|
|
287
|
+
occurredAt: input.occurredAt,
|
|
288
|
+
source: input.source,
|
|
289
|
+
durationMinutes: input.durationMinutes,
|
|
290
|
+
activityType: input.activityType ?? 'strength-training',
|
|
291
|
+
distanceKm: input.distanceKm,
|
|
292
|
+
strengthExercises: input.strengthExercises,
|
|
293
|
+
workout: input.workout,
|
|
294
|
+
text: input.text,
|
|
295
|
+
title: input.title,
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
else {
|
|
299
|
+
const capture = resolveWorkoutCapture({
|
|
300
|
+
text: input.text ?? '',
|
|
301
|
+
durationMinutes: input.durationMinutes,
|
|
302
|
+
activityType: input.activityType,
|
|
303
|
+
distanceKm: input.distanceKm,
|
|
304
|
+
strengthExercises: input.strengthExercises,
|
|
305
|
+
});
|
|
306
|
+
payload = {
|
|
307
|
+
kind: 'activity_session',
|
|
308
|
+
occurredAt: input.occurredAt ?? new Date().toISOString(),
|
|
309
|
+
source: input.source ?? 'manual',
|
|
310
|
+
title: capture.title,
|
|
311
|
+
activityType: capture.activityType,
|
|
312
|
+
durationMinutes: capture.durationMinutes,
|
|
313
|
+
...(typeof capture.distanceKm === 'number'
|
|
314
|
+
? { distanceKm: capture.distanceKm }
|
|
315
|
+
: {}),
|
|
316
|
+
...(capture.strengthExercises
|
|
317
|
+
? { strengthExercises: capture.strengthExercises }
|
|
318
|
+
: {}),
|
|
319
|
+
note: capture.note,
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
const mediaPaths = Array.isArray(input.mediaPaths)
|
|
323
|
+
? input.mediaPaths.filter((entry) => typeof entry === 'string' && entry.trim().length > 0)
|
|
324
|
+
: [];
|
|
325
|
+
let manifestFile = null;
|
|
326
|
+
if (mediaPaths.length > 0) {
|
|
327
|
+
const eventId = ensureWorkoutEventId(payload);
|
|
328
|
+
const occurredAt = String(payload.occurredAt ?? input.occurredAt ?? new Date().toISOString());
|
|
329
|
+
const stagedMedia = await stageWorkoutMediaBatch({
|
|
330
|
+
vault: input.vault,
|
|
331
|
+
eventId,
|
|
332
|
+
occurredAt,
|
|
333
|
+
family: 'workout',
|
|
334
|
+
source: valueAsString(payload.source) ?? input.source ?? 'manual',
|
|
335
|
+
mediaPaths,
|
|
336
|
+
});
|
|
337
|
+
if (stagedMedia) {
|
|
338
|
+
manifestFile = stagedMedia.manifestFile;
|
|
339
|
+
payload = applyStagedWorkoutMedia({
|
|
340
|
+
payload,
|
|
341
|
+
eventId,
|
|
342
|
+
media: stagedMedia.media,
|
|
343
|
+
rawRefs: stagedMedia.rawRefs,
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
const result = await (async () => {
|
|
348
|
+
try {
|
|
349
|
+
return await upsertEventRecord({
|
|
350
|
+
vault: input.vault,
|
|
351
|
+
payload,
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
catch (error) {
|
|
355
|
+
if (manifestFile) {
|
|
356
|
+
await cleanupStagedWorkoutMediaBatch({
|
|
357
|
+
vault: input.vault,
|
|
358
|
+
manifestFile,
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
throw error;
|
|
362
|
+
}
|
|
363
|
+
})();
|
|
123
364
|
return {
|
|
124
365
|
...result,
|
|
125
|
-
occurredAt,
|
|
366
|
+
occurredAt: String(payload.occurredAt ?? input.occurredAt ?? new Date().toISOString()),
|
|
126
367
|
kind: 'activity_session',
|
|
127
|
-
title:
|
|
128
|
-
activityType:
|
|
129
|
-
durationMinutes:
|
|
130
|
-
distanceKm:
|
|
131
|
-
strengthExercises:
|
|
132
|
-
|
|
368
|
+
title: String(payload.title ?? ''),
|
|
369
|
+
activityType: String(payload.activityType ?? ''),
|
|
370
|
+
durationMinutes: Number(payload.durationMinutes ?? 1),
|
|
371
|
+
distanceKm: typeof payload.distanceKm === 'number' ? payload.distanceKm : null,
|
|
372
|
+
strengthExercises: Array.isArray(payload.strengthExercises)
|
|
373
|
+
? payload.strengthExercises
|
|
374
|
+
: null,
|
|
375
|
+
workout: normalizeStructuredWorkout(payload.workout) ?? null,
|
|
376
|
+
manifestFile,
|
|
377
|
+
note: String(payload.note ?? payload.title ?? ''),
|
|
133
378
|
};
|
|
134
379
|
}
|
|
135
380
|
export async function editWorkoutRecord(input) {
|
|
@@ -143,7 +388,7 @@ export async function editWorkoutRecord(input) {
|
|
|
143
388
|
dayKeyPolicy: input.dayKeyPolicy,
|
|
144
389
|
expectedKinds: ['activity_session'],
|
|
145
390
|
});
|
|
146
|
-
return
|
|
391
|
+
return showWorkoutRecord(input.vault, result.lookupId);
|
|
147
392
|
}
|
|
148
393
|
export async function deleteWorkoutRecord(input) {
|
|
149
394
|
return deleteEventRecord({
|
|
@@ -166,17 +411,22 @@ function resolveWorkoutActivityDescriptor(text, requestedActivityType) {
|
|
|
166
411
|
}
|
|
167
412
|
return {
|
|
168
413
|
activityType,
|
|
169
|
-
label:
|
|
414
|
+
label: requested,
|
|
170
415
|
};
|
|
171
416
|
}
|
|
172
|
-
|
|
417
|
+
const inferred = inferKnownWorkoutType(text);
|
|
418
|
+
if (inferred) {
|
|
419
|
+
return inferred;
|
|
420
|
+
}
|
|
421
|
+
return {
|
|
173
422
|
activityType: 'workout',
|
|
174
423
|
label: 'Workout',
|
|
175
424
|
};
|
|
176
425
|
}
|
|
177
426
|
function inferKnownWorkoutType(text) {
|
|
427
|
+
const normalized = text.toLowerCase();
|
|
178
428
|
for (const candidate of knownWorkoutTypes) {
|
|
179
|
-
if (candidate.patterns.some((pattern) => pattern.test(
|
|
429
|
+
if (candidate.patterns.some((pattern) => pattern.test(normalized))) {
|
|
180
430
|
return {
|
|
181
431
|
activityType: candidate.activityType,
|
|
182
432
|
label: candidate.label,
|
|
@@ -185,42 +435,47 @@ function inferKnownWorkoutType(text) {
|
|
|
185
435
|
}
|
|
186
436
|
return null;
|
|
187
437
|
}
|
|
188
|
-
function
|
|
189
|
-
|
|
190
|
-
|
|
438
|
+
function slugifyWorkoutType(value) {
|
|
439
|
+
const normalized = value
|
|
440
|
+
.trim()
|
|
441
|
+
.toLowerCase()
|
|
442
|
+
.replace(/[^a-z0-9]+/gu, '-')
|
|
443
|
+
.replace(/^-+|-+$/gu, '');
|
|
444
|
+
return normalized.length > 0 ? normalized : null;
|
|
445
|
+
}
|
|
446
|
+
function resolveDurationMinutes(text, explicitDurationMinutes) {
|
|
447
|
+
if (typeof explicitDurationMinutes === 'number') {
|
|
448
|
+
return validateDurationMinutes(explicitDurationMinutes);
|
|
191
449
|
}
|
|
192
450
|
const inferred = inferDurationMinutes(text);
|
|
193
|
-
if (inferred === 'ambiguous') {
|
|
194
|
-
throw new VaultCliError('invalid_option', 'Workout duration is ambiguous in the note. Pass --duration <minutes> to record it explicitly.');
|
|
195
|
-
}
|
|
196
451
|
if (typeof inferred === 'number') {
|
|
197
452
|
return inferred;
|
|
198
453
|
}
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
function resolveDistanceKm(text, requestedDistanceKm) {
|
|
202
|
-
if (typeof requestedDistanceKm === 'number') {
|
|
203
|
-
return validateDistanceKm(requestedDistanceKm);
|
|
454
|
+
if (inferred === 'ambiguous') {
|
|
455
|
+
throw new VaultCliError('invalid_option', 'Workout duration is ambiguous. Pass --duration <minutes> to record it explicitly.');
|
|
204
456
|
}
|
|
205
|
-
return
|
|
457
|
+
return 30;
|
|
206
458
|
}
|
|
207
|
-
function
|
|
208
|
-
if (
|
|
209
|
-
return
|
|
459
|
+
function resolveDistanceKm(text, explicitDistanceKm) {
|
|
460
|
+
if (typeof explicitDistanceKm === 'number' && Number.isFinite(explicitDistanceKm)) {
|
|
461
|
+
return explicitDistanceKm > 0 ? explicitDistanceKm : undefined;
|
|
210
462
|
}
|
|
211
|
-
|
|
463
|
+
if (!text || ambiguousDistancePattern.test(text)) {
|
|
464
|
+
return undefined;
|
|
465
|
+
}
|
|
466
|
+
const kilometerMatch = kilometerDistancePattern.exec(text);
|
|
212
467
|
if (kilometerMatch) {
|
|
213
|
-
return
|
|
468
|
+
return parseFloat(kilometerMatch[1]);
|
|
214
469
|
}
|
|
215
|
-
const
|
|
216
|
-
if (
|
|
217
|
-
return
|
|
470
|
+
const shortKilometerMatch = kilometerShortDistancePattern.exec(text);
|
|
471
|
+
if (shortKilometerMatch) {
|
|
472
|
+
return parseFloat(shortKilometerMatch[1]);
|
|
218
473
|
}
|
|
219
|
-
const mileMatch =
|
|
474
|
+
const mileMatch = mileDistancePattern.exec(text);
|
|
220
475
|
if (mileMatch) {
|
|
221
|
-
return
|
|
476
|
+
return parseFloat(mileMatch[1]) * MILES_TO_KM;
|
|
222
477
|
}
|
|
223
|
-
return
|
|
478
|
+
return undefined;
|
|
224
479
|
}
|
|
225
480
|
function inferStrengthExercises(text, activityType) {
|
|
226
481
|
if (activityType !== 'strength-training') {
|
|
@@ -228,184 +483,75 @@ function inferStrengthExercises(text, activityType) {
|
|
|
228
483
|
}
|
|
229
484
|
const exercises = [];
|
|
230
485
|
for (const match of text.matchAll(strengthExercisePattern)) {
|
|
231
|
-
const
|
|
232
|
-
const
|
|
233
|
-
const
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
setCount < 1 ||
|
|
237
|
-
!Number.isInteger(repsPerSet) ||
|
|
238
|
-
repsPerSet < 1) {
|
|
486
|
+
const [, rawSetCount, rawRepsPerSet, rawDescription] = match;
|
|
487
|
+
const setCount = Number.parseInt(rawSetCount ?? '', 10);
|
|
488
|
+
const repsPerSet = Number.parseInt(rawRepsPerSet ?? '', 10);
|
|
489
|
+
const details = parseStrengthExerciseDetails(rawDescription ?? '');
|
|
490
|
+
if (!Number.isFinite(setCount) || !Number.isFinite(repsPerSet) || !details) {
|
|
239
491
|
continue;
|
|
240
492
|
}
|
|
241
|
-
exercises.push(
|
|
493
|
+
exercises.push(compactObject({
|
|
494
|
+
exercise: details.exercise,
|
|
495
|
+
setCount,
|
|
496
|
+
repsPerSet,
|
|
497
|
+
load: details.load,
|
|
498
|
+
loadUnit: details.loadUnit,
|
|
499
|
+
loadDescription: details.loadDescription,
|
|
500
|
+
}));
|
|
242
501
|
}
|
|
243
502
|
return exercises.length > 0 ? exercises : null;
|
|
244
503
|
}
|
|
245
|
-
function parseStrengthExerciseDetails(
|
|
246
|
-
const
|
|
247
|
-
if (!
|
|
504
|
+
function parseStrengthExerciseDetails(rawDescription) {
|
|
505
|
+
const description = normalizeOptionalText(rawDescription);
|
|
506
|
+
if (!description) {
|
|
248
507
|
return null;
|
|
249
508
|
}
|
|
250
|
-
const
|
|
251
|
-
if (
|
|
252
|
-
const
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
const
|
|
257
|
-
const
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
const plateWeight = Number.parseFloat(barbellLoadMatch[4] ?? '');
|
|
261
|
-
if (barUnit &&
|
|
262
|
-
plateUnit &&
|
|
263
|
-
barUnit === plateUnit &&
|
|
264
|
-
Number.isFinite(barWeight) &&
|
|
265
|
-
Number.isFinite(plateWeight)) {
|
|
266
|
-
const load = normalizeStrengthLoadValue(barWeight + (plateWeight * 2));
|
|
267
|
-
return loadDescription
|
|
268
|
-
? {
|
|
269
|
-
exercise,
|
|
270
|
-
load,
|
|
271
|
-
loadUnit: barUnit,
|
|
272
|
-
loadDescription,
|
|
273
|
-
}
|
|
274
|
-
: {
|
|
275
|
-
exercise,
|
|
276
|
-
load,
|
|
277
|
-
loadUnit: barUnit,
|
|
278
|
-
};
|
|
279
|
-
}
|
|
280
|
-
return loadDescription
|
|
281
|
-
? {
|
|
282
|
-
exercise,
|
|
283
|
-
loadDescription,
|
|
284
|
-
}
|
|
285
|
-
: {
|
|
509
|
+
const barbellMatch = description.match(strengthBarbellLoadPattern);
|
|
510
|
+
if (barbellMatch) {
|
|
511
|
+
const [, rawExercise, rawBarWeight, rawBarUnit, rawPlateWeight, rawPlateUnit] = barbellMatch;
|
|
512
|
+
const exercise = normalizeOptionalText(rawExercise);
|
|
513
|
+
const barWeight = Number.parseFloat(rawBarWeight ?? '');
|
|
514
|
+
const plateWeight = Number.parseFloat(rawPlateWeight ?? '');
|
|
515
|
+
const barUnit = normalizeLoadUnit(rawBarUnit);
|
|
516
|
+
const plateUnit = normalizeLoadUnit(rawPlateUnit);
|
|
517
|
+
if (exercise && Number.isFinite(barWeight) && Number.isFinite(plateWeight) && barUnit && plateUnit && barUnit === plateUnit) {
|
|
518
|
+
return {
|
|
286
519
|
exercise,
|
|
520
|
+
load: barWeight + plateWeight * 2,
|
|
521
|
+
loadUnit: barUnit,
|
|
522
|
+
loadDescription: `${barWeight} ${barUnit} bar plus ${plateWeight} ${plateUnit} plates on both sides`,
|
|
287
523
|
};
|
|
524
|
+
}
|
|
288
525
|
}
|
|
289
|
-
const simpleLoadMatch =
|
|
526
|
+
const simpleLoadMatch = description.match(strengthSimpleLoadPattern);
|
|
290
527
|
if (simpleLoadMatch) {
|
|
291
|
-
const
|
|
292
|
-
const
|
|
293
|
-
const load = Number.parseFloat(
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
if (loadUnit && Number.isFinite(load)) {
|
|
298
|
-
const normalizedLoad = normalizeStrengthLoadValue(load);
|
|
299
|
-
const loadDescription = extractStrengthLoadDescription(normalized, simpleLoadMatch[1] ?? '');
|
|
300
|
-
return loadDescription
|
|
301
|
-
? {
|
|
302
|
-
exercise,
|
|
303
|
-
load: normalizedLoad,
|
|
304
|
-
loadUnit,
|
|
305
|
-
loadDescription,
|
|
306
|
-
}
|
|
307
|
-
: {
|
|
308
|
-
exercise,
|
|
309
|
-
load: normalizedLoad,
|
|
310
|
-
loadUnit,
|
|
311
|
-
};
|
|
312
|
-
}
|
|
313
|
-
const loadDescription = extractStrengthLoadDescription(normalized, simpleLoadMatch[1] ?? '');
|
|
314
|
-
return loadDescription
|
|
315
|
-
? {
|
|
316
|
-
exercise,
|
|
317
|
-
loadDescription,
|
|
318
|
-
}
|
|
319
|
-
: {
|
|
528
|
+
const [, rawExercise, rawLoad, rawUnit] = simpleLoadMatch;
|
|
529
|
+
const exercise = normalizeOptionalText(rawExercise);
|
|
530
|
+
const load = Number.parseFloat(rawLoad ?? '');
|
|
531
|
+
const loadUnit = normalizeLoadUnit(rawUnit);
|
|
532
|
+
if (exercise && Number.isFinite(load) && loadUnit) {
|
|
533
|
+
return {
|
|
320
534
|
exercise,
|
|
535
|
+
load,
|
|
536
|
+
loadUnit,
|
|
321
537
|
};
|
|
538
|
+
}
|
|
322
539
|
}
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
}
|
|
327
|
-
return { exercise };
|
|
328
|
-
}
|
|
329
|
-
function buildStrengthExerciseRecord(details, setCount, repsPerSet) {
|
|
330
|
-
if (typeof details.load === 'number' && details.loadUnit) {
|
|
331
|
-
return compactObject({
|
|
332
|
-
exercise: details.exercise,
|
|
333
|
-
setCount,
|
|
334
|
-
repsPerSet,
|
|
335
|
-
load: details.load,
|
|
336
|
-
loadUnit: details.loadUnit,
|
|
337
|
-
loadDescription: details.loadDescription,
|
|
338
|
-
});
|
|
339
|
-
}
|
|
340
|
-
return compactObject({
|
|
341
|
-
exercise: details.exercise,
|
|
342
|
-
setCount,
|
|
343
|
-
repsPerSet,
|
|
344
|
-
loadDescription: details.loadDescription,
|
|
345
|
-
});
|
|
346
|
-
}
|
|
347
|
-
function normalizeExerciseFragment(value) {
|
|
348
|
-
return normalizeOptionalText(value.replace(/[.,;:!?]+$/gu, '').replace(/\s+/gu, ' '));
|
|
349
|
-
}
|
|
350
|
-
function normalizeStrengthExerciseName(value) {
|
|
351
|
-
const normalized = normalizeOptionalText(value
|
|
352
|
-
.replace(/[.,;:!?]+$/gu, '')
|
|
353
|
-
.replace(/^(?:an?|the)\s+/iu, '')
|
|
354
|
-
.replace(/\s+/gu, ' '));
|
|
355
|
-
return normalized ? normalized.toLowerCase() : null;
|
|
540
|
+
return {
|
|
541
|
+
exercise: description,
|
|
542
|
+
};
|
|
356
543
|
}
|
|
357
|
-
function
|
|
358
|
-
|
|
359
|
-
if (exerciseLength <= 0 || exerciseLength >= fragment.length) {
|
|
544
|
+
function normalizeLoadUnit(value) {
|
|
545
|
+
if (!value) {
|
|
360
546
|
return undefined;
|
|
361
547
|
}
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
.replace(/^\s+(?:with|at)\s+/iu, '')
|
|
365
|
-
.replace(/^(?:an?\s+)/iu, ''));
|
|
366
|
-
}
|
|
367
|
-
function normalizeStrengthLoadUnit(value) {
|
|
368
|
-
const normalized = value.trim().toLowerCase();
|
|
369
|
-
if (normalized === 'lb' ||
|
|
370
|
-
normalized === 'lbs' ||
|
|
371
|
-
normalized === 'pound' ||
|
|
372
|
-
normalized === 'pounds') {
|
|
548
|
+
const normalized = value.toLowerCase();
|
|
549
|
+
if (normalized.startsWith('lb') || normalized.startsWith('pound')) {
|
|
373
550
|
return 'lb';
|
|
374
551
|
}
|
|
375
|
-
if (normalized
|
|
376
|
-
normalized === 'kgs' ||
|
|
377
|
-
normalized === 'kilogram' ||
|
|
378
|
-
normalized === 'kilograms') {
|
|
552
|
+
if (normalized.startsWith('kg') || normalized.startsWith('kilo')) {
|
|
379
553
|
return 'kg';
|
|
380
554
|
}
|
|
381
555
|
return undefined;
|
|
382
556
|
}
|
|
383
|
-
function normalizeStrengthLoadValue(value) {
|
|
384
|
-
return Number(value.toFixed(3));
|
|
385
|
-
}
|
|
386
|
-
function validateDistanceKm(value) {
|
|
387
|
-
if (!Number.isFinite(value) || value <= 0) {
|
|
388
|
-
throw new VaultCliError('invalid_option', 'Workout distance must be a positive number of kilometers.');
|
|
389
|
-
}
|
|
390
|
-
return Number(value.toFixed(3));
|
|
391
|
-
}
|
|
392
|
-
function buildWorkoutTitle(label, durationMinutes) {
|
|
393
|
-
const loweredLabel = label.trim().toLowerCase() || 'workout';
|
|
394
|
-
const title = `${durationMinutes}-minute ${loweredLabel}`;
|
|
395
|
-
return title.slice(0, 160);
|
|
396
|
-
}
|
|
397
|
-
function humanizeRequestedWorkoutType(value) {
|
|
398
|
-
const normalized = value.trim().replace(/\s+/gu, ' ');
|
|
399
|
-
if (normalized.length === 0) {
|
|
400
|
-
return 'Workout';
|
|
401
|
-
}
|
|
402
|
-
return normalized[0].toUpperCase() + normalized.slice(1);
|
|
403
|
-
}
|
|
404
|
-
function slugifyWorkoutType(value) {
|
|
405
|
-
return value
|
|
406
|
-
.trim()
|
|
407
|
-
.toLowerCase()
|
|
408
|
-
.replace(/[^a-z0-9]+/gu, '-')
|
|
409
|
-
.replace(/^-+|-+$/gu, '');
|
|
410
|
-
}
|
|
411
557
|
//# sourceMappingURL=workout.js.map
|