@murphai/murph 0.1.1 → 0.1.14

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.
Files changed (88) hide show
  1. package/CHANGELOG.md +174 -0
  2. package/README.md +103 -60
  3. package/dist/.tsbuildinfo +1 -1
  4. package/dist/assistant/ui/ink.d.ts.map +1 -1
  5. package/dist/assistant/ui/ink.js +2 -3
  6. package/dist/assistant/ui/ink.js.map +1 -1
  7. package/dist/assistant-runtime.d.ts +0 -2
  8. package/dist/assistant-runtime.d.ts.map +1 -1
  9. package/dist/assistant-runtime.js +0 -1
  10. package/dist/assistant-runtime.js.map +1 -1
  11. package/dist/commands/device.js +1 -1
  12. package/dist/commands/device.js.map +1 -1
  13. package/dist/commands/export-intake-read-helpers.d.ts +1 -1
  14. package/dist/commands/knowledge.d.ts +3 -0
  15. package/dist/commands/knowledge.d.ts.map +1 -0
  16. package/dist/commands/knowledge.js +163 -0
  17. package/dist/commands/knowledge.js.map +1 -0
  18. package/dist/commands/wearables.d.ts +4985 -0
  19. package/dist/commands/wearables.d.ts.map +1 -0
  20. package/dist/commands/wearables.js +355 -0
  21. package/dist/commands/wearables.js.map +1 -0
  22. package/dist/commands/workout.d.ts.map +1 -1
  23. package/dist/commands/workout.js +330 -28
  24. package/dist/commands/workout.js.map +1 -1
  25. package/dist/incur-error-bridge.d.ts +2 -0
  26. package/dist/incur-error-bridge.d.ts.map +1 -0
  27. package/dist/incur-error-bridge.js +25 -0
  28. package/dist/incur-error-bridge.js.map +1 -0
  29. package/dist/incur.generated.d.ts +118 -1
  30. package/dist/incur.generated.d.ts.map +1 -1
  31. package/dist/index.d.ts +1 -0
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.js +1 -0
  34. package/dist/index.js.map +1 -1
  35. package/dist/knowledge-cli-contracts.d.ts +167 -0
  36. package/dist/knowledge-cli-contracts.d.ts.map +1 -0
  37. package/dist/knowledge-cli-contracts.js +73 -0
  38. package/dist/knowledge-cli-contracts.js.map +1 -0
  39. package/dist/research-runtime.d.ts +3 -40
  40. package/dist/research-runtime.d.ts.map +1 -1
  41. package/dist/research-runtime.js +54 -253
  42. package/dist/research-runtime.js.map +1 -1
  43. package/dist/review-gpt-runtime.d.ts +85 -0
  44. package/dist/review-gpt-runtime.d.ts.map +1 -0
  45. package/dist/review-gpt-runtime.js +239 -0
  46. package/dist/review-gpt-runtime.js.map +1 -0
  47. package/dist/setup-assistant.d.ts +1 -0
  48. package/dist/setup-assistant.d.ts.map +1 -1
  49. package/dist/setup-assistant.js +2 -1
  50. package/dist/setup-assistant.js.map +1 -1
  51. package/dist/setup-cli.d.ts.map +1 -1
  52. package/dist/setup-cli.js +10 -1
  53. package/dist/setup-cli.js.map +1 -1
  54. package/dist/setup-wizard.d.ts.map +1 -1
  55. package/dist/setup-wizard.js +26 -7
  56. package/dist/setup-wizard.js.map +1 -1
  57. package/dist/usecases/workout-artifacts.d.ts +21 -0
  58. package/dist/usecases/workout-artifacts.d.ts.map +1 -0
  59. package/dist/usecases/workout-artifacts.js +149 -0
  60. package/dist/usecases/workout-artifacts.js.map +1 -0
  61. package/dist/usecases/workout-format.d.ts +92 -10
  62. package/dist/usecases/workout-format.d.ts.map +1 -1
  63. package/dist/usecases/workout-format.js +211 -391
  64. package/dist/usecases/workout-format.js.map +1 -1
  65. package/dist/usecases/workout-import.d.ts +36 -0
  66. package/dist/usecases/workout-import.d.ts.map +1 -0
  67. package/dist/usecases/workout-import.js +587 -0
  68. package/dist/usecases/workout-import.js.map +1 -0
  69. package/dist/usecases/workout-measurement.d.ts +66 -0
  70. package/dist/usecases/workout-measurement.d.ts.map +1 -0
  71. package/dist/usecases/workout-measurement.js +285 -0
  72. package/dist/usecases/workout-measurement.js.map +1 -0
  73. package/dist/usecases/workout-model.d.ts +15 -0
  74. package/dist/usecases/workout-model.d.ts.map +1 -0
  75. package/dist/usecases/workout-model.js +161 -0
  76. package/dist/usecases/workout-model.js.map +1 -0
  77. package/dist/usecases/workout.d.ts +57 -20
  78. package/dist/usecases/workout.d.ts.map +1 -1
  79. package/dist/usecases/workout.js +360 -214
  80. package/dist/usecases/workout.js.map +1 -1
  81. package/dist/vault-cli-command-manifest.d.ts +5171 -3
  82. package/dist/vault-cli-command-manifest.d.ts.map +1 -1
  83. package/dist/vault-cli-command-manifest.js +140 -3
  84. package/dist/vault-cli-command-manifest.js.map +1 -1
  85. package/dist/vault-cli.d.ts.map +1 -1
  86. package/dist/vault-cli.js +2 -0
  87. package/dist/vault-cli.js.map +1 -1
  88. package/package.json +22 -13
@@ -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 { showEventRecord, upsertEventRecord, } from '@murphai/assistant-core/usecases/provider-event';
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.label, durationMinutes),
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
- export async function addWorkoutRecord(input) {
102
- const capture = resolveWorkoutCapture(input);
103
- const occurredAt = input.occurredAt ?? new Date().toISOString();
104
- const payload = {
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: capture.title,
109
- activityType: capture.activityType,
110
- durationMinutes: capture.durationMinutes,
111
- ...(typeof capture.distanceKm === 'number'
112
- ? { distanceKm: capture.distanceKm }
113
- : {}),
114
- ...(capture.strengthExercises
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
- const result = await upsertEventRecord({
120
- vault: input.vault,
121
- payload,
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: capture.title,
128
- activityType: capture.activityType,
129
- durationMinutes: capture.durationMinutes,
130
- distanceKm: capture.distanceKm,
131
- strengthExercises: capture.strengthExercises,
132
- note: capture.note,
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 showEventRecord(input.vault, result.lookupId);
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: humanizeRequestedWorkoutType(requested),
414
+ label: requested,
170
415
  };
171
416
  }
172
- return inferKnownWorkoutType(text) ?? {
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(text))) {
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 resolveDurationMinutes(text, requestedDurationMinutes) {
189
- if (typeof requestedDurationMinutes === 'number') {
190
- return validateDurationMinutes(requestedDurationMinutes, 'Workout duration');
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
- throw new VaultCliError('invalid_option', 'Could not infer a workout duration from the note. Pass --duration <minutes> to record it explicitly.');
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 inferDistanceKm(text);
457
+ return 30;
206
458
  }
207
- function inferDistanceKm(text) {
208
- if (ambiguousDistancePattern.test(text)) {
209
- return null;
459
+ function resolveDistanceKm(text, explicitDistanceKm) {
460
+ if (typeof explicitDistanceKm === 'number' && Number.isFinite(explicitDistanceKm)) {
461
+ return explicitDistanceKm > 0 ? explicitDistanceKm : undefined;
210
462
  }
211
- const kilometerMatch = text.match(kilometerDistancePattern);
463
+ if (!text || ambiguousDistancePattern.test(text)) {
464
+ return undefined;
465
+ }
466
+ const kilometerMatch = kilometerDistancePattern.exec(text);
212
467
  if (kilometerMatch) {
213
- return validateDistanceKm(Number.parseFloat(kilometerMatch[1] ?? ''));
468
+ return parseFloat(kilometerMatch[1]);
214
469
  }
215
- const kilometerShortMatch = text.match(kilometerShortDistancePattern);
216
- if (kilometerShortMatch) {
217
- return validateDistanceKm(Number.parseFloat(kilometerShortMatch[1] ?? ''));
470
+ const shortKilometerMatch = kilometerShortDistancePattern.exec(text);
471
+ if (shortKilometerMatch) {
472
+ return parseFloat(shortKilometerMatch[1]);
218
473
  }
219
- const mileMatch = text.match(mileDistancePattern);
474
+ const mileMatch = mileDistancePattern.exec(text);
220
475
  if (mileMatch) {
221
- return validateDistanceKm(Number.parseFloat(mileMatch[1] ?? '') * MILES_TO_KM);
476
+ return parseFloat(mileMatch[1]) * MILES_TO_KM;
222
477
  }
223
- return null;
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 setCount = Number.parseInt(match[1] ?? '', 10);
232
- const repsPerSet = Number.parseInt(match[2] ?? '', 10);
233
- const details = parseStrengthExerciseDetails(match[3] ?? '');
234
- if (!details ||
235
- !Number.isInteger(setCount) ||
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(buildStrengthExerciseRecord(details, setCount, repsPerSet));
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(value) {
246
- const normalized = normalizeExerciseFragment(value);
247
- if (!normalized) {
504
+ function parseStrengthExerciseDetails(rawDescription) {
505
+ const description = normalizeOptionalText(rawDescription);
506
+ if (!description) {
248
507
  return null;
249
508
  }
250
- const barbellLoadMatch = normalized.match(strengthBarbellLoadPattern);
251
- if (barbellLoadMatch) {
252
- const exercise = normalizeStrengthExerciseName(barbellLoadMatch[1] ?? '');
253
- if (!exercise) {
254
- return null;
255
- }
256
- const loadDescription = extractStrengthLoadDescription(normalized, barbellLoadMatch[1] ?? '');
257
- const barUnit = normalizeStrengthLoadUnit(barbellLoadMatch[3] ?? '');
258
- const plateUnit = normalizeStrengthLoadUnit(barbellLoadMatch[5] ?? '');
259
- const barWeight = Number.parseFloat(barbellLoadMatch[2] ?? '');
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 = normalized.match(strengthSimpleLoadPattern);
526
+ const simpleLoadMatch = description.match(strengthSimpleLoadPattern);
290
527
  if (simpleLoadMatch) {
291
- const exercise = normalizeStrengthExerciseName(simpleLoadMatch[1] ?? '');
292
- const loadUnit = normalizeStrengthLoadUnit(simpleLoadMatch[3] ?? '');
293
- const load = Number.parseFloat(simpleLoadMatch[2] ?? '');
294
- if (!exercise) {
295
- return null;
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
- const exercise = normalizeStrengthExerciseName(normalized);
324
- if (!exercise) {
325
- return null;
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 extractStrengthLoadDescription(fragment, exerciseText) {
358
- const exerciseLength = exerciseText.length;
359
- if (exerciseLength <= 0 || exerciseLength >= fragment.length) {
544
+ function normalizeLoadUnit(value) {
545
+ if (!value) {
360
546
  return undefined;
361
547
  }
362
- return normalizeOptionalText(fragment
363
- .slice(exerciseLength)
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 === 'kg' ||
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