@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.
Files changed (100) hide show
  1. package/CHANGELOG.md +154 -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 +164 -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 +2 -0
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.js +2 -0
  34. package/dist/index.js.map +1 -1
  35. package/dist/knowledge-cli-contracts.d.ts +179 -0
  36. package/dist/knowledge-cli-contracts.d.ts.map +1 -0
  37. package/dist/knowledge-cli-contracts.js +78 -0
  38. package/dist/knowledge-cli-contracts.js.map +1 -0
  39. package/dist/knowledge-documents.d.ts +44 -0
  40. package/dist/knowledge-documents.d.ts.map +1 -0
  41. package/dist/knowledge-documents.js +195 -0
  42. package/dist/knowledge-documents.js.map +1 -0
  43. package/dist/knowledge-lint.d.ts +11 -0
  44. package/dist/knowledge-lint.d.ts.map +1 -0
  45. package/dist/knowledge-lint.js +254 -0
  46. package/dist/knowledge-lint.js.map +1 -0
  47. package/dist/knowledge-runtime.d.ts +49 -0
  48. package/dist/knowledge-runtime.d.ts.map +1 -0
  49. package/dist/knowledge-runtime.js +227 -0
  50. package/dist/knowledge-runtime.js.map +1 -0
  51. package/dist/research-runtime.d.ts +3 -40
  52. package/dist/research-runtime.d.ts.map +1 -1
  53. package/dist/research-runtime.js +54 -253
  54. package/dist/research-runtime.js.map +1 -1
  55. package/dist/review-gpt-runtime.d.ts +85 -0
  56. package/dist/review-gpt-runtime.d.ts.map +1 -0
  57. package/dist/review-gpt-runtime.js +239 -0
  58. package/dist/review-gpt-runtime.js.map +1 -0
  59. package/dist/setup-assistant.d.ts +1 -0
  60. package/dist/setup-assistant.d.ts.map +1 -1
  61. package/dist/setup-assistant.js +2 -1
  62. package/dist/setup-assistant.js.map +1 -1
  63. package/dist/setup-cli.d.ts.map +1 -1
  64. package/dist/setup-cli.js +10 -1
  65. package/dist/setup-cli.js.map +1 -1
  66. package/dist/setup-wizard.d.ts.map +1 -1
  67. package/dist/setup-wizard.js +26 -7
  68. package/dist/setup-wizard.js.map +1 -1
  69. package/dist/usecases/workout-artifacts.d.ts +21 -0
  70. package/dist/usecases/workout-artifacts.d.ts.map +1 -0
  71. package/dist/usecases/workout-artifacts.js +149 -0
  72. package/dist/usecases/workout-artifacts.js.map +1 -0
  73. package/dist/usecases/workout-format.d.ts +92 -10
  74. package/dist/usecases/workout-format.d.ts.map +1 -1
  75. package/dist/usecases/workout-format.js +211 -391
  76. package/dist/usecases/workout-format.js.map +1 -1
  77. package/dist/usecases/workout-import.d.ts +36 -0
  78. package/dist/usecases/workout-import.d.ts.map +1 -0
  79. package/dist/usecases/workout-import.js +587 -0
  80. package/dist/usecases/workout-import.js.map +1 -0
  81. package/dist/usecases/workout-measurement.d.ts +66 -0
  82. package/dist/usecases/workout-measurement.d.ts.map +1 -0
  83. package/dist/usecases/workout-measurement.js +285 -0
  84. package/dist/usecases/workout-measurement.js.map +1 -0
  85. package/dist/usecases/workout-model.d.ts +15 -0
  86. package/dist/usecases/workout-model.d.ts.map +1 -0
  87. package/dist/usecases/workout-model.js +161 -0
  88. package/dist/usecases/workout-model.js.map +1 -0
  89. package/dist/usecases/workout.d.ts +57 -20
  90. package/dist/usecases/workout.d.ts.map +1 -1
  91. package/dist/usecases/workout.js +360 -214
  92. package/dist/usecases/workout.js.map +1 -1
  93. package/dist/vault-cli-command-manifest.d.ts +5179 -3
  94. package/dist/vault-cli-command-manifest.d.ts.map +1 -1
  95. package/dist/vault-cli-command-manifest.js +140 -3
  96. package/dist/vault-cli-command-manifest.js.map +1 -1
  97. package/dist/vault-cli.d.ts.map +1 -1
  98. package/dist/vault-cli.js +2 -0
  99. package/dist/vault-cli.js.map +1 -1
  100. package/package.json +22 -13
@@ -1,445 +1,265 @@
1
- import { readdir, readFile } from 'node:fs/promises';
2
- import { ID_PREFIXES, } from '@murphai/contracts';
3
- import { applyCanonicalWriteBatch, parseFrontmatterDocument, resolveVaultPathOnDisk, stringifyFrontmatterDocument, WORKOUT_FORMAT_DOC_TYPE, WORKOUT_FORMAT_SCHEMA_VERSION, WORKOUT_FORMATS_DIRECTORY, } from '@murphai/core';
4
- import { generateUlid } from '@murphai/runtime-state';
1
+ import { workoutFormatUpsertPayloadSchema, } from '@murphai/contracts';
2
+ import { listWorkoutFormats as listCoreWorkoutFormats, readWorkoutFormat as readCoreWorkoutFormat, upsertWorkoutFormat, } from '@murphai/core';
3
+ import { loadJsonInputObject } from '@murphai/assistant-core/json-input';
5
4
  import { VaultCliError } from '@murphai/assistant-core/vault-cli-errors';
6
5
  import { asListEnvelope } from '@murphai/assistant-core/usecases/shared';
7
- import { resolveWorkoutCapture, addWorkoutRecord, } from './workout.js';
8
- import { compactObject, normalizeOptionalText, } from '@murphai/assistant-core/usecases/vault-usecase-helpers';
9
- const LOAD_UNITS = new Set(['lb', 'kg']);
10
- const WORKOUT_FORMAT_RECORD_ID_PATTERN = new RegExp(`^${ID_PREFIXES.workoutFormat}_[0-9A-Za-z]+$`, 'u');
11
- export async function saveWorkoutFormat(input) {
12
- const title = normalizeOptionalText(input.name);
13
- if (!title) {
14
- throw new VaultCliError('contract_invalid', 'Workout format name is required.');
15
- }
16
- const slug = slugifyWorkoutFormatName(title);
17
- if (!slug) {
18
- throw new VaultCliError('contract_invalid', 'Workout format name must include at least one letter or number.');
19
- }
20
- const text = normalizeOptionalText(input.text);
21
- if (!text) {
22
- throw new VaultCliError('contract_invalid', 'Workout format text is required.');
6
+ import { normalizeOptionalText, toVaultCliError, } from '@murphai/assistant-core/usecases/vault-usecase-helpers';
7
+ import { addWorkoutRecord, resolveWorkoutCapture, } from './workout.js';
8
+ import { buildWorkoutSessionFromTemplate, } from './workout-model.js';
9
+ function requireTitle(value, label) {
10
+ const normalized = normalizeOptionalText(value);
11
+ if (!normalized) {
12
+ throw new VaultCliError('contract_invalid', `${label} is required.`);
23
13
  }
24
- const capture = validateWorkoutFormatDefaults({
25
- text,
26
- durationMinutes: input.durationMinutes,
27
- activityType: input.activityType,
28
- distanceKm: input.distanceKm,
29
- });
30
- const relativePath = formatWorkoutFormatPath(slug);
31
- const resolved = await resolveVaultPathOnDisk(input.vault, relativePath);
32
- const existingMarkdown = await readOptionalUtf8File(resolved.absolutePath);
33
- const existingRecord = existingMarkdown
34
- ? parseWorkoutFormatRecord(existingMarkdown, relativePath)
35
- : null;
36
- const created = existingRecord === null;
37
- const markdown = stringifyWorkoutFormatRecord({
38
- workoutFormatId: existingRecord?.workoutFormatId ?? createWorkoutFormatId(),
39
- title,
40
- slug,
41
- status: existingRecord?.status ?? 'active',
42
- summary: existingRecord?.summary,
43
- templateText: capture.note,
44
- activityType: capture.activityType,
45
- durationMinutes: capture.durationMinutes,
46
- distanceKm: capture.distanceKm ?? undefined,
47
- strengthExercises: capture.strengthExercises ?? undefined,
48
- tags: existingRecord?.tags,
49
- note: existingRecord?.note,
50
- relativePath,
51
- markdown: existingMarkdown ?? '',
52
- });
53
- await applyCanonicalWriteBatch({
54
- vaultRoot: input.vault,
55
- operationType: 'workout_format_save',
56
- summary: `Save workout format ${slug}`,
57
- textWrites: [
58
- {
59
- relativePath,
60
- content: markdown,
61
- overwrite: true,
62
- allowExistingMatch: true,
63
- },
64
- ],
65
- });
66
- return {
67
- vault: input.vault,
68
- name: title,
69
- slug,
70
- path: relativePath,
71
- created,
72
- };
73
- }
74
- export async function showWorkoutFormat(vault, name) {
75
- const record = await resolveWorkoutFormat(vault, name);
76
- return {
77
- vault,
78
- entity: toWorkoutFormatEntity(record, {
79
- includeMarkdown: true,
80
- }),
81
- };
82
- }
83
- export async function listWorkoutFormats(input) {
84
- const records = await loadWorkoutFormats(input.vault);
85
- const items = records.slice(0, input.limit).map((record) => toWorkoutFormatEntity(record, {
86
- includeMarkdown: false,
87
- }));
88
- return asListEnvelope(input.vault, {
89
- limit: input.limit,
90
- }, items);
14
+ return normalized;
91
15
  }
92
- export async function logWorkoutFormat(input) {
93
- const record = await resolveWorkoutFormat(input.vault, input.name);
94
- return addWorkoutRecord({
95
- vault: input.vault,
96
- text: requireWorkoutFormatTemplateText(record),
97
- durationMinutes: typeof input.durationMinutes === 'number'
98
- ? input.durationMinutes
99
- : record.durationMinutes,
100
- activityType: typeof input.activityType === 'string'
101
- ? input.activityType
102
- : record.activityType,
103
- distanceKm: typeof input.distanceKm === 'number'
104
- ? input.distanceKm
105
- : record.distanceKm,
106
- strengthExercises: record.strengthExercises ?? null,
107
- occurredAt: input.occurredAt,
108
- source: input.source,
109
- });
16
+ function valueAsString(value) {
17
+ return typeof value === 'string' ? value : undefined;
110
18
  }
111
- async function loadWorkoutFormats(vault) {
112
- const resolvedDirectory = await resolveVaultPathOnDisk(vault, WORKOUT_FORMATS_DIRECTORY);
113
- const records = [];
114
- try {
115
- const entries = await readdir(resolvedDirectory.absolutePath, {
116
- withFileTypes: true,
117
- encoding: 'utf8',
118
- });
119
- for (const entry of entries) {
120
- if (!entry.isFile() || !entry.name.endsWith('.md')) {
121
- continue;
122
- }
123
- const relativePath = `${WORKOUT_FORMATS_DIRECTORY}/${entry.name}`;
124
- const resolvedFile = await resolveVaultPathOnDisk(vault, relativePath);
125
- const markdown = await readFile(resolvedFile.absolutePath, 'utf8');
126
- records.push(parseWorkoutFormatRecord(markdown, relativePath));
127
- }
128
- }
129
- catch (error) {
130
- if (isMissingPathError(error)) {
131
- return [];
132
- }
133
- throw error;
134
- }
135
- return records.sort((left, right) => left.title.localeCompare(right.title) ||
136
- left.slug.localeCompare(right.slug));
19
+ function valueAsNumber(value) {
20
+ return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
137
21
  }
138
- async function resolveWorkoutFormat(vault, lookup) {
139
- const normalizedLookup = normalizeOptionalText(lookup);
140
- if (!normalizedLookup) {
141
- throw new VaultCliError('contract_invalid', 'Workout format name is required.');
142
- }
143
- const records = await loadWorkoutFormats(vault);
144
- if (isWorkoutFormatId(normalizedLookup)) {
145
- const idMatch = records.find((record) => record.workoutFormatId === normalizedLookup);
146
- if (idMatch) {
147
- return idMatch;
148
- }
149
- }
150
- const directSlug = slugifyWorkoutFormatName(normalizedLookup);
151
- if (directSlug) {
152
- const directRecord = await readWorkoutFormatBySlug(vault, directSlug);
153
- if (directRecord) {
154
- return directRecord;
155
- }
156
- }
157
- const slugMatch = records.find((record) => record.slug === normalizedLookup ||
158
- (directSlug !== '' && record.slug === directSlug));
159
- if (slugMatch) {
160
- return slugMatch;
161
- }
162
- const titleMatches = records.filter((record) => record.title.toLowerCase() === normalizedLookup.toLowerCase());
163
- if (titleMatches.length === 1) {
164
- return titleMatches[0];
165
- }
166
- if (titleMatches.length > 1) {
167
- throw new VaultCliError('conflict', `Multiple workout formats match "${normalizedLookup}". Use the saved slug instead.`);
168
- }
169
- throw new VaultCliError('not_found', `Workout format "${normalizedLookup}" was not found.`);
170
- }
171
- async function readWorkoutFormatBySlug(vault, slug) {
172
- const relativePath = formatWorkoutFormatPath(slug);
173
- const resolvedFile = await resolveVaultPathOnDisk(vault, relativePath);
174
- const markdown = await readOptionalUtf8File(resolvedFile.absolutePath);
175
- if (!markdown) {
176
- return null;
177
- }
178
- return parseWorkoutFormatRecord(markdown, relativePath);
179
- }
180
- function parseWorkoutFormatRecord(markdown, relativePath) {
181
- const parsed = parseFrontmatterDocument(markdown);
182
- const attributes = parsed.attributes;
183
- const schemaVersion = normalizeOptionalText(String(attributes.schemaVersion ?? ''));
184
- const docType = normalizeOptionalText(String(attributes.docType ?? ''));
185
- if (schemaVersion !== WORKOUT_FORMAT_SCHEMA_VERSION) {
186
- throw new VaultCliError('contract_invalid', `Workout format document "${relativePath}" has an unexpected schemaVersion.`);
187
- }
188
- if (docType !== WORKOUT_FORMAT_DOC_TYPE) {
189
- throw new VaultCliError('contract_invalid', `Workout format document "${relativePath}" has an unexpected docType.`);
22
+ function buildTemplateFromCapture(capture) {
23
+ if (!capture.strengthExercises || capture.strengthExercises.length === 0) {
24
+ return undefined;
190
25
  }
191
- const title = requireWorkoutFormatString(attributes.title, 'title', relativePath);
192
- const slug = requireWorkoutFormatString(attributes.slug, 'slug', relativePath);
193
- const workoutFormatId = requireWorkoutFormatString(attributes.workoutFormatId, 'workoutFormatId', relativePath);
194
- const templateText = optionalWorkoutFormatString(attributes.templateText);
195
- const activityType = requireWorkoutFormatString(attributes.activityType, 'activityType', relativePath);
196
- const durationMinutes = optionalWorkoutFormatPositiveInteger(attributes.durationMinutes, 'durationMinutes', relativePath);
197
- const distanceKm = optionalWorkoutFormatPositiveNumber(attributes.distanceKm, 'distanceKm', relativePath);
198
- const strengthExercises = optionalWorkoutFormatStrengthExercises(attributes.strengthExercises, relativePath);
199
26
  return {
200
- workoutFormatId,
201
- title,
202
- slug,
203
- status: optionalWorkoutFormatString(attributes.status) ?? 'active',
204
- summary: optionalWorkoutFormatString(attributes.summary),
205
- templateText,
206
- activityType,
207
- durationMinutes,
208
- distanceKm,
209
- strengthExercises,
210
- tags: optionalWorkoutFormatTags(attributes.tags, relativePath),
211
- note: optionalWorkoutFormatString(attributes.note),
212
- relativePath,
213
- markdown,
27
+ routineNote: capture.note,
28
+ exercises: capture.strengthExercises.map((exercise, index) => ({
29
+ name: exercise.exercise,
30
+ order: index + 1,
31
+ mode: 'weight_reps',
32
+ ...(exercise.loadDescription
33
+ ? { note: exercise.loadDescription }
34
+ : {}),
35
+ plannedSets: Array.from({ length: exercise.setCount }, (_, setIndex) => ({
36
+ order: setIndex + 1,
37
+ targetReps: exercise.repsPerSet,
38
+ ...('load' in exercise
39
+ ? {
40
+ targetWeight: exercise.load,
41
+ targetWeightUnit: exercise.loadUnit,
42
+ }
43
+ : {}),
44
+ })),
45
+ })),
214
46
  };
215
47
  }
216
- function stringifyWorkoutFormatRecord(record) {
217
- const text = requireWorkoutFormatTemplateText(record);
218
- const body = [
219
- `# ${record.title}`,
220
- '',
221
- `- Status: ${record.status}`,
222
- `- Activity type: ${record.activityType}`,
223
- `- Default duration: ${record.durationMinutes ?? 'none'}${record.durationMinutes === undefined ? '' : ' min'}`,
224
- `- Default distance: ${record.distanceKm ?? 'none'}${record.distanceKm === undefined ? '' : ' km'}`,
225
- '',
226
- ...(record.summary ? ['## Summary', '', record.summary, ''] : []),
227
- ...(record.strengthExercises?.length
228
- ? [
229
- '## Strength Exercises',
230
- '',
231
- ...record.strengthExercises.map((exercise) => `- ${formatStrengthExerciseLine(exercise)}`),
232
- '',
233
- ]
234
- : []),
235
- '## Saved workout text',
236
- '',
237
- text,
238
- '',
239
- ...(record.tags?.length
240
- ? ['## Tags', '', ...record.tags.map((tag) => `- ${tag}`), '']
241
- : []),
242
- ...(record.note ? ['## Notes', '', record.note, ''] : []),
243
- ].join('\n');
244
- return stringifyFrontmatterDocument({
245
- attributes: compactObject({
246
- schemaVersion: WORKOUT_FORMAT_SCHEMA_VERSION,
247
- docType: WORKOUT_FORMAT_DOC_TYPE,
248
- workoutFormatId: record.workoutFormatId,
249
- slug: record.slug,
250
- title: record.title,
251
- status: record.status,
252
- summary: record.summary,
253
- activityType: record.activityType,
254
- durationMinutes: record.durationMinutes,
255
- distanceKm: record.distanceKm,
256
- strengthExercises: record.strengthExercises,
257
- tags: record.tags,
258
- note: record.note,
259
- templateText: text,
260
- }),
261
- body,
262
- });
263
- }
264
- function toWorkoutFormatEntity(record, options) {
48
+ function toWorkoutFormatEntity(record, includeMarkdown) {
265
49
  return {
266
50
  id: record.workoutFormatId,
267
51
  kind: 'workout_format',
268
52
  title: record.title,
269
53
  occurredAt: null,
270
54
  path: record.relativePath,
271
- markdown: options.includeMarkdown ? record.markdown : null,
272
- data: compactObject({
55
+ markdown: includeMarkdown ? record.markdown : null,
56
+ data: {
273
57
  workoutFormatId: record.workoutFormatId,
274
58
  slug: record.slug,
59
+ title: record.title,
60
+ status: record.status,
275
61
  summary: record.summary,
276
- text: record.templateText,
277
- templateText: record.templateText,
278
- type: record.activityType,
279
62
  activityType: record.activityType,
280
63
  durationMinutes: record.durationMinutes,
281
64
  distanceKm: record.distanceKm,
282
65
  strengthExercises: record.strengthExercises,
66
+ template: record.template,
283
67
  tags: record.tags,
284
68
  note: record.note,
285
- status: record.status,
286
- }),
69
+ text: record.templateText,
70
+ templateText: record.templateText,
71
+ },
287
72
  links: [],
288
73
  };
289
74
  }
290
- function validateWorkoutFormatDefaults(input) {
75
+ async function loadWorkoutFormats(vault) {
291
76
  try {
292
- return resolveWorkoutCapture({
293
- text: input.text,
294
- durationMinutes: input.durationMinutes,
295
- activityType: input.activityType,
296
- distanceKm: input.distanceKm,
297
- });
77
+ return await listCoreWorkoutFormats(vault);
298
78
  }
299
79
  catch (error) {
300
- if (!(error instanceof VaultCliError)) {
301
- throw error;
302
- }
303
- throw new VaultCliError(error.code, `Workout format defaults are invalid: ${error.message}`);
80
+ throw toWorkoutFormatCliError(error);
304
81
  }
305
82
  }
306
- function requireWorkoutFormatTemplateText(record) {
307
- if (record.templateText) {
308
- return record.templateText;
309
- }
310
- throw new VaultCliError('contract_invalid', `Workout format document "${record.relativePath}" is missing templateText.`);
311
- }
312
- function requireWorkoutFormatString(value, fieldName, relativePath) {
313
- const normalized = normalizeOptionalText(typeof value === 'string' ? value : undefined);
314
- if (!normalized) {
315
- throw new VaultCliError('contract_invalid', `Workout format document "${relativePath}" is missing ${fieldName}.`);
83
+ async function resolveWorkoutFormat(vault, lookup) {
84
+ const normalizedLookup = normalizeOptionalText(lookup);
85
+ if (!normalizedLookup) {
86
+ throw new VaultCliError('contract_invalid', 'Workout format lookup is required.');
316
87
  }
317
- return normalized;
318
- }
319
- function optionalWorkoutFormatString(value) {
320
- return typeof value === 'string' ? normalizeOptionalText(value) ?? undefined : undefined;
321
- }
322
- function optionalWorkoutFormatTags(value, relativePath) {
323
- if (value === undefined || value === null) {
324
- return undefined;
88
+ if (/^wfmt_[0-9A-Za-z]+$/u.test(normalizedLookup)) {
89
+ try {
90
+ return await readCoreWorkoutFormat({
91
+ vaultRoot: vault,
92
+ workoutFormatId: normalizedLookup,
93
+ });
94
+ }
95
+ catch (error) {
96
+ throw toWorkoutFormatCliError(error);
97
+ }
325
98
  }
326
- if (!Array.isArray(value)) {
327
- throw new VaultCliError('contract_invalid', `Workout format document "${relativePath}" has invalid tags.`);
99
+ const records = await loadWorkoutFormats(vault);
100
+ const slugMatch = records.find((record) => record.slug === normalizedLookup);
101
+ if (slugMatch) {
102
+ return slugMatch;
328
103
  }
329
- const tags = [...new Set(value.map((entry) => {
330
- if (typeof entry !== 'string') {
331
- throw new VaultCliError('contract_invalid', `Workout format document "${relativePath}" has invalid tags.`);
332
- }
333
- const normalized = normalizeOptionalText(entry);
334
- if (!normalized) {
335
- throw new VaultCliError('contract_invalid', `Workout format document "${relativePath}" has invalid tags.`);
336
- }
337
- return normalized;
338
- }))];
339
- return tags.length > 0 ? tags : undefined;
340
- }
341
- function optionalWorkoutFormatPositiveInteger(value, fieldName, relativePath) {
342
- if (value === undefined || value === null) {
343
- return undefined;
104
+ const titleMatches = records.filter((record) => normalizeOptionalText(record.title)?.toLowerCase() === normalizedLookup.toLowerCase());
105
+ if (titleMatches.length > 1) {
106
+ throw new VaultCliError('command_failed', `Multiple workout formats match "${normalizedLookup}". Use the saved slug instead.`);
344
107
  }
345
- if (!Number.isInteger(value) || Number(value) <= 0) {
346
- throw new VaultCliError('contract_invalid', `Workout format document "${relativePath}" has an invalid ${fieldName}.`);
108
+ if (titleMatches[0]) {
109
+ return titleMatches[0];
347
110
  }
348
- return Number(value);
111
+ throw new VaultCliError('not_found', `No workout format found for "${normalizedLookup}".`);
349
112
  }
350
- function optionalWorkoutFormatPositiveNumber(value, fieldName, relativePath) {
351
- if (value === undefined || value === null) {
352
- return undefined;
113
+ function formatSchemaIssues(issues) {
114
+ return issues
115
+ .map((issue) => {
116
+ const path = issue.path.length > 0 ? issue.path.join('.') : 'value';
117
+ return `${path}: ${issue.message}`;
118
+ })
119
+ .join('; ');
120
+ }
121
+ function toWorkoutFormatCliError(error) {
122
+ if (error instanceof Error && error.message === 'workoutFormatId is required.') {
123
+ return new VaultCliError('contract_invalid', 'Workout format document is missing workoutFormatId.');
353
124
  }
354
- if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) {
355
- throw new VaultCliError('contract_invalid', `Workout format document "${relativePath}" has an invalid ${fieldName}.`);
125
+ return toVaultCliError(error, {
126
+ VAULT_INVALID_INPUT: { code: 'contract_invalid' },
127
+ VAULT_INVALID_WORKOUT_FORMAT: { code: 'contract_invalid' },
128
+ VAULT_WORKOUT_FORMAT_MISSING: { code: 'not_found' },
129
+ VAULT_WORKOUT_FORMAT_CONFLICT: { code: 'command_failed' },
130
+ });
131
+ }
132
+ function normalizeStructuredWorkoutFormatPayload(payload, fallbackName) {
133
+ const candidate = {
134
+ ...payload,
135
+ title: valueAsString(payload.title) ?? fallbackName,
136
+ activityType: valueAsString(payload.activityType) ?? 'strength-training',
137
+ };
138
+ const parsed = workoutFormatUpsertPayloadSchema.safeParse(candidate);
139
+ if (!parsed.success) {
140
+ throw new VaultCliError('invalid_payload', `Workout format payload is invalid. ${formatSchemaIssues(parsed.error.issues)}`);
356
141
  }
357
- return Number(value.toFixed(3));
142
+ return parsed.data;
358
143
  }
359
- function optionalWorkoutFormatStrengthExercises(value, relativePath) {
360
- if (value === undefined || value === null) {
361
- return undefined;
144
+ export async function saveWorkoutFormat(input) {
145
+ let payload;
146
+ if (typeof input.inputFile === 'string') {
147
+ payload = normalizeStructuredWorkoutFormatPayload(await loadJsonInputObject(input.inputFile, 'workout format payload'), input.name);
362
148
  }
363
- if (!Array.isArray(value)) {
364
- throw new VaultCliError('contract_invalid', `Workout format document "${relativePath}" has invalid strengthExercises.`);
149
+ else {
150
+ const title = requireTitle(input.name, 'Workout format name');
151
+ const text = requireTitle(input.text, 'Workout format text');
152
+ const capture = resolveWorkoutCapture({
153
+ text,
154
+ durationMinutes: input.durationMinutes,
155
+ activityType: input.activityType,
156
+ distanceKm: input.distanceKm,
157
+ });
158
+ const template = buildTemplateFromCapture(capture);
159
+ payload = {
160
+ title,
161
+ status: 'active',
162
+ summary: undefined,
163
+ activityType: capture.activityType,
164
+ durationMinutes: capture.durationMinutes,
165
+ distanceKm: capture.distanceKm ?? undefined,
166
+ strengthExercises: capture.strengthExercises ?? undefined,
167
+ template,
168
+ note: undefined,
169
+ templateText: text,
170
+ };
365
171
  }
366
- const exercises = value.map((entry, index) => {
367
- if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
368
- throw new VaultCliError('contract_invalid', `Workout format document "${relativePath}" has invalid strengthExercises[${index}].`);
369
- }
370
- const exercise = requireWorkoutFormatString('exercise' in entry ? entry.exercise : undefined, `strengthExercises[${index}].exercise`, relativePath);
371
- const setCount = optionalWorkoutFormatPositiveInteger('setCount' in entry ? entry.setCount : undefined, `strengthExercises[${index}].setCount`, relativePath);
372
- const repsPerSet = optionalWorkoutFormatPositiveInteger('repsPerSet' in entry ? entry.repsPerSet : undefined, `strengthExercises[${index}].repsPerSet`, relativePath);
373
- const load = 'load' in entry ? optionalWorkoutFormatPositiveNumber(entry.load, `strengthExercises[${index}].load`, relativePath) : undefined;
374
- const loadUnit = 'loadUnit' in entry && typeof entry.loadUnit === 'string' && LOAD_UNITS.has(entry.loadUnit)
375
- ? entry.loadUnit
376
- : undefined;
377
- const loadDescription = 'loadDescription' in entry
378
- ? optionalWorkoutFormatString(entry.loadDescription)
379
- : undefined;
380
- if (setCount === undefined || repsPerSet === undefined) {
381
- throw new VaultCliError('contract_invalid', `Workout format document "${relativePath}" has invalid strengthExercises[${index}].`);
382
- }
383
- if ((load === undefined) !== (loadUnit === undefined)) {
384
- throw new VaultCliError('contract_invalid', `Workout format document "${relativePath}" has invalid strengthExercises[${index}].`);
385
- }
386
- return compactObject({
387
- exercise,
388
- setCount,
389
- repsPerSet,
390
- load,
391
- loadUnit,
392
- loadDescription,
172
+ let result;
173
+ try {
174
+ result = await upsertWorkoutFormat({
175
+ vaultRoot: input.vault,
176
+ workoutFormatId: payload.workoutFormatId,
177
+ slug: payload.slug,
178
+ title: payload.title,
179
+ status: payload.status,
180
+ summary: payload.summary,
181
+ activityType: payload.activityType,
182
+ durationMinutes: payload.durationMinutes,
183
+ distanceKm: payload.distanceKm,
184
+ strengthExercises: payload.strengthExercises,
185
+ template: payload.template,
186
+ tags: payload.tags,
187
+ note: payload.note,
188
+ templateText: payload.templateText,
393
189
  });
394
- });
395
- return exercises.length > 0 ? exercises : undefined;
396
- }
397
- function formatStrengthExerciseLine(exercise) {
398
- const parts = [
399
- `${exercise.exercise} — ${exercise.setCount} sets x ${exercise.repsPerSet} reps`,
400
- ];
401
- if ('load' in exercise &&
402
- exercise.load !== undefined &&
403
- exercise.loadUnit &&
404
- LOAD_UNITS.has(exercise.loadUnit)) {
405
- parts.push(`load: ${exercise.load} ${exercise.loadUnit}`);
406
190
  }
407
- if (exercise.loadDescription) {
408
- parts.push(exercise.loadDescription);
191
+ catch (error) {
192
+ throw toWorkoutFormatCliError(error);
409
193
  }
410
- return parts.join('; ');
411
- }
412
- function slugifyWorkoutFormatName(value) {
413
- return value
414
- .trim()
415
- .toLowerCase()
416
- .replace(/[^a-z0-9]+/gu, '-')
417
- .replace(/^-+|-+$/gu, '');
418
- }
419
- function formatWorkoutFormatPath(slug) {
420
- return `${WORKOUT_FORMATS_DIRECTORY}/${slug}.md`;
194
+ return {
195
+ vault: input.vault,
196
+ name: result.record.title,
197
+ slug: result.record.slug,
198
+ path: result.record.relativePath,
199
+ created: result.created,
200
+ };
421
201
  }
422
- function isWorkoutFormatId(value) {
423
- return WORKOUT_FORMAT_RECORD_ID_PATTERN.test(value);
202
+ export async function showWorkoutFormat(vault, name) {
203
+ const record = await resolveWorkoutFormat(vault, name);
204
+ return {
205
+ vault,
206
+ entity: toWorkoutFormatEntity(record, true),
207
+ };
424
208
  }
425
- function createWorkoutFormatId() {
426
- return `${ID_PREFIXES.workoutFormat}_${generateUlid()}`;
209
+ export async function listWorkoutFormats(input) {
210
+ const records = await loadWorkoutFormats(input.vault);
211
+ const items = records.slice(0, input.limit).map((record) => toWorkoutFormatEntity(record, false));
212
+ return asListEnvelope(input.vault, {
213
+ limit: input.limit,
214
+ }, items);
427
215
  }
428
- async function readOptionalUtf8File(absolutePath) {
429
- try {
430
- return await readFile(absolutePath, 'utf8');
216
+ export async function logWorkoutFormat(input) {
217
+ const record = await resolveWorkoutFormat(input.vault, input.name);
218
+ if (record.template) {
219
+ const templateNote = record.templateText
220
+ ?? record.template.routineNote
221
+ ?? record.title;
222
+ return addWorkoutRecord({
223
+ vault: input.vault,
224
+ workout: buildWorkoutSessionFromTemplate(record.template, {
225
+ routineId: record.workoutFormatId,
226
+ routineName: record.title,
227
+ }),
228
+ text: templateNote,
229
+ durationMinutes: typeof input.durationMinutes === 'number'
230
+ ? input.durationMinutes
231
+ : record.durationMinutes,
232
+ activityType: typeof input.activityType === 'string'
233
+ ? input.activityType
234
+ : record.activityType,
235
+ distanceKm: typeof input.distanceKm === 'number'
236
+ ? input.distanceKm
237
+ : record.distanceKm,
238
+ occurredAt: input.occurredAt,
239
+ source: input.source,
240
+ mediaPaths: input.mediaPaths,
241
+ });
431
242
  }
432
- catch (error) {
433
- if (isMissingPathError(error)) {
434
- return null;
435
- }
436
- throw error;
243
+ if (record.templateText) {
244
+ return addWorkoutRecord({
245
+ vault: input.vault,
246
+ text: record.templateText,
247
+ durationMinutes: typeof input.durationMinutes === 'number'
248
+ ? input.durationMinutes
249
+ : record.durationMinutes,
250
+ activityType: typeof input.activityType === 'string'
251
+ ? input.activityType
252
+ : record.activityType,
253
+ distanceKm: typeof input.distanceKm === 'number'
254
+ ? input.distanceKm
255
+ : record.distanceKm,
256
+ strengthExercises: record.strengthExercises ?? null,
257
+ occurredAt: input.occurredAt,
258
+ source: input.source,
259
+ title: record.title,
260
+ mediaPaths: input.mediaPaths,
261
+ });
437
262
  }
438
- }
439
- function isMissingPathError(error) {
440
- return Boolean(error &&
441
- typeof error === 'object' &&
442
- 'code' in error &&
443
- error.code === 'ENOENT');
263
+ throw new VaultCliError('contract_invalid', `Workout format document "${record.relativePath}" is missing templateText.`);
444
264
  }
445
265
  //# sourceMappingURL=workout-format.js.map