@marifold/core 0.65.0 → 0.67.0
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/dist/agent/AgentRunner.d.ts +6 -0
- package/dist/agent/AgentRunner.d.ts.map +1 -1
- package/dist/agent/AgentRunner.js +21 -6
- package/dist/agent/AgentRunner.js.map +1 -1
- package/dist/agent/RunWorkspace.d.ts +10 -0
- package/dist/agent/RunWorkspace.d.ts.map +1 -1
- package/dist/agent/RunWorkspace.js +19 -1
- package/dist/agent/RunWorkspace.js.map +1 -1
- package/dist/agent/tools/ReadFileTool.d.ts +7 -0
- package/dist/agent/tools/ReadFileTool.d.ts.map +1 -1
- package/dist/agent/tools/ReadFileTool.js +24 -2
- package/dist/agent/tools/ReadFileTool.js.map +1 -1
- package/dist/agent/tools/SkillAppTools.d.ts +90 -0
- package/dist/agent/tools/SkillAppTools.d.ts.map +1 -0
- package/dist/agent/tools/SkillAppTools.js +402 -0
- package/dist/agent/tools/SkillAppTools.js.map +1 -0
- package/dist/app/AppStore.d.ts +11 -1
- package/dist/app/AppStore.d.ts.map +1 -1
- package/dist/app/AppStore.js +110 -15
- package/dist/app/AppStore.js.map +1 -1
- package/dist/app/SkillAppCompiler.d.ts.map +1 -1
- package/dist/app/SkillAppCompiler.js +370 -44
- package/dist/app/SkillAppCompiler.js.map +1 -1
- package/dist/app/SkillAppDsl.d.ts +77 -4
- package/dist/app/SkillAppDsl.d.ts.map +1 -1
- package/dist/app/SkillAppDsl.js +53 -0
- package/dist/app/SkillAppDsl.js.map +1 -1
- package/dist/app/SkillAppInstanceRegistry.d.ts +23 -2
- package/dist/app/SkillAppInstanceRegistry.d.ts.map +1 -1
- package/dist/app/SkillAppInstanceRegistry.js +439 -11
- package/dist/app/SkillAppInstanceRegistry.js.map +1 -1
- package/dist/app/SkillAppResolver.d.ts +5 -2
- package/dist/app/SkillAppResolver.d.ts.map +1 -1
- package/dist/app/SkillAppResolver.js +66 -15
- package/dist/app/SkillAppResolver.js.map +1 -1
- package/dist/app/SkillAppSchema.d.ts +120 -9
- package/dist/app/SkillAppSchema.d.ts.map +1 -1
- package/dist/app/SkillAppSchema.js +2 -1
- package/dist/app/SkillAppSchema.js.map +1 -1
- package/dist/index.d.ts +6 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +14 -4
- package/dist/index.js.map +1 -1
- package/dist/runtime/MarifoldRuntime.d.ts +14 -8
- package/dist/runtime/MarifoldRuntime.d.ts.map +1 -1
- package/dist/runtime/MarifoldRuntime.js +246 -30
- package/dist/runtime/MarifoldRuntime.js.map +1 -1
- package/dist/skill/BuiltInSkillAppBuilder.d.ts +10 -0
- package/dist/skill/BuiltInSkillAppBuilder.d.ts.map +1 -0
- package/dist/skill/BuiltInSkillAppBuilder.js +34 -0
- package/dist/skill/BuiltInSkillAppBuilder.js.map +1 -0
- package/dist/skill/BuiltInSkills.d.ts.map +1 -1
- package/dist/skill/BuiltInSkills.js +37 -0
- package/dist/skill/BuiltInSkills.js.map +1 -1
- package/dist/skill/SkillStore.d.ts +2 -2
- package/dist/skill/SkillStore.js +2 -2
- package/dist/skill/index.d.ts +1 -0
- package/dist/skill/index.d.ts.map +1 -1
- package/dist/skill/index.js +4 -1
- package/dist/skill/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -42,9 +42,14 @@ const SAFE_APP_NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
|
42
42
|
const SAFE_LOCAL_NAME = /^[a-zA-Z][a-zA-Z0-9_]*$/;
|
|
43
43
|
const SAFE_SKILL_NAME = /^[a-z0-9][a-z0-9_-]*$/;
|
|
44
44
|
const SAFE_PROVIDER_NAME = /^[a-z0-9][a-z0-9_-]*$/;
|
|
45
|
+
const SAFE_PROFILE_NAME = /^[A-Za-z0-9_-]+$/;
|
|
46
|
+
const SAFE_DOWNLOAD_FILENAME = /^[^/\\\u0000-\u001f\u007f]{1,255}$/;
|
|
47
|
+
const SAFE_DOWNLOAD_MEDIA_TYPE = /^[a-z0-9][a-z0-9!#$&^_.+-]*\/[a-z0-9][a-z0-9!#$&^_.+-]*(?:\s*;\s*charset=[a-z0-9._-]+)?$/i;
|
|
45
48
|
const ALLOWED_BUILDERS = new Set([
|
|
46
|
-
'App', '
|
|
47
|
-
'
|
|
49
|
+
'App', 'AttachmentState', 'Attachments', 'Button', 'Column', 'Download', 'FileAccess',
|
|
50
|
+
'FolderAccess', 'Markdown', 'Row', 'Select', 'Spacer', 'State', 'Textarea',
|
|
51
|
+
'TextResult', 'defineSkillApp', 'registerModel', 'registerProfile', 'registerSkill',
|
|
52
|
+
'trigger', 'useProfileSkill', 'useSkill',
|
|
48
53
|
]);
|
|
49
54
|
/** Compile the restricted TypeScript authoring syntax into renderer-neutral data.
|
|
50
55
|
* The source is inspected as an AST and is never evaluated or imported. */
|
|
@@ -63,7 +68,9 @@ function compileSkillApp(source, sourcePath = 'skillapp.ts') {
|
|
|
63
68
|
imports: new Map(),
|
|
64
69
|
values: new Map(),
|
|
65
70
|
states: [],
|
|
71
|
+
attachmentStates: [],
|
|
66
72
|
models: [],
|
|
73
|
+
profiles: [],
|
|
67
74
|
skills: [],
|
|
68
75
|
operations: [],
|
|
69
76
|
triggers: [],
|
|
@@ -89,6 +96,8 @@ function compileSkillApp(source, sourcePath = 'skillapp.ts') {
|
|
|
89
96
|
const tagged = requireTagged(value, 'skillapp', statement.expression, state, sourcePath);
|
|
90
97
|
state.template = {
|
|
91
98
|
app: requireObject(tagged.app, 'defineSkillApp app', statement.expression, state, sourcePath),
|
|
99
|
+
permissions: requireArray(tagged.permissions ?? [], 'defineSkillApp permissions', statement.expression, state, sourcePath)
|
|
100
|
+
.map(permission => requireTagged(permission, 'permission', statement.expression, state, sourcePath)),
|
|
92
101
|
ui: requireTagged(tagged.ui, 'component', statement.expression, state, sourcePath),
|
|
93
102
|
};
|
|
94
103
|
}
|
|
@@ -99,16 +108,20 @@ function compileSkillApp(source, sourcePath = 'skillapp.ts') {
|
|
|
99
108
|
if (!state.template)
|
|
100
109
|
throw MarifoldError_1.MarifoldError.appInvalid('SkillApp must export default defineSkillApp({...}).', sourcePath);
|
|
101
110
|
const app = normalizeAppInfo(state.template.app, state, sourcePath);
|
|
111
|
+
const permissions = normalizePermissions(state.template.permissions, state, sourcePath);
|
|
102
112
|
const layoutRoot = normalizeComponent(state.template.ui, state, sourcePath);
|
|
103
113
|
if (layoutRoot.component !== 'app') {
|
|
104
114
|
throw MarifoldError_1.MarifoldError.appInvalid('defineSkillApp.ui must be App([...]).', sourcePath);
|
|
105
115
|
}
|
|
106
116
|
validateReferences(state, layoutRoot.children ?? [], sourcePath);
|
|
107
117
|
return {
|
|
108
|
-
schema: SkillAppSchema_1.SKILL_APP_SCHEMA,
|
|
118
|
+
schema: state.profiles.length > 0 ? SkillAppSchema_1.SKILL_APP_PROFILE_SCHEMA : SkillAppSchema_1.SKILL_APP_SCHEMA,
|
|
109
119
|
app,
|
|
110
120
|
states: state.states,
|
|
121
|
+
attachmentStates: state.attachmentStates,
|
|
122
|
+
permissions,
|
|
111
123
|
models: state.models,
|
|
124
|
+
profiles: state.profiles,
|
|
112
125
|
skills: state.skills,
|
|
113
126
|
operations: state.operations,
|
|
114
127
|
triggers: state.triggers,
|
|
@@ -156,20 +169,12 @@ function registerDeclaration(name, value, node, state, sourcePath) {
|
|
|
156
169
|
if (value.__kind === 'state') {
|
|
157
170
|
state.states.push({ name, initial: requireString(value.initial, 'State initial value', node, state, sourcePath) });
|
|
158
171
|
}
|
|
172
|
+
else if (value.__kind === 'attachment_state') {
|
|
173
|
+
state.attachmentStates.push({ name });
|
|
174
|
+
}
|
|
159
175
|
else if (value.__kind === 'model') {
|
|
160
176
|
const id = requireString(value.id, 'model id', node, state, sourcePath);
|
|
161
|
-
const
|
|
162
|
-
if (separator <= 0 || separator === id.length - 1) {
|
|
163
|
-
throw invalidAt(state.sourceFile, node.pos, `Model '${id}' must use provider/model format.`, sourcePath);
|
|
164
|
-
}
|
|
165
|
-
const provider = id.slice(0, separator);
|
|
166
|
-
if (!SAFE_PROVIDER_NAME.test(provider)) {
|
|
167
|
-
throw invalidAt(state.sourceFile, node.pos, `Invalid model provider '${provider}'.`, sourcePath);
|
|
168
|
-
}
|
|
169
|
-
const model = id.slice(separator + 1);
|
|
170
|
-
if (model.trim() !== model || model.length === 0) {
|
|
171
|
-
throw invalidAt(state.sourceFile, node.pos, `Invalid model id '${model}'.`, sourcePath);
|
|
172
|
-
}
|
|
177
|
+
const { provider, model } = parseModelId(id, node, state, sourcePath);
|
|
173
178
|
state.models.push({
|
|
174
179
|
name,
|
|
175
180
|
provider,
|
|
@@ -177,6 +182,28 @@ function registerDeclaration(name, value, node, state, sourcePath) {
|
|
|
177
182
|
think: requireBoolean(value.think, 'model think', node, state, sourcePath),
|
|
178
183
|
});
|
|
179
184
|
}
|
|
185
|
+
else if (value.__kind === 'profile') {
|
|
186
|
+
const profile = requireString(value.profileName, 'profile name', node, state, sourcePath);
|
|
187
|
+
if (!SAFE_PROFILE_NAME.test(profile)) {
|
|
188
|
+
throw invalidAt(state.sourceFile, node.pos, `Invalid profile name '${profile}'.`, sourcePath);
|
|
189
|
+
}
|
|
190
|
+
const modelId = value.modelId === undefined
|
|
191
|
+
? undefined
|
|
192
|
+
: requireString(value.modelId, 'profile model override', node, state, sourcePath);
|
|
193
|
+
const modelOverride = modelId === undefined
|
|
194
|
+
? undefined
|
|
195
|
+
: parseModelId(modelId, node, state, sourcePath);
|
|
196
|
+
state.profiles.push({
|
|
197
|
+
name,
|
|
198
|
+
profile,
|
|
199
|
+
...(modelOverride ?? {}),
|
|
200
|
+
...(value.think !== undefined
|
|
201
|
+
? { think: requireBoolean(value.think, 'profile think', node, state, sourcePath) }
|
|
202
|
+
: {}),
|
|
203
|
+
memory: requireBoolean(value.memory, 'profile memory', node, state, sourcePath),
|
|
204
|
+
history: requireBoolean(value.history, 'profile history', node, state, sourcePath),
|
|
205
|
+
});
|
|
206
|
+
}
|
|
180
207
|
else if (value.__kind === 'skill') {
|
|
181
208
|
const skillName = requireString(value.skillName, 'skill name', node, state, sourcePath);
|
|
182
209
|
if (!SAFE_SKILL_NAME.test(skillName)) {
|
|
@@ -189,18 +216,65 @@ function registerDeclaration(name, value, node, state, sourcePath) {
|
|
|
189
216
|
});
|
|
190
217
|
}
|
|
191
218
|
else if (value.__kind === 'operation') {
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
219
|
+
const parameters = Object.fromEntries(Object.entries(requireObject(value.parameters, 'operation parameters', node, state, sourcePath)).map(([parameter, reference]) => [
|
|
220
|
+
parameter,
|
|
221
|
+
requireNamedReference(reference, 'state', node, state, sourcePath),
|
|
222
|
+
]));
|
|
223
|
+
if (value.profile !== undefined) {
|
|
224
|
+
const profile = requireTagged(value.profile, 'profile', node, state, sourcePath);
|
|
225
|
+
const result = requireTagged(value.result, 'text_result', node, state, sourcePath);
|
|
226
|
+
const skillName = value.skillName === undefined
|
|
227
|
+
? undefined
|
|
228
|
+
: requireString(value.skillName, 'profile skill name', node, state, sourcePath);
|
|
229
|
+
const skillState = value.skillState === undefined
|
|
230
|
+
? undefined
|
|
231
|
+
: requireNamedReference(value.skillState, 'state', node, state, sourcePath);
|
|
232
|
+
const skillOptions = value.skillOptions === undefined
|
|
233
|
+
? undefined
|
|
234
|
+
: requireSkillOptionValues(value.skillOptions, node, state, sourcePath);
|
|
235
|
+
state.operations.push({
|
|
236
|
+
name,
|
|
237
|
+
profile: requireName(profile, 'profile', node, state, sourcePath),
|
|
238
|
+
...(skillName ? { skill: skillName } : {}),
|
|
239
|
+
...(skillState ? { skillState } : {}),
|
|
240
|
+
...(skillOptions ? { skillOptions } : {}),
|
|
241
|
+
...(value.stripSkillName !== undefined
|
|
242
|
+
? { stripSkillName: requireBoolean(value.stripSkillName, 'stripSkillName', node, state, sourcePath) }
|
|
243
|
+
: {}),
|
|
244
|
+
...(value.input !== undefined
|
|
245
|
+
? { input: requireNamedReference(value.input, 'state', node, state, sourcePath) }
|
|
246
|
+
: {}),
|
|
247
|
+
...(value.attachments !== undefined
|
|
248
|
+
? { attachments: requireNamedReference(value.attachments, 'attachment_state', node, state, sourcePath) }
|
|
249
|
+
: {}),
|
|
250
|
+
parameters,
|
|
251
|
+
requiredInputs: [],
|
|
252
|
+
output: requireNamedReference(value.output, 'state', node, state, sourcePath),
|
|
253
|
+
result: {
|
|
254
|
+
kind: 'text',
|
|
255
|
+
trim: requireBoolean(result.trim, 'result trim', node, state, sourcePath),
|
|
256
|
+
},
|
|
257
|
+
...(value.interactive !== undefined
|
|
258
|
+
? { interactive: requireBoolean(value.interactive, 'interactive', node, state, sourcePath) }
|
|
259
|
+
: {}),
|
|
260
|
+
execution: {
|
|
261
|
+
memory: requireBoolean(profile.memory, 'profile memory', node, state, sourcePath),
|
|
262
|
+
history: requireBoolean(profile.history, 'profile history', node, state, sourcePath),
|
|
263
|
+
profileContext: true,
|
|
264
|
+
},
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
else {
|
|
268
|
+
state.operations.push({
|
|
269
|
+
name,
|
|
270
|
+
model: requireNamedReference(value.model, 'model', node, state, sourcePath),
|
|
271
|
+
skill: requireNamedReference(value.skill, 'skill', node, state, sourcePath, true),
|
|
272
|
+
parameters,
|
|
273
|
+
requiredInputs: [],
|
|
274
|
+
output: requireNamedReference(value.output, 'state', node, state, sourcePath),
|
|
275
|
+
execution: { memory: false, history: false, profileContext: false },
|
|
276
|
+
});
|
|
277
|
+
}
|
|
204
278
|
}
|
|
205
279
|
}
|
|
206
280
|
function evaluateExpression(rawNode, state, sourcePath, declarationName) {
|
|
@@ -269,6 +343,25 @@ function evaluateCall(node, state, sourcePath, _declarationName) {
|
|
|
269
343
|
case 'State':
|
|
270
344
|
exactArgs(name, args, 1, node, state, sourcePath);
|
|
271
345
|
return { __kind: 'state', initial: requireString(args[0], 'State initial value', node, state, sourcePath) };
|
|
346
|
+
case 'AttachmentState':
|
|
347
|
+
exactArgs(name, args, 0, node, state, sourcePath);
|
|
348
|
+
return { __kind: 'attachment_state' };
|
|
349
|
+
case 'FileAccess':
|
|
350
|
+
case 'FolderAccess': {
|
|
351
|
+
exactArgs(name, args, 2, node, state, sourcePath);
|
|
352
|
+
const options = requireObject(args[1], `${name} options`, node, state, sourcePath);
|
|
353
|
+
rejectUnknown(options, ['access'], name, node, state, sourcePath);
|
|
354
|
+
const access = requireString(options.access, `${name} access`, node, state, sourcePath);
|
|
355
|
+
if (access !== 'read') {
|
|
356
|
+
throw invalidAt(state.sourceFile, node.pos, `${name} access must be "read".`, sourcePath);
|
|
357
|
+
}
|
|
358
|
+
return {
|
|
359
|
+
__kind: 'permission',
|
|
360
|
+
resource: name === 'FileAccess' ? 'file' : 'folder',
|
|
361
|
+
path: requireNonEmptyString(args[0], `${name} path`, node, state, sourcePath),
|
|
362
|
+
access,
|
|
363
|
+
};
|
|
364
|
+
}
|
|
272
365
|
case 'registerModel': {
|
|
273
366
|
argsRange(name, args, 1, 2, node, state, sourcePath);
|
|
274
367
|
const options = args[1] === undefined ? {} : requireObject(args[1], 'registerModel options', node, state, sourcePath);
|
|
@@ -279,6 +372,23 @@ function evaluateCall(node, state, sourcePath, _declarationName) {
|
|
|
279
372
|
think: options.think === undefined ? false : requireBoolean(options.think, 'think', node, state, sourcePath),
|
|
280
373
|
};
|
|
281
374
|
}
|
|
375
|
+
case 'registerProfile': {
|
|
376
|
+
argsRange(name, args, 1, 2, node, state, sourcePath);
|
|
377
|
+
const options = args[1] === undefined ? {} : requireObject(args[1], 'registerProfile options', node, state, sourcePath);
|
|
378
|
+
rejectUnknown(options, ['model', 'think', 'memory', 'history'], name, node, state, sourcePath);
|
|
379
|
+
return {
|
|
380
|
+
__kind: 'profile',
|
|
381
|
+
profileName: requireString(args[0], 'profile name', node, state, sourcePath),
|
|
382
|
+
...(options.model !== undefined
|
|
383
|
+
? { modelId: requireString(options.model, 'model', node, state, sourcePath) }
|
|
384
|
+
: {}),
|
|
385
|
+
...(options.think !== undefined
|
|
386
|
+
? { think: requireBoolean(options.think, 'think', node, state, sourcePath) }
|
|
387
|
+
: {}),
|
|
388
|
+
memory: options.memory === undefined ? false : requireBoolean(options.memory, 'memory', node, state, sourcePath),
|
|
389
|
+
history: options.history === undefined ? false : requireBoolean(options.history, 'history', node, state, sourcePath),
|
|
390
|
+
};
|
|
391
|
+
}
|
|
282
392
|
case 'TextResult': {
|
|
283
393
|
argsRange(name, args, 0, 1, node, state, sourcePath);
|
|
284
394
|
const options = args[0] === undefined ? {} : requireObject(args[0], 'TextResult options', node, state, sourcePath);
|
|
@@ -314,6 +424,52 @@ function evaluateCall(node, state, sourcePath, _declarationName) {
|
|
|
314
424
|
output: requireTagged(options.output, 'state', node, state, sourcePath),
|
|
315
425
|
};
|
|
316
426
|
}
|
|
427
|
+
case 'useProfileSkill': {
|
|
428
|
+
exactArgs(name, args, 3, node, state, sourcePath);
|
|
429
|
+
const profile = requireTagged(args[0], 'profile', node, state, sourcePath);
|
|
430
|
+
const skillName = typeof args[1] === 'string' ? args[1] : undefined;
|
|
431
|
+
const skillState = skillName === undefined
|
|
432
|
+
? requireTagged(args[1], 'state', node, state, sourcePath)
|
|
433
|
+
: undefined;
|
|
434
|
+
if (skillName !== undefined && !SAFE_SKILL_NAME.test(skillName)) {
|
|
435
|
+
throw invalidAt(state.sourceFile, node.pos, `Invalid profile skill name '${skillName}'.`, sourcePath);
|
|
436
|
+
}
|
|
437
|
+
const options = requireObject(args[2], 'useProfileSkill options', node, state, sourcePath);
|
|
438
|
+
rejectUnknown(options, ['skills', 'input', 'attachments', 'stripSkillName', 'parameters', 'output', 'result', 'interactive'], name, node, state, sourcePath);
|
|
439
|
+
if (skillName !== undefined && options.skills !== undefined) {
|
|
440
|
+
throw invalidAt(state.sourceFile, node.pos, 'useProfileSkill.skills is only valid when the Skill is selected by State.', sourcePath);
|
|
441
|
+
}
|
|
442
|
+
if (skillState !== undefined && options.skills === undefined) {
|
|
443
|
+
throw invalidAt(state.sourceFile, node.pos, 'A state-selected profile Skill requires a non-empty useProfileSkill.skills allowlist.', sourcePath);
|
|
444
|
+
}
|
|
445
|
+
const interactive = options.interactive === undefined
|
|
446
|
+
? undefined
|
|
447
|
+
: requireBoolean(options.interactive, 'interactive', node, state, sourcePath);
|
|
448
|
+
if (interactive && skillState !== undefined) {
|
|
449
|
+
throw invalidAt(state.sourceFile, node.pos, 'An interactive profile operation must use one fixed Agent Skill.', sourcePath);
|
|
450
|
+
}
|
|
451
|
+
return {
|
|
452
|
+
__kind: 'operation',
|
|
453
|
+
profile,
|
|
454
|
+
...(skillName !== undefined ? { skillName } : { skillState }),
|
|
455
|
+
...(options.skills !== undefined ? { skillOptions: options.skills } : {}),
|
|
456
|
+
...(options.input !== undefined
|
|
457
|
+
? { input: requireTagged(options.input, 'state', node, state, sourcePath) }
|
|
458
|
+
: {}),
|
|
459
|
+
...(options.attachments !== undefined
|
|
460
|
+
? { attachments: requireTagged(options.attachments, 'attachment_state', node, state, sourcePath) }
|
|
461
|
+
: {}),
|
|
462
|
+
...(options.stripSkillName !== undefined
|
|
463
|
+
? { stripSkillName: requireBoolean(options.stripSkillName, 'stripSkillName', node, state, sourcePath) }
|
|
464
|
+
: {}),
|
|
465
|
+
parameters: options.parameters === undefined
|
|
466
|
+
? {}
|
|
467
|
+
: requireObject(options.parameters, 'useProfileSkill parameters', node, state, sourcePath),
|
|
468
|
+
output: requireTagged(options.output, 'state', node, state, sourcePath),
|
|
469
|
+
result: requireTagged(options.result, 'text_result', node, state, sourcePath),
|
|
470
|
+
...(interactive !== undefined ? { interactive } : {}),
|
|
471
|
+
};
|
|
472
|
+
}
|
|
317
473
|
case 'trigger': {
|
|
318
474
|
exactArgs(name, args, 2, node, state, sourcePath);
|
|
319
475
|
const operation = requireTagged(args[0], 'operation', node, state, sourcePath);
|
|
@@ -342,10 +498,13 @@ function evaluateCall(node, state, sourcePath, _declarationName) {
|
|
|
342
498
|
case 'defineSkillApp': {
|
|
343
499
|
exactArgs(name, args, 1, node, state, sourcePath);
|
|
344
500
|
const template = requireObject(args[0], 'defineSkillApp template', node, state, sourcePath);
|
|
345
|
-
rejectUnknown(template, ['app', 'ui'], name, node, state, sourcePath);
|
|
501
|
+
rejectUnknown(template, ['app', 'permissions', 'ui'], name, node, state, sourcePath);
|
|
346
502
|
return {
|
|
347
503
|
__kind: 'skillapp',
|
|
348
504
|
app: requireObject(template.app, 'app metadata', node, state, sourcePath),
|
|
505
|
+
permissions: template.permissions === undefined
|
|
506
|
+
? []
|
|
507
|
+
: requireArray(template.permissions, 'defineSkillApp permissions', node, state, sourcePath),
|
|
349
508
|
ui: requireTagged(template.ui, 'component', node, state, sourcePath),
|
|
350
509
|
};
|
|
351
510
|
}
|
|
@@ -366,29 +525,73 @@ function evaluateCall(node, state, sourcePath, _declarationName) {
|
|
|
366
525
|
case 'Textarea': {
|
|
367
526
|
argsRange(name, args, 2, 3, node, state, sourcePath);
|
|
368
527
|
const options = args[2] === undefined ? {} : requireObject(args[2], 'Textarea options', node, state, sourcePath);
|
|
369
|
-
rejectUnknown(options, ['showLabel', 'grow', 'editable', 'copyable', 'placeholder'], name, node, state, sourcePath);
|
|
528
|
+
rejectUnknown(options, ['showLabel', 'grow', 'editable', 'copyable', 'rows', 'autoGrow', 'placeholder'], name, node, state, sourcePath);
|
|
370
529
|
return {
|
|
371
530
|
__kind: 'component', component: 'textarea', label: requireNonEmptyString(args[0], 'Textarea label', node, state, sourcePath),
|
|
372
531
|
bind: requireTagged(args[1], 'state', node, state, sourcePath), options,
|
|
373
532
|
};
|
|
374
533
|
}
|
|
534
|
+
case 'Markdown': {
|
|
535
|
+
argsRange(name, args, 2, 3, node, state, sourcePath);
|
|
536
|
+
const options = args[2] === undefined ? {} : requireObject(args[2], 'Markdown options', node, state, sourcePath);
|
|
537
|
+
rejectUnknown(options, ['showLabel', 'grow', 'copyable', 'sourceToggle', 'placeholder'], name, node, state, sourcePath);
|
|
538
|
+
return {
|
|
539
|
+
__kind: 'component', component: 'markdown', label: requireNonEmptyString(args[0], 'Markdown label', node, state, sourcePath),
|
|
540
|
+
bind: requireTagged(args[1], 'state', node, state, sourcePath),
|
|
541
|
+
options: {
|
|
542
|
+
...options,
|
|
543
|
+
copyable: options.copyable === undefined ? true : requireBoolean(options.copyable, 'copyable', node, state, sourcePath),
|
|
544
|
+
sourceToggle: options.sourceToggle === undefined ? true : requireBoolean(options.sourceToggle, 'sourceToggle', node, state, sourcePath),
|
|
545
|
+
},
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
case 'Download': {
|
|
549
|
+
exactArgs(name, args, 3, node, state, sourcePath);
|
|
550
|
+
const options = requireObject(args[2], 'Download options', node, state, sourcePath);
|
|
551
|
+
rejectUnknown(options, ['filename', 'mediaType', 'description', 'showLabel', 'grow'], name, node, state, sourcePath);
|
|
552
|
+
const filename = requireNonEmptyString(options.filename, 'Download filename', node, state, sourcePath);
|
|
553
|
+
if (!SAFE_DOWNLOAD_FILENAME.test(filename) || filename === '.' || filename === '..') {
|
|
554
|
+
throw invalidAt(state.sourceFile, node.pos, 'Download filename must be one safe file name without a path.', sourcePath);
|
|
555
|
+
}
|
|
556
|
+
const mediaType = options.mediaType === undefined
|
|
557
|
+
? 'text/plain;charset=utf-8'
|
|
558
|
+
: requireNonEmptyString(options.mediaType, 'Download mediaType', node, state, sourcePath);
|
|
559
|
+
if (!SAFE_DOWNLOAD_MEDIA_TYPE.test(mediaType)) {
|
|
560
|
+
throw invalidAt(state.sourceFile, node.pos, `Invalid Download mediaType '${mediaType}'.`, sourcePath);
|
|
561
|
+
}
|
|
562
|
+
return {
|
|
563
|
+
__kind: 'component', component: 'download', label: requireNonEmptyString(args[0], 'Download label', node, state, sourcePath),
|
|
564
|
+
bind: requireTagged(args[1], 'state', node, state, sourcePath),
|
|
565
|
+
options: { ...options, filename, mediaType },
|
|
566
|
+
};
|
|
567
|
+
}
|
|
375
568
|
case 'Select': {
|
|
376
569
|
exactArgs(name, args, 3, node, state, sourcePath);
|
|
377
570
|
const options = requireObject(args[2], 'Select options', node, state, sourcePath);
|
|
378
571
|
rejectUnknown(options, ['options', 'showLabel', 'grow'], name, node, state, sourcePath);
|
|
379
572
|
const choices = requireArray(options.options, 'Select options.options', node, state, sourcePath)
|
|
380
|
-
.map(value =>
|
|
573
|
+
.map(value => requireSelectOption(value, node, state, sourcePath));
|
|
381
574
|
if (choices.length === 0)
|
|
382
575
|
throw invalidAt(state.sourceFile, node.pos, 'Select options cannot be empty.', sourcePath);
|
|
383
576
|
return {
|
|
384
577
|
__kind: 'component', component: 'select', label: requireNonEmptyString(args[0], 'Select label', node, state, sourcePath),
|
|
385
|
-
bind: requireTagged(args[1], 'state', node, state, sourcePath),
|
|
578
|
+
bind: requireTagged(args[1], 'state', node, state, sourcePath),
|
|
579
|
+
options: { ...options, options: choices },
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
case 'Attachments': {
|
|
583
|
+
argsRange(name, args, 2, 3, node, state, sourcePath);
|
|
584
|
+
const options = args[2] === undefined ? {} : requireObject(args[2], 'Attachments options', node, state, sourcePath);
|
|
585
|
+
rejectUnknown(options, ['showLabel', 'grow'], name, node, state, sourcePath);
|
|
586
|
+
return {
|
|
587
|
+
__kind: 'component', component: 'attachments', label: requireNonEmptyString(args[0], 'Attachments label', node, state, sourcePath),
|
|
588
|
+
bind: requireTagged(args[1], 'attachment_state', node, state, sourcePath), options,
|
|
386
589
|
};
|
|
387
590
|
}
|
|
388
591
|
case 'Button': {
|
|
389
592
|
exactArgs(name, args, 2, node, state, sourcePath);
|
|
390
593
|
const options = requireObject(args[1], 'Button options', node, state, sourcePath);
|
|
391
|
-
rejectUnknown(options, ['trigger', 'emphasis'], name, node, state, sourcePath);
|
|
594
|
+
rejectUnknown(options, ['trigger', 'emphasis', 'alignToField'], name, node, state, sourcePath);
|
|
392
595
|
return {
|
|
393
596
|
__kind: 'component', component: 'button', label: requireNonEmptyString(args[0], 'Button label', node, state, sourcePath),
|
|
394
597
|
operation: requireTagged(options.trigger, 'operation', node, state, sourcePath), options,
|
|
@@ -410,20 +613,32 @@ function normalizeComponent(value, state, sourcePath, depth = 0) {
|
|
|
410
613
|
if (value.label !== undefined)
|
|
411
614
|
item.label = requireString(value.label, `${component} label`, state.sourceFile, state, sourcePath);
|
|
412
615
|
if (value.bind !== undefined)
|
|
413
|
-
item.bind = requireNamedReference(value.bind, 'state', state.sourceFile, state, sourcePath);
|
|
616
|
+
item.bind = requireNamedReference(value.bind, component === 'attachments' ? 'attachment_state' : 'state', state.sourceFile, state, sourcePath);
|
|
414
617
|
if (value.operation !== undefined)
|
|
415
618
|
item.trigger = requireName(requireTagged(value.operation, 'operation', state.sourceFile, state, sourcePath), 'operation', state.sourceFile, state, sourcePath);
|
|
416
619
|
copyBoolean(options, 'showLabel', item, state, sourcePath);
|
|
417
620
|
copyBoolean(options, 'grow', item, state, sourcePath);
|
|
418
621
|
copyBoolean(options, 'editable', item, state, sourcePath);
|
|
419
622
|
copyBoolean(options, 'copyable', item, state, sourcePath);
|
|
623
|
+
copyBoolean(options, 'autoGrow', item, state, sourcePath);
|
|
624
|
+
copyBoolean(options, 'sourceToggle', item, state, sourcePath);
|
|
625
|
+
copyBoolean(options, 'alignToField', item, state, sourcePath);
|
|
626
|
+
if (options.rows !== undefined) {
|
|
627
|
+
item.rows = requireNonNegativeInteger(options.rows, 'rows', state.sourceFile, state, sourcePath);
|
|
628
|
+
if (item.rows < 1 || item.rows > 40) {
|
|
629
|
+
throw MarifoldError_1.MarifoldError.appInvalid('Textarea rows must be between 1 and 40.', sourcePath);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
420
632
|
copyString(options, 'placeholder', item, state, sourcePath);
|
|
633
|
+
copyString(options, 'filename', item, state, sourcePath);
|
|
634
|
+
copyString(options, 'mediaType', item, state, sourcePath);
|
|
635
|
+
copyString(options, 'description', item, state, sourcePath);
|
|
421
636
|
copyString(options, 'gap', item, state, sourcePath);
|
|
422
637
|
copyString(options, 'responsive', item, state, sourcePath);
|
|
423
638
|
copyString(options, 'emphasis', item, state, sourcePath);
|
|
424
639
|
if (options.options !== undefined) {
|
|
425
640
|
item.options = requireArray(options.options, 'Select options', state.sourceFile, state, sourcePath)
|
|
426
|
-
.map(choice =>
|
|
641
|
+
.map(choice => requireSelectOption(choice, state.sourceFile, state, sourcePath));
|
|
427
642
|
}
|
|
428
643
|
if (item.component === 'textarea' && item.editable === undefined)
|
|
429
644
|
item.editable = true;
|
|
@@ -438,30 +653,65 @@ function normalizeComponent(value, state, sourcePath, depth = 0) {
|
|
|
438
653
|
if (item.emphasis !== undefined && !['primary', 'secondary'].includes(item.emphasis)) {
|
|
439
654
|
throw MarifoldError_1.MarifoldError.appInvalid(`Invalid button emphasis '${item.emphasis}'.`, sourcePath);
|
|
440
655
|
}
|
|
441
|
-
if (item.options && new Set(item.options).size !== item.options.length) {
|
|
656
|
+
if (item.options && new Set(item.options.map(selectOptionValue)).size !== item.options.length) {
|
|
442
657
|
throw MarifoldError_1.MarifoldError.appInvalid('Select options must be unique.', sourcePath);
|
|
443
658
|
}
|
|
444
659
|
return item;
|
|
445
660
|
}
|
|
446
661
|
function validateReferences(state, layout, sourcePath) {
|
|
447
662
|
const stateNames = new Set(state.states.map(item => item.name));
|
|
663
|
+
const attachmentStateNames = new Set(state.attachmentStates.map(item => item.name));
|
|
448
664
|
const modelNames = new Set(state.models.map(item => item.name));
|
|
665
|
+
const profileNames = new Set(state.profiles.map(item => item.name));
|
|
449
666
|
const skillNames = new Set(state.skills.map(item => item.name));
|
|
450
667
|
const operationNames = new Set(state.operations.map(item => item.name));
|
|
451
668
|
assertUnique(state.models.map(item => item.name), 'model declarations', sourcePath);
|
|
669
|
+
assertUnique(state.profiles.map(item => item.name), 'profile declarations', sourcePath);
|
|
452
670
|
assertUnique(state.skills.map(item => item.name), 'registered Skills', sourcePath);
|
|
453
671
|
assertUnique(state.operations.map(item => item.name), 'operations', sourcePath);
|
|
672
|
+
assertUnique(state.attachmentStates.map(item => item.name), 'attachment state declarations', sourcePath);
|
|
454
673
|
if (state.states.length === 0)
|
|
455
674
|
throw MarifoldError_1.MarifoldError.appInvalid('SkillApp must declare at least one State.', sourcePath);
|
|
456
675
|
if (state.operations.length === 0)
|
|
457
|
-
throw MarifoldError_1.MarifoldError.appInvalid('SkillApp must declare at least one useSkill operation.', sourcePath);
|
|
676
|
+
throw MarifoldError_1.MarifoldError.appInvalid('SkillApp must declare at least one useSkill or useProfileSkill operation.', sourcePath);
|
|
458
677
|
for (const operation of state.operations) {
|
|
459
|
-
if (
|
|
460
|
-
throw MarifoldError_1.MarifoldError.appInvalid(`
|
|
461
|
-
|
|
462
|
-
|
|
678
|
+
if (operation.interactive && (!operation.profile || !operation.skill || operation.skillState)) {
|
|
679
|
+
throw MarifoldError_1.MarifoldError.appInvalid(`Interactive operation '${operation.name}' must reference one fixed profile Agent Skill.`, sourcePath);
|
|
680
|
+
}
|
|
681
|
+
if (operation.profile) {
|
|
682
|
+
if (!profileNames.has(operation.profile))
|
|
683
|
+
throw MarifoldError_1.MarifoldError.appInvalid(`Operation '${operation.name}' references missing profile '${operation.profile}'.`, sourcePath);
|
|
684
|
+
if (operation.model)
|
|
685
|
+
throw MarifoldError_1.MarifoldError.appInvalid(`Operation '${operation.name}' cannot reference both a profile and model.`, sourcePath);
|
|
686
|
+
const fixedSkill = operation.skill !== undefined;
|
|
687
|
+
const selectedSkill = operation.skillState !== undefined;
|
|
688
|
+
if (fixedSkill === selectedSkill) {
|
|
689
|
+
throw MarifoldError_1.MarifoldError.appInvalid(`Operation '${operation.name}' must reference exactly one fixed or state-selected profile Skill.`, sourcePath);
|
|
690
|
+
}
|
|
691
|
+
if (selectedSkill) {
|
|
692
|
+
if (!stateNames.has(operation.skillState))
|
|
693
|
+
throw MarifoldError_1.MarifoldError.appInvalid(`Operation '${operation.name}' references missing Skill state '${operation.skillState}'.`, sourcePath);
|
|
694
|
+
if (!operation.skillOptions?.length)
|
|
695
|
+
throw MarifoldError_1.MarifoldError.appInvalid(`Operation '${operation.name}' requires a non-empty Skill allowlist.`, sourcePath);
|
|
696
|
+
const initial = state.states.find(candidate => candidate.name === operation.skillState)?.initial;
|
|
697
|
+
if (initial !== undefined && !operation.skillOptions.includes(initial)) {
|
|
698
|
+
throw MarifoldError_1.MarifoldError.appInvalid(`Operation '${operation.name}' initial Skill '${initial}' is not allowlisted.`, sourcePath);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
else {
|
|
703
|
+
if (!operation.model || !modelNames.has(operation.model))
|
|
704
|
+
throw MarifoldError_1.MarifoldError.appInvalid(`Operation '${operation.name}' references missing model '${operation.model ?? ''}'.`, sourcePath);
|
|
705
|
+
if (!operation.skill || !skillNames.has(operation.skill))
|
|
706
|
+
throw MarifoldError_1.MarifoldError.appInvalid(`Operation '${operation.name}' references missing skill '${operation.skill ?? ''}'.`, sourcePath);
|
|
707
|
+
}
|
|
463
708
|
if (!stateNames.has(operation.output))
|
|
464
709
|
throw MarifoldError_1.MarifoldError.appInvalid(`Operation '${operation.name}' references missing output state '${operation.output}'.`, sourcePath);
|
|
710
|
+
if (operation.input && !stateNames.has(operation.input))
|
|
711
|
+
throw MarifoldError_1.MarifoldError.appInvalid(`Operation '${operation.name}' references missing input state '${operation.input}'.`, sourcePath);
|
|
712
|
+
if (operation.attachments && !attachmentStateNames.has(operation.attachments)) {
|
|
713
|
+
throw MarifoldError_1.MarifoldError.appInvalid(`Operation '${operation.name}' references missing attachment state '${operation.attachments}'.`, sourcePath);
|
|
714
|
+
}
|
|
465
715
|
for (const name of Object.values(operation.parameters)) {
|
|
466
716
|
if (!stateNames.has(name))
|
|
467
717
|
throw MarifoldError_1.MarifoldError.appInvalid(`Operation '${operation.name}' references missing state '${name}'.`, sourcePath);
|
|
@@ -470,16 +720,31 @@ function validateReferences(state, layout, sourcePath) {
|
|
|
470
720
|
const layoutItems = flatten(layout);
|
|
471
721
|
if (layoutItems.length > 100)
|
|
472
722
|
throw MarifoldError_1.MarifoldError.appInvalid('SkillApp layout cannot exceed 100 components.', sourcePath);
|
|
723
|
+
for (const operation of state.operations.filter(candidate => candidate.skillState)) {
|
|
724
|
+
const selectors = layoutItems.filter(item => item.component === 'select' && item.bind === operation.skillState);
|
|
725
|
+
if (selectors.length === 0) {
|
|
726
|
+
throw MarifoldError_1.MarifoldError.appInvalid(`Operation '${operation.name}' Skill state '${operation.skillState}' must bind to Select.`, sourcePath);
|
|
727
|
+
}
|
|
728
|
+
const selectable = new Set(selectors.flatMap(item => (item.options ?? []).map(selectOptionValue)));
|
|
729
|
+
const allowlisted = new Set(operation.skillOptions ?? []);
|
|
730
|
+
if (selectable.size !== allowlisted.size || [...selectable].some(skill => !allowlisted.has(skill))) {
|
|
731
|
+
throw MarifoldError_1.MarifoldError.appInvalid(`Operation '${operation.name}' Select options must exactly match its Skill allowlist.`, sourcePath);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
473
734
|
for (const item of layoutItems) {
|
|
474
735
|
if (item.component === 'app')
|
|
475
736
|
throw MarifoldError_1.MarifoldError.appInvalid('App(...) can only be the root UI component.', sourcePath);
|
|
476
|
-
if (item.bind && !
|
|
737
|
+
if (item.bind && item.component === 'attachments' && !attachmentStateNames.has(item.bind)) {
|
|
738
|
+
throw MarifoldError_1.MarifoldError.appInvalid(`Layout references missing attachment state '${item.bind}'.`, sourcePath);
|
|
739
|
+
}
|
|
740
|
+
if (item.bind && item.component !== 'attachments' && !stateNames.has(item.bind)) {
|
|
477
741
|
throw MarifoldError_1.MarifoldError.appInvalid(`Layout references missing state '${item.bind}'.`, sourcePath);
|
|
742
|
+
}
|
|
478
743
|
if (item.trigger && !operationNames.has(item.trigger))
|
|
479
744
|
throw MarifoldError_1.MarifoldError.appInvalid(`Button references missing operation '${item.trigger}'.`, sourcePath);
|
|
480
745
|
if (item.component === 'select' && item.bind) {
|
|
481
746
|
const initial = state.states.find(candidate => candidate.name === item.bind)?.initial;
|
|
482
|
-
if (initial !== undefined && !(item.options ?? []).includes(initial)) {
|
|
747
|
+
if (initial !== undefined && !(item.options ?? []).map(selectOptionValue).includes(initial)) {
|
|
483
748
|
throw MarifoldError_1.MarifoldError.appInvalid(`Select state '${item.bind}' initial value must be one of its options.`, sourcePath);
|
|
484
749
|
}
|
|
485
750
|
}
|
|
@@ -487,6 +752,9 @@ function validateReferences(state, layout, sourcePath) {
|
|
|
487
752
|
for (const triggerDefinition of state.triggers) {
|
|
488
753
|
if (!operationNames.has(triggerDefinition.operation))
|
|
489
754
|
throw MarifoldError_1.MarifoldError.appInvalid(`Trigger references missing operation '${triggerDefinition.operation}'.`, sourcePath);
|
|
755
|
+
if (state.operations.find(operation => operation.name === triggerDefinition.operation)?.interactive) {
|
|
756
|
+
throw MarifoldError_1.MarifoldError.appInvalid(`Interactive operation '${triggerDefinition.operation}' cannot use an automatic trigger.`, sourcePath);
|
|
757
|
+
}
|
|
490
758
|
for (const name of triggerDefinition.onChange) {
|
|
491
759
|
if (!stateNames.has(name))
|
|
492
760
|
throw MarifoldError_1.MarifoldError.appInvalid(`Trigger references missing state '${name}'.`, sourcePath);
|
|
@@ -494,9 +762,11 @@ function validateReferences(state, layout, sourcePath) {
|
|
|
494
762
|
}
|
|
495
763
|
const outputStates = new Set(state.operations.map(operation => operation.output));
|
|
496
764
|
for (const operation of state.operations) {
|
|
497
|
-
for (const input of Object.values(operation.parameters)) {
|
|
765
|
+
for (const input of [operation.input, operation.skillState, ...Object.values(operation.parameters)]) {
|
|
766
|
+
if (!input)
|
|
767
|
+
continue;
|
|
498
768
|
if (outputStates.has(input))
|
|
499
|
-
throw MarifoldError_1.MarifoldError.appInvalid(`Operation '${operation.name}' cannot use output state '${input}' as
|
|
769
|
+
throw MarifoldError_1.MarifoldError.appInvalid(`Operation '${operation.name}' cannot use output state '${input}' as an input.`, sourcePath);
|
|
500
770
|
}
|
|
501
771
|
}
|
|
502
772
|
for (const item of layoutItems) {
|
|
@@ -516,6 +786,32 @@ function validateReferences(state, layout, sourcePath) {
|
|
|
516
786
|
}
|
|
517
787
|
}
|
|
518
788
|
}
|
|
789
|
+
function normalizePermissions(values, state, sourcePath) {
|
|
790
|
+
const permissions = values.map(value => ({
|
|
791
|
+
kind: requireString(value.resource, 'permission resource', state.sourceFile, state, sourcePath),
|
|
792
|
+
path: requireNonEmptyString(value.path, 'permission path', state.sourceFile, state, sourcePath),
|
|
793
|
+
access: requireString(value.access, 'permission access', state.sourceFile, state, sourcePath),
|
|
794
|
+
}));
|
|
795
|
+
const duplicate = permissions.find((permission, index) => permissions.findIndex(candidate => (candidate.kind === permission.kind && candidate.path === permission.path)) !== index);
|
|
796
|
+
if (duplicate)
|
|
797
|
+
throw MarifoldError_1.MarifoldError.appInvalid(`Duplicate ${duplicate.kind} permission '${duplicate.path}'.`, sourcePath);
|
|
798
|
+
return permissions;
|
|
799
|
+
}
|
|
800
|
+
function parseModelId(id, node, state, sourcePath) {
|
|
801
|
+
const separator = id.indexOf('/');
|
|
802
|
+
if (separator <= 0 || separator === id.length - 1) {
|
|
803
|
+
throw invalidAt(state.sourceFile, node.pos, `Model '${id}' must use provider/model format.`, sourcePath);
|
|
804
|
+
}
|
|
805
|
+
const provider = id.slice(0, separator);
|
|
806
|
+
if (!SAFE_PROVIDER_NAME.test(provider)) {
|
|
807
|
+
throw invalidAt(state.sourceFile, node.pos, `Invalid model provider '${provider}'.`, sourcePath);
|
|
808
|
+
}
|
|
809
|
+
const model = id.slice(separator + 1);
|
|
810
|
+
if (model.trim() !== model || model.length === 0) {
|
|
811
|
+
throw invalidAt(state.sourceFile, node.pos, `Invalid model id '${model}'.`, sourcePath);
|
|
812
|
+
}
|
|
813
|
+
return { provider, model };
|
|
814
|
+
}
|
|
519
815
|
function assertUnique(values, label, sourcePath) {
|
|
520
816
|
const seen = new Set();
|
|
521
817
|
for (const value of values) {
|
|
@@ -572,6 +868,36 @@ function requireArray(value, label, node, state, sourcePath) {
|
|
|
572
868
|
return value;
|
|
573
869
|
throw invalidAt(state.sourceFile, node.pos, `Expected ${label} to be an array.`, sourcePath);
|
|
574
870
|
}
|
|
871
|
+
function requireSelectOption(value, node, state, sourcePath) {
|
|
872
|
+
if (typeof value === 'string') {
|
|
873
|
+
return requireNonEmptyString(value, 'Select option', node, state, sourcePath);
|
|
874
|
+
}
|
|
875
|
+
const option = requireObject(value, 'Select option', node, state, sourcePath);
|
|
876
|
+
rejectUnknown(option, ['label', 'value'], 'Select option', node, state, sourcePath);
|
|
877
|
+
return {
|
|
878
|
+
label: requireNonEmptyString(option.label, 'Select option label', node, state, sourcePath),
|
|
879
|
+
value: requireNonEmptyString(option.value, 'Select option value', node, state, sourcePath),
|
|
880
|
+
};
|
|
881
|
+
}
|
|
882
|
+
function requireSkillOptionValues(value, node, state, sourcePath) {
|
|
883
|
+
const skills = requireArray(value, 'useProfileSkill skills', node, state, sourcePath)
|
|
884
|
+
.map(option => selectOptionValue(requireSelectOption(option, node, state, sourcePath)));
|
|
885
|
+
if (skills.length === 0) {
|
|
886
|
+
throw invalidAt(state.sourceFile, node.pos, 'useProfileSkill.skills cannot be empty.', sourcePath);
|
|
887
|
+
}
|
|
888
|
+
for (const skill of skills) {
|
|
889
|
+
if (!SAFE_SKILL_NAME.test(skill)) {
|
|
890
|
+
throw invalidAt(state.sourceFile, node.pos, `Invalid profile skill name '${skill}'.`, sourcePath);
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
if (new Set(skills).size !== skills.length) {
|
|
894
|
+
throw invalidAt(state.sourceFile, node.pos, 'useProfileSkill.skills must be unique.', sourcePath);
|
|
895
|
+
}
|
|
896
|
+
return skills;
|
|
897
|
+
}
|
|
898
|
+
function selectOptionValue(option) {
|
|
899
|
+
return typeof option === 'string' ? option : option.value;
|
|
900
|
+
}
|
|
575
901
|
function requireString(value, label, node, state, sourcePath) {
|
|
576
902
|
if (typeof value === 'string')
|
|
577
903
|
return value;
|