@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
|
@@ -1,445 +1,265 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
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 {
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
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
|
-
|
|
93
|
-
|
|
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
|
-
|
|
112
|
-
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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
|
|
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:
|
|
272
|
-
data:
|
|
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
|
-
|
|
286
|
-
|
|
69
|
+
text: record.templateText,
|
|
70
|
+
templateText: record.templateText,
|
|
71
|
+
},
|
|
287
72
|
links: [],
|
|
288
73
|
};
|
|
289
74
|
}
|
|
290
|
-
function
|
|
75
|
+
async function loadWorkoutFormats(vault) {
|
|
291
76
|
try {
|
|
292
|
-
return
|
|
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
|
-
|
|
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
|
|
307
|
-
|
|
308
|
-
|
|
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
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
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
|
-
|
|
327
|
-
|
|
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
|
|
330
|
-
|
|
331
|
-
|
|
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 (
|
|
346
|
-
|
|
108
|
+
if (titleMatches[0]) {
|
|
109
|
+
return titleMatches[0];
|
|
347
110
|
}
|
|
348
|
-
|
|
111
|
+
throw new VaultCliError('not_found', `No workout format found for "${normalizedLookup}".`);
|
|
349
112
|
}
|
|
350
|
-
function
|
|
351
|
-
|
|
352
|
-
|
|
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
|
-
|
|
355
|
-
|
|
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
|
|
142
|
+
return parsed.data;
|
|
358
143
|
}
|
|
359
|
-
function
|
|
360
|
-
|
|
361
|
-
|
|
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
|
-
|
|
364
|
-
|
|
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
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
:
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
:
|
|
380
|
-
|
|
381
|
-
|
|
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
|
-
|
|
408
|
-
|
|
191
|
+
catch (error) {
|
|
192
|
+
throw toWorkoutFormatCliError(error);
|
|
409
193
|
}
|
|
410
|
-
return
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
.
|
|
415
|
-
.
|
|
416
|
-
|
|
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
|
|
423
|
-
|
|
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
|
|
426
|
-
|
|
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
|
|
429
|
-
|
|
430
|
-
|
|
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
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
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
|