@marifold/core 0.65.0 → 0.66.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 +3 -0
- 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/app/AppStore.d.ts +11 -1
- package/dist/app/AppStore.d.ts.map +1 -1
- package/dist/app/AppStore.js +105 -16
- package/dist/app/AppStore.js.map +1 -1
- package/dist/app/SkillAppCompiler.d.ts.map +1 -1
- package/dist/app/SkillAppCompiler.js +314 -44
- package/dist/app/SkillAppCompiler.js.map +1 -1
- package/dist/app/SkillAppDsl.d.ts +60 -4
- package/dist/app/SkillAppDsl.d.ts.map +1 -1
- package/dist/app/SkillAppDsl.js +44 -0
- package/dist/app/SkillAppDsl.js.map +1 -1
- package/dist/app/SkillAppInstanceRegistry.d.ts +3 -2
- package/dist/app/SkillAppInstanceRegistry.d.ts.map +1 -1
- package/dist/app/SkillAppInstanceRegistry.js +191 -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 +84 -8
- 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 +5 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -4
- package/dist/index.js.map +1 -1
- package/dist/runtime/MarifoldRuntime.d.ts +12 -6
- package/dist/runtime/MarifoldRuntime.d.ts.map +1 -1
- package/dist/runtime/MarifoldRuntime.js +169 -28
- package/dist/runtime/MarifoldRuntime.js.map +1 -1
- package/dist/skill/SkillStore.d.ts +2 -2
- package/dist/skill/SkillStore.js +2 -2
- package/package.json +1 -1
|
@@ -42,9 +42,12 @@ 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_-]+$/;
|
|
45
46
|
const ALLOWED_BUILDERS = new Set([
|
|
46
|
-
'App', '
|
|
47
|
-
'
|
|
47
|
+
'App', 'AttachmentState', 'Attachments', 'Button', 'Column', 'FileAccess',
|
|
48
|
+
'FolderAccess', 'Row', 'Select', 'Spacer', 'State', 'Textarea',
|
|
49
|
+
'TextResult', 'defineSkillApp', 'registerModel', 'registerProfile', 'registerSkill',
|
|
50
|
+
'trigger', 'useProfileSkill', 'useSkill',
|
|
48
51
|
]);
|
|
49
52
|
/** Compile the restricted TypeScript authoring syntax into renderer-neutral data.
|
|
50
53
|
* The source is inspected as an AST and is never evaluated or imported. */
|
|
@@ -63,7 +66,9 @@ function compileSkillApp(source, sourcePath = 'skillapp.ts') {
|
|
|
63
66
|
imports: new Map(),
|
|
64
67
|
values: new Map(),
|
|
65
68
|
states: [],
|
|
69
|
+
attachmentStates: [],
|
|
66
70
|
models: [],
|
|
71
|
+
profiles: [],
|
|
67
72
|
skills: [],
|
|
68
73
|
operations: [],
|
|
69
74
|
triggers: [],
|
|
@@ -89,6 +94,8 @@ function compileSkillApp(source, sourcePath = 'skillapp.ts') {
|
|
|
89
94
|
const tagged = requireTagged(value, 'skillapp', statement.expression, state, sourcePath);
|
|
90
95
|
state.template = {
|
|
91
96
|
app: requireObject(tagged.app, 'defineSkillApp app', statement.expression, state, sourcePath),
|
|
97
|
+
permissions: requireArray(tagged.permissions ?? [], 'defineSkillApp permissions', statement.expression, state, sourcePath)
|
|
98
|
+
.map(permission => requireTagged(permission, 'permission', statement.expression, state, sourcePath)),
|
|
92
99
|
ui: requireTagged(tagged.ui, 'component', statement.expression, state, sourcePath),
|
|
93
100
|
};
|
|
94
101
|
}
|
|
@@ -99,16 +106,20 @@ function compileSkillApp(source, sourcePath = 'skillapp.ts') {
|
|
|
99
106
|
if (!state.template)
|
|
100
107
|
throw MarifoldError_1.MarifoldError.appInvalid('SkillApp must export default defineSkillApp({...}).', sourcePath);
|
|
101
108
|
const app = normalizeAppInfo(state.template.app, state, sourcePath);
|
|
109
|
+
const permissions = normalizePermissions(state.template.permissions, state, sourcePath);
|
|
102
110
|
const layoutRoot = normalizeComponent(state.template.ui, state, sourcePath);
|
|
103
111
|
if (layoutRoot.component !== 'app') {
|
|
104
112
|
throw MarifoldError_1.MarifoldError.appInvalid('defineSkillApp.ui must be App([...]).', sourcePath);
|
|
105
113
|
}
|
|
106
114
|
validateReferences(state, layoutRoot.children ?? [], sourcePath);
|
|
107
115
|
return {
|
|
108
|
-
schema: SkillAppSchema_1.SKILL_APP_SCHEMA,
|
|
116
|
+
schema: state.profiles.length > 0 ? SkillAppSchema_1.SKILL_APP_PROFILE_SCHEMA : SkillAppSchema_1.SKILL_APP_SCHEMA,
|
|
109
117
|
app,
|
|
110
118
|
states: state.states,
|
|
119
|
+
attachmentStates: state.attachmentStates,
|
|
120
|
+
permissions,
|
|
111
121
|
models: state.models,
|
|
122
|
+
profiles: state.profiles,
|
|
112
123
|
skills: state.skills,
|
|
113
124
|
operations: state.operations,
|
|
114
125
|
triggers: state.triggers,
|
|
@@ -156,20 +167,12 @@ function registerDeclaration(name, value, node, state, sourcePath) {
|
|
|
156
167
|
if (value.__kind === 'state') {
|
|
157
168
|
state.states.push({ name, initial: requireString(value.initial, 'State initial value', node, state, sourcePath) });
|
|
158
169
|
}
|
|
170
|
+
else if (value.__kind === 'attachment_state') {
|
|
171
|
+
state.attachmentStates.push({ name });
|
|
172
|
+
}
|
|
159
173
|
else if (value.__kind === 'model') {
|
|
160
174
|
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
|
-
}
|
|
175
|
+
const { provider, model } = parseModelId(id, node, state, sourcePath);
|
|
173
176
|
state.models.push({
|
|
174
177
|
name,
|
|
175
178
|
provider,
|
|
@@ -177,6 +180,28 @@ function registerDeclaration(name, value, node, state, sourcePath) {
|
|
|
177
180
|
think: requireBoolean(value.think, 'model think', node, state, sourcePath),
|
|
178
181
|
});
|
|
179
182
|
}
|
|
183
|
+
else if (value.__kind === 'profile') {
|
|
184
|
+
const profile = requireString(value.profileName, 'profile name', node, state, sourcePath);
|
|
185
|
+
if (!SAFE_PROFILE_NAME.test(profile)) {
|
|
186
|
+
throw invalidAt(state.sourceFile, node.pos, `Invalid profile name '${profile}'.`, sourcePath);
|
|
187
|
+
}
|
|
188
|
+
const modelId = value.modelId === undefined
|
|
189
|
+
? undefined
|
|
190
|
+
: requireString(value.modelId, 'profile model override', node, state, sourcePath);
|
|
191
|
+
const modelOverride = modelId === undefined
|
|
192
|
+
? undefined
|
|
193
|
+
: parseModelId(modelId, node, state, sourcePath);
|
|
194
|
+
state.profiles.push({
|
|
195
|
+
name,
|
|
196
|
+
profile,
|
|
197
|
+
...(modelOverride ?? {}),
|
|
198
|
+
...(value.think !== undefined
|
|
199
|
+
? { think: requireBoolean(value.think, 'profile think', node, state, sourcePath) }
|
|
200
|
+
: {}),
|
|
201
|
+
memory: requireBoolean(value.memory, 'profile memory', node, state, sourcePath),
|
|
202
|
+
history: requireBoolean(value.history, 'profile history', node, state, sourcePath),
|
|
203
|
+
});
|
|
204
|
+
}
|
|
180
205
|
else if (value.__kind === 'skill') {
|
|
181
206
|
const skillName = requireString(value.skillName, 'skill name', node, state, sourcePath);
|
|
182
207
|
if (!SAFE_SKILL_NAME.test(skillName)) {
|
|
@@ -189,18 +214,62 @@ function registerDeclaration(name, value, node, state, sourcePath) {
|
|
|
189
214
|
});
|
|
190
215
|
}
|
|
191
216
|
else if (value.__kind === 'operation') {
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
217
|
+
const parameters = Object.fromEntries(Object.entries(requireObject(value.parameters, 'operation parameters', node, state, sourcePath)).map(([parameter, reference]) => [
|
|
218
|
+
parameter,
|
|
219
|
+
requireNamedReference(reference, 'state', node, state, sourcePath),
|
|
220
|
+
]));
|
|
221
|
+
if (value.profile !== undefined) {
|
|
222
|
+
const profile = requireTagged(value.profile, 'profile', node, state, sourcePath);
|
|
223
|
+
const result = requireTagged(value.result, 'text_result', node, state, sourcePath);
|
|
224
|
+
const skillName = value.skillName === undefined
|
|
225
|
+
? undefined
|
|
226
|
+
: requireString(value.skillName, 'profile skill name', node, state, sourcePath);
|
|
227
|
+
const skillState = value.skillState === undefined
|
|
228
|
+
? undefined
|
|
229
|
+
: requireNamedReference(value.skillState, 'state', node, state, sourcePath);
|
|
230
|
+
const skillOptions = value.skillOptions === undefined
|
|
231
|
+
? undefined
|
|
232
|
+
: requireSkillOptionValues(value.skillOptions, node, state, sourcePath);
|
|
233
|
+
state.operations.push({
|
|
234
|
+
name,
|
|
235
|
+
profile: requireName(profile, 'profile', node, state, sourcePath),
|
|
236
|
+
...(skillName ? { skill: skillName } : {}),
|
|
237
|
+
...(skillState ? { skillState } : {}),
|
|
238
|
+
...(skillOptions ? { skillOptions } : {}),
|
|
239
|
+
...(value.stripSkillName !== undefined
|
|
240
|
+
? { stripSkillName: requireBoolean(value.stripSkillName, 'stripSkillName', node, state, sourcePath) }
|
|
241
|
+
: {}),
|
|
242
|
+
...(value.input !== undefined
|
|
243
|
+
? { input: requireNamedReference(value.input, 'state', node, state, sourcePath) }
|
|
244
|
+
: {}),
|
|
245
|
+
...(value.attachments !== undefined
|
|
246
|
+
? { attachments: requireNamedReference(value.attachments, 'attachment_state', node, state, sourcePath) }
|
|
247
|
+
: {}),
|
|
248
|
+
parameters,
|
|
249
|
+
requiredInputs: [],
|
|
250
|
+
output: requireNamedReference(value.output, 'state', node, state, sourcePath),
|
|
251
|
+
result: {
|
|
252
|
+
kind: 'text',
|
|
253
|
+
trim: requireBoolean(result.trim, 'result trim', node, state, sourcePath),
|
|
254
|
+
},
|
|
255
|
+
execution: {
|
|
256
|
+
memory: requireBoolean(profile.memory, 'profile memory', node, state, sourcePath),
|
|
257
|
+
history: requireBoolean(profile.history, 'profile history', node, state, sourcePath),
|
|
258
|
+
profileContext: true,
|
|
259
|
+
},
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
else {
|
|
263
|
+
state.operations.push({
|
|
264
|
+
name,
|
|
265
|
+
model: requireNamedReference(value.model, 'model', node, state, sourcePath),
|
|
266
|
+
skill: requireNamedReference(value.skill, 'skill', node, state, sourcePath, true),
|
|
267
|
+
parameters,
|
|
268
|
+
requiredInputs: [],
|
|
269
|
+
output: requireNamedReference(value.output, 'state', node, state, sourcePath),
|
|
270
|
+
execution: { memory: false, history: false, profileContext: false },
|
|
271
|
+
});
|
|
272
|
+
}
|
|
204
273
|
}
|
|
205
274
|
}
|
|
206
275
|
function evaluateExpression(rawNode, state, sourcePath, declarationName) {
|
|
@@ -269,6 +338,25 @@ function evaluateCall(node, state, sourcePath, _declarationName) {
|
|
|
269
338
|
case 'State':
|
|
270
339
|
exactArgs(name, args, 1, node, state, sourcePath);
|
|
271
340
|
return { __kind: 'state', initial: requireString(args[0], 'State initial value', node, state, sourcePath) };
|
|
341
|
+
case 'AttachmentState':
|
|
342
|
+
exactArgs(name, args, 0, node, state, sourcePath);
|
|
343
|
+
return { __kind: 'attachment_state' };
|
|
344
|
+
case 'FileAccess':
|
|
345
|
+
case 'FolderAccess': {
|
|
346
|
+
exactArgs(name, args, 2, node, state, sourcePath);
|
|
347
|
+
const options = requireObject(args[1], `${name} options`, node, state, sourcePath);
|
|
348
|
+
rejectUnknown(options, ['access'], name, node, state, sourcePath);
|
|
349
|
+
const access = requireString(options.access, `${name} access`, node, state, sourcePath);
|
|
350
|
+
if (access !== 'read') {
|
|
351
|
+
throw invalidAt(state.sourceFile, node.pos, `${name} access must be "read".`, sourcePath);
|
|
352
|
+
}
|
|
353
|
+
return {
|
|
354
|
+
__kind: 'permission',
|
|
355
|
+
resource: name === 'FileAccess' ? 'file' : 'folder',
|
|
356
|
+
path: requireNonEmptyString(args[0], `${name} path`, node, state, sourcePath),
|
|
357
|
+
access,
|
|
358
|
+
};
|
|
359
|
+
}
|
|
272
360
|
case 'registerModel': {
|
|
273
361
|
argsRange(name, args, 1, 2, node, state, sourcePath);
|
|
274
362
|
const options = args[1] === undefined ? {} : requireObject(args[1], 'registerModel options', node, state, sourcePath);
|
|
@@ -279,6 +367,23 @@ function evaluateCall(node, state, sourcePath, _declarationName) {
|
|
|
279
367
|
think: options.think === undefined ? false : requireBoolean(options.think, 'think', node, state, sourcePath),
|
|
280
368
|
};
|
|
281
369
|
}
|
|
370
|
+
case 'registerProfile': {
|
|
371
|
+
argsRange(name, args, 1, 2, node, state, sourcePath);
|
|
372
|
+
const options = args[1] === undefined ? {} : requireObject(args[1], 'registerProfile options', node, state, sourcePath);
|
|
373
|
+
rejectUnknown(options, ['model', 'think', 'memory', 'history'], name, node, state, sourcePath);
|
|
374
|
+
return {
|
|
375
|
+
__kind: 'profile',
|
|
376
|
+
profileName: requireString(args[0], 'profile name', node, state, sourcePath),
|
|
377
|
+
...(options.model !== undefined
|
|
378
|
+
? { modelId: requireString(options.model, 'model', node, state, sourcePath) }
|
|
379
|
+
: {}),
|
|
380
|
+
...(options.think !== undefined
|
|
381
|
+
? { think: requireBoolean(options.think, 'think', node, state, sourcePath) }
|
|
382
|
+
: {}),
|
|
383
|
+
memory: options.memory === undefined ? false : requireBoolean(options.memory, 'memory', node, state, sourcePath),
|
|
384
|
+
history: options.history === undefined ? false : requireBoolean(options.history, 'history', node, state, sourcePath),
|
|
385
|
+
};
|
|
386
|
+
}
|
|
282
387
|
case 'TextResult': {
|
|
283
388
|
argsRange(name, args, 0, 1, node, state, sourcePath);
|
|
284
389
|
const options = args[0] === undefined ? {} : requireObject(args[0], 'TextResult options', node, state, sourcePath);
|
|
@@ -314,6 +419,45 @@ function evaluateCall(node, state, sourcePath, _declarationName) {
|
|
|
314
419
|
output: requireTagged(options.output, 'state', node, state, sourcePath),
|
|
315
420
|
};
|
|
316
421
|
}
|
|
422
|
+
case 'useProfileSkill': {
|
|
423
|
+
exactArgs(name, args, 3, node, state, sourcePath);
|
|
424
|
+
const profile = requireTagged(args[0], 'profile', node, state, sourcePath);
|
|
425
|
+
const skillName = typeof args[1] === 'string' ? args[1] : undefined;
|
|
426
|
+
const skillState = skillName === undefined
|
|
427
|
+
? requireTagged(args[1], 'state', node, state, sourcePath)
|
|
428
|
+
: undefined;
|
|
429
|
+
if (skillName !== undefined && !SAFE_SKILL_NAME.test(skillName)) {
|
|
430
|
+
throw invalidAt(state.sourceFile, node.pos, `Invalid profile skill name '${skillName}'.`, sourcePath);
|
|
431
|
+
}
|
|
432
|
+
const options = requireObject(args[2], 'useProfileSkill options', node, state, sourcePath);
|
|
433
|
+
rejectUnknown(options, ['skills', 'input', 'attachments', 'stripSkillName', 'parameters', 'output', 'result'], name, node, state, sourcePath);
|
|
434
|
+
if (skillName !== undefined && options.skills !== undefined) {
|
|
435
|
+
throw invalidAt(state.sourceFile, node.pos, 'useProfileSkill.skills is only valid when the Skill is selected by State.', sourcePath);
|
|
436
|
+
}
|
|
437
|
+
if (skillState !== undefined && options.skills === undefined) {
|
|
438
|
+
throw invalidAt(state.sourceFile, node.pos, 'A state-selected profile Skill requires a non-empty useProfileSkill.skills allowlist.', sourcePath);
|
|
439
|
+
}
|
|
440
|
+
return {
|
|
441
|
+
__kind: 'operation',
|
|
442
|
+
profile,
|
|
443
|
+
...(skillName !== undefined ? { skillName } : { skillState }),
|
|
444
|
+
...(options.skills !== undefined ? { skillOptions: options.skills } : {}),
|
|
445
|
+
...(options.input !== undefined
|
|
446
|
+
? { input: requireTagged(options.input, 'state', node, state, sourcePath) }
|
|
447
|
+
: {}),
|
|
448
|
+
...(options.attachments !== undefined
|
|
449
|
+
? { attachments: requireTagged(options.attachments, 'attachment_state', node, state, sourcePath) }
|
|
450
|
+
: {}),
|
|
451
|
+
...(options.stripSkillName !== undefined
|
|
452
|
+
? { stripSkillName: requireBoolean(options.stripSkillName, 'stripSkillName', node, state, sourcePath) }
|
|
453
|
+
: {}),
|
|
454
|
+
parameters: options.parameters === undefined
|
|
455
|
+
? {}
|
|
456
|
+
: requireObject(options.parameters, 'useProfileSkill parameters', node, state, sourcePath),
|
|
457
|
+
output: requireTagged(options.output, 'state', node, state, sourcePath),
|
|
458
|
+
result: requireTagged(options.result, 'text_result', node, state, sourcePath),
|
|
459
|
+
};
|
|
460
|
+
}
|
|
317
461
|
case 'trigger': {
|
|
318
462
|
exactArgs(name, args, 2, node, state, sourcePath);
|
|
319
463
|
const operation = requireTagged(args[0], 'operation', node, state, sourcePath);
|
|
@@ -342,10 +486,13 @@ function evaluateCall(node, state, sourcePath, _declarationName) {
|
|
|
342
486
|
case 'defineSkillApp': {
|
|
343
487
|
exactArgs(name, args, 1, node, state, sourcePath);
|
|
344
488
|
const template = requireObject(args[0], 'defineSkillApp template', node, state, sourcePath);
|
|
345
|
-
rejectUnknown(template, ['app', 'ui'], name, node, state, sourcePath);
|
|
489
|
+
rejectUnknown(template, ['app', 'permissions', 'ui'], name, node, state, sourcePath);
|
|
346
490
|
return {
|
|
347
491
|
__kind: 'skillapp',
|
|
348
492
|
app: requireObject(template.app, 'app metadata', node, state, sourcePath),
|
|
493
|
+
permissions: template.permissions === undefined
|
|
494
|
+
? []
|
|
495
|
+
: requireArray(template.permissions, 'defineSkillApp permissions', node, state, sourcePath),
|
|
349
496
|
ui: requireTagged(template.ui, 'component', node, state, sourcePath),
|
|
350
497
|
};
|
|
351
498
|
}
|
|
@@ -366,7 +513,7 @@ function evaluateCall(node, state, sourcePath, _declarationName) {
|
|
|
366
513
|
case 'Textarea': {
|
|
367
514
|
argsRange(name, args, 2, 3, node, state, sourcePath);
|
|
368
515
|
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);
|
|
516
|
+
rejectUnknown(options, ['showLabel', 'grow', 'editable', 'copyable', 'rows', 'autoGrow', 'placeholder'], name, node, state, sourcePath);
|
|
370
517
|
return {
|
|
371
518
|
__kind: 'component', component: 'textarea', label: requireNonEmptyString(args[0], 'Textarea label', node, state, sourcePath),
|
|
372
519
|
bind: requireTagged(args[1], 'state', node, state, sourcePath), options,
|
|
@@ -377,18 +524,28 @@ function evaluateCall(node, state, sourcePath, _declarationName) {
|
|
|
377
524
|
const options = requireObject(args[2], 'Select options', node, state, sourcePath);
|
|
378
525
|
rejectUnknown(options, ['options', 'showLabel', 'grow'], name, node, state, sourcePath);
|
|
379
526
|
const choices = requireArray(options.options, 'Select options.options', node, state, sourcePath)
|
|
380
|
-
.map(value =>
|
|
527
|
+
.map(value => requireSelectOption(value, node, state, sourcePath));
|
|
381
528
|
if (choices.length === 0)
|
|
382
529
|
throw invalidAt(state.sourceFile, node.pos, 'Select options cannot be empty.', sourcePath);
|
|
383
530
|
return {
|
|
384
531
|
__kind: 'component', component: 'select', label: requireNonEmptyString(args[0], 'Select label', node, state, sourcePath),
|
|
385
|
-
bind: requireTagged(args[1], 'state', node, state, sourcePath),
|
|
532
|
+
bind: requireTagged(args[1], 'state', node, state, sourcePath),
|
|
533
|
+
options: { ...options, options: choices },
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
case 'Attachments': {
|
|
537
|
+
argsRange(name, args, 2, 3, node, state, sourcePath);
|
|
538
|
+
const options = args[2] === undefined ? {} : requireObject(args[2], 'Attachments options', node, state, sourcePath);
|
|
539
|
+
rejectUnknown(options, ['showLabel', 'grow'], name, node, state, sourcePath);
|
|
540
|
+
return {
|
|
541
|
+
__kind: 'component', component: 'attachments', label: requireNonEmptyString(args[0], 'Attachments label', node, state, sourcePath),
|
|
542
|
+
bind: requireTagged(args[1], 'attachment_state', node, state, sourcePath), options,
|
|
386
543
|
};
|
|
387
544
|
}
|
|
388
545
|
case 'Button': {
|
|
389
546
|
exactArgs(name, args, 2, node, state, sourcePath);
|
|
390
547
|
const options = requireObject(args[1], 'Button options', node, state, sourcePath);
|
|
391
|
-
rejectUnknown(options, ['trigger', 'emphasis'], name, node, state, sourcePath);
|
|
548
|
+
rejectUnknown(options, ['trigger', 'emphasis', 'alignToField'], name, node, state, sourcePath);
|
|
392
549
|
return {
|
|
393
550
|
__kind: 'component', component: 'button', label: requireNonEmptyString(args[0], 'Button label', node, state, sourcePath),
|
|
394
551
|
operation: requireTagged(options.trigger, 'operation', node, state, sourcePath), options,
|
|
@@ -410,20 +567,28 @@ function normalizeComponent(value, state, sourcePath, depth = 0) {
|
|
|
410
567
|
if (value.label !== undefined)
|
|
411
568
|
item.label = requireString(value.label, `${component} label`, state.sourceFile, state, sourcePath);
|
|
412
569
|
if (value.bind !== undefined)
|
|
413
|
-
item.bind = requireNamedReference(value.bind, 'state', state.sourceFile, state, sourcePath);
|
|
570
|
+
item.bind = requireNamedReference(value.bind, component === 'attachments' ? 'attachment_state' : 'state', state.sourceFile, state, sourcePath);
|
|
414
571
|
if (value.operation !== undefined)
|
|
415
572
|
item.trigger = requireName(requireTagged(value.operation, 'operation', state.sourceFile, state, sourcePath), 'operation', state.sourceFile, state, sourcePath);
|
|
416
573
|
copyBoolean(options, 'showLabel', item, state, sourcePath);
|
|
417
574
|
copyBoolean(options, 'grow', item, state, sourcePath);
|
|
418
575
|
copyBoolean(options, 'editable', item, state, sourcePath);
|
|
419
576
|
copyBoolean(options, 'copyable', item, state, sourcePath);
|
|
577
|
+
copyBoolean(options, 'autoGrow', item, state, sourcePath);
|
|
578
|
+
copyBoolean(options, 'alignToField', item, state, sourcePath);
|
|
579
|
+
if (options.rows !== undefined) {
|
|
580
|
+
item.rows = requireNonNegativeInteger(options.rows, 'rows', state.sourceFile, state, sourcePath);
|
|
581
|
+
if (item.rows < 1 || item.rows > 40) {
|
|
582
|
+
throw MarifoldError_1.MarifoldError.appInvalid('Textarea rows must be between 1 and 40.', sourcePath);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
420
585
|
copyString(options, 'placeholder', item, state, sourcePath);
|
|
421
586
|
copyString(options, 'gap', item, state, sourcePath);
|
|
422
587
|
copyString(options, 'responsive', item, state, sourcePath);
|
|
423
588
|
copyString(options, 'emphasis', item, state, sourcePath);
|
|
424
589
|
if (options.options !== undefined) {
|
|
425
590
|
item.options = requireArray(options.options, 'Select options', state.sourceFile, state, sourcePath)
|
|
426
|
-
.map(choice =>
|
|
591
|
+
.map(choice => requireSelectOption(choice, state.sourceFile, state, sourcePath));
|
|
427
592
|
}
|
|
428
593
|
if (item.component === 'textarea' && item.editable === undefined)
|
|
429
594
|
item.editable = true;
|
|
@@ -438,30 +603,62 @@ function normalizeComponent(value, state, sourcePath, depth = 0) {
|
|
|
438
603
|
if (item.emphasis !== undefined && !['primary', 'secondary'].includes(item.emphasis)) {
|
|
439
604
|
throw MarifoldError_1.MarifoldError.appInvalid(`Invalid button emphasis '${item.emphasis}'.`, sourcePath);
|
|
440
605
|
}
|
|
441
|
-
if (item.options && new Set(item.options).size !== item.options.length) {
|
|
606
|
+
if (item.options && new Set(item.options.map(selectOptionValue)).size !== item.options.length) {
|
|
442
607
|
throw MarifoldError_1.MarifoldError.appInvalid('Select options must be unique.', sourcePath);
|
|
443
608
|
}
|
|
444
609
|
return item;
|
|
445
610
|
}
|
|
446
611
|
function validateReferences(state, layout, sourcePath) {
|
|
447
612
|
const stateNames = new Set(state.states.map(item => item.name));
|
|
613
|
+
const attachmentStateNames = new Set(state.attachmentStates.map(item => item.name));
|
|
448
614
|
const modelNames = new Set(state.models.map(item => item.name));
|
|
615
|
+
const profileNames = new Set(state.profiles.map(item => item.name));
|
|
449
616
|
const skillNames = new Set(state.skills.map(item => item.name));
|
|
450
617
|
const operationNames = new Set(state.operations.map(item => item.name));
|
|
451
618
|
assertUnique(state.models.map(item => item.name), 'model declarations', sourcePath);
|
|
619
|
+
assertUnique(state.profiles.map(item => item.name), 'profile declarations', sourcePath);
|
|
452
620
|
assertUnique(state.skills.map(item => item.name), 'registered Skills', sourcePath);
|
|
453
621
|
assertUnique(state.operations.map(item => item.name), 'operations', sourcePath);
|
|
622
|
+
assertUnique(state.attachmentStates.map(item => item.name), 'attachment state declarations', sourcePath);
|
|
454
623
|
if (state.states.length === 0)
|
|
455
624
|
throw MarifoldError_1.MarifoldError.appInvalid('SkillApp must declare at least one State.', sourcePath);
|
|
456
625
|
if (state.operations.length === 0)
|
|
457
|
-
throw MarifoldError_1.MarifoldError.appInvalid('SkillApp must declare at least one useSkill operation.', sourcePath);
|
|
626
|
+
throw MarifoldError_1.MarifoldError.appInvalid('SkillApp must declare at least one useSkill or useProfileSkill operation.', sourcePath);
|
|
458
627
|
for (const operation of state.operations) {
|
|
459
|
-
if (
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
628
|
+
if (operation.profile) {
|
|
629
|
+
if (!profileNames.has(operation.profile))
|
|
630
|
+
throw MarifoldError_1.MarifoldError.appInvalid(`Operation '${operation.name}' references missing profile '${operation.profile}'.`, sourcePath);
|
|
631
|
+
if (operation.model)
|
|
632
|
+
throw MarifoldError_1.MarifoldError.appInvalid(`Operation '${operation.name}' cannot reference both a profile and model.`, sourcePath);
|
|
633
|
+
const fixedSkill = operation.skill !== undefined;
|
|
634
|
+
const selectedSkill = operation.skillState !== undefined;
|
|
635
|
+
if (fixedSkill === selectedSkill) {
|
|
636
|
+
throw MarifoldError_1.MarifoldError.appInvalid(`Operation '${operation.name}' must reference exactly one fixed or state-selected profile Skill.`, sourcePath);
|
|
637
|
+
}
|
|
638
|
+
if (selectedSkill) {
|
|
639
|
+
if (!stateNames.has(operation.skillState))
|
|
640
|
+
throw MarifoldError_1.MarifoldError.appInvalid(`Operation '${operation.name}' references missing Skill state '${operation.skillState}'.`, sourcePath);
|
|
641
|
+
if (!operation.skillOptions?.length)
|
|
642
|
+
throw MarifoldError_1.MarifoldError.appInvalid(`Operation '${operation.name}' requires a non-empty Skill allowlist.`, sourcePath);
|
|
643
|
+
const initial = state.states.find(candidate => candidate.name === operation.skillState)?.initial;
|
|
644
|
+
if (initial !== undefined && !operation.skillOptions.includes(initial)) {
|
|
645
|
+
throw MarifoldError_1.MarifoldError.appInvalid(`Operation '${operation.name}' initial Skill '${initial}' is not allowlisted.`, sourcePath);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
else {
|
|
650
|
+
if (!operation.model || !modelNames.has(operation.model))
|
|
651
|
+
throw MarifoldError_1.MarifoldError.appInvalid(`Operation '${operation.name}' references missing model '${operation.model ?? ''}'.`, sourcePath);
|
|
652
|
+
if (!operation.skill || !skillNames.has(operation.skill))
|
|
653
|
+
throw MarifoldError_1.MarifoldError.appInvalid(`Operation '${operation.name}' references missing skill '${operation.skill ?? ''}'.`, sourcePath);
|
|
654
|
+
}
|
|
463
655
|
if (!stateNames.has(operation.output))
|
|
464
656
|
throw MarifoldError_1.MarifoldError.appInvalid(`Operation '${operation.name}' references missing output state '${operation.output}'.`, sourcePath);
|
|
657
|
+
if (operation.input && !stateNames.has(operation.input))
|
|
658
|
+
throw MarifoldError_1.MarifoldError.appInvalid(`Operation '${operation.name}' references missing input state '${operation.input}'.`, sourcePath);
|
|
659
|
+
if (operation.attachments && !attachmentStateNames.has(operation.attachments)) {
|
|
660
|
+
throw MarifoldError_1.MarifoldError.appInvalid(`Operation '${operation.name}' references missing attachment state '${operation.attachments}'.`, sourcePath);
|
|
661
|
+
}
|
|
465
662
|
for (const name of Object.values(operation.parameters)) {
|
|
466
663
|
if (!stateNames.has(name))
|
|
467
664
|
throw MarifoldError_1.MarifoldError.appInvalid(`Operation '${operation.name}' references missing state '${name}'.`, sourcePath);
|
|
@@ -470,16 +667,31 @@ function validateReferences(state, layout, sourcePath) {
|
|
|
470
667
|
const layoutItems = flatten(layout);
|
|
471
668
|
if (layoutItems.length > 100)
|
|
472
669
|
throw MarifoldError_1.MarifoldError.appInvalid('SkillApp layout cannot exceed 100 components.', sourcePath);
|
|
670
|
+
for (const operation of state.operations.filter(candidate => candidate.skillState)) {
|
|
671
|
+
const selectors = layoutItems.filter(item => item.component === 'select' && item.bind === operation.skillState);
|
|
672
|
+
if (selectors.length === 0) {
|
|
673
|
+
throw MarifoldError_1.MarifoldError.appInvalid(`Operation '${operation.name}' Skill state '${operation.skillState}' must bind to Select.`, sourcePath);
|
|
674
|
+
}
|
|
675
|
+
const selectable = new Set(selectors.flatMap(item => (item.options ?? []).map(selectOptionValue)));
|
|
676
|
+
const allowlisted = new Set(operation.skillOptions ?? []);
|
|
677
|
+
if (selectable.size !== allowlisted.size || [...selectable].some(skill => !allowlisted.has(skill))) {
|
|
678
|
+
throw MarifoldError_1.MarifoldError.appInvalid(`Operation '${operation.name}' Select options must exactly match its Skill allowlist.`, sourcePath);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
473
681
|
for (const item of layoutItems) {
|
|
474
682
|
if (item.component === 'app')
|
|
475
683
|
throw MarifoldError_1.MarifoldError.appInvalid('App(...) can only be the root UI component.', sourcePath);
|
|
476
|
-
if (item.bind && !
|
|
684
|
+
if (item.bind && item.component === 'attachments' && !attachmentStateNames.has(item.bind)) {
|
|
685
|
+
throw MarifoldError_1.MarifoldError.appInvalid(`Layout references missing attachment state '${item.bind}'.`, sourcePath);
|
|
686
|
+
}
|
|
687
|
+
if (item.bind && item.component !== 'attachments' && !stateNames.has(item.bind)) {
|
|
477
688
|
throw MarifoldError_1.MarifoldError.appInvalid(`Layout references missing state '${item.bind}'.`, sourcePath);
|
|
689
|
+
}
|
|
478
690
|
if (item.trigger && !operationNames.has(item.trigger))
|
|
479
691
|
throw MarifoldError_1.MarifoldError.appInvalid(`Button references missing operation '${item.trigger}'.`, sourcePath);
|
|
480
692
|
if (item.component === 'select' && item.bind) {
|
|
481
693
|
const initial = state.states.find(candidate => candidate.name === item.bind)?.initial;
|
|
482
|
-
if (initial !== undefined && !(item.options ?? []).includes(initial)) {
|
|
694
|
+
if (initial !== undefined && !(item.options ?? []).map(selectOptionValue).includes(initial)) {
|
|
483
695
|
throw MarifoldError_1.MarifoldError.appInvalid(`Select state '${item.bind}' initial value must be one of its options.`, sourcePath);
|
|
484
696
|
}
|
|
485
697
|
}
|
|
@@ -494,9 +706,11 @@ function validateReferences(state, layout, sourcePath) {
|
|
|
494
706
|
}
|
|
495
707
|
const outputStates = new Set(state.operations.map(operation => operation.output));
|
|
496
708
|
for (const operation of state.operations) {
|
|
497
|
-
for (const input of Object.values(operation.parameters)) {
|
|
709
|
+
for (const input of [operation.input, operation.skillState, ...Object.values(operation.parameters)]) {
|
|
710
|
+
if (!input)
|
|
711
|
+
continue;
|
|
498
712
|
if (outputStates.has(input))
|
|
499
|
-
throw MarifoldError_1.MarifoldError.appInvalid(`Operation '${operation.name}' cannot use output state '${input}' as
|
|
713
|
+
throw MarifoldError_1.MarifoldError.appInvalid(`Operation '${operation.name}' cannot use output state '${input}' as an input.`, sourcePath);
|
|
500
714
|
}
|
|
501
715
|
}
|
|
502
716
|
for (const item of layoutItems) {
|
|
@@ -516,6 +730,32 @@ function validateReferences(state, layout, sourcePath) {
|
|
|
516
730
|
}
|
|
517
731
|
}
|
|
518
732
|
}
|
|
733
|
+
function normalizePermissions(values, state, sourcePath) {
|
|
734
|
+
const permissions = values.map(value => ({
|
|
735
|
+
kind: requireString(value.resource, 'permission resource', state.sourceFile, state, sourcePath),
|
|
736
|
+
path: requireNonEmptyString(value.path, 'permission path', state.sourceFile, state, sourcePath),
|
|
737
|
+
access: requireString(value.access, 'permission access', state.sourceFile, state, sourcePath),
|
|
738
|
+
}));
|
|
739
|
+
const duplicate = permissions.find((permission, index) => permissions.findIndex(candidate => (candidate.kind === permission.kind && candidate.path === permission.path)) !== index);
|
|
740
|
+
if (duplicate)
|
|
741
|
+
throw MarifoldError_1.MarifoldError.appInvalid(`Duplicate ${duplicate.kind} permission '${duplicate.path}'.`, sourcePath);
|
|
742
|
+
return permissions;
|
|
743
|
+
}
|
|
744
|
+
function parseModelId(id, node, state, sourcePath) {
|
|
745
|
+
const separator = id.indexOf('/');
|
|
746
|
+
if (separator <= 0 || separator === id.length - 1) {
|
|
747
|
+
throw invalidAt(state.sourceFile, node.pos, `Model '${id}' must use provider/model format.`, sourcePath);
|
|
748
|
+
}
|
|
749
|
+
const provider = id.slice(0, separator);
|
|
750
|
+
if (!SAFE_PROVIDER_NAME.test(provider)) {
|
|
751
|
+
throw invalidAt(state.sourceFile, node.pos, `Invalid model provider '${provider}'.`, sourcePath);
|
|
752
|
+
}
|
|
753
|
+
const model = id.slice(separator + 1);
|
|
754
|
+
if (model.trim() !== model || model.length === 0) {
|
|
755
|
+
throw invalidAt(state.sourceFile, node.pos, `Invalid model id '${model}'.`, sourcePath);
|
|
756
|
+
}
|
|
757
|
+
return { provider, model };
|
|
758
|
+
}
|
|
519
759
|
function assertUnique(values, label, sourcePath) {
|
|
520
760
|
const seen = new Set();
|
|
521
761
|
for (const value of values) {
|
|
@@ -572,6 +812,36 @@ function requireArray(value, label, node, state, sourcePath) {
|
|
|
572
812
|
return value;
|
|
573
813
|
throw invalidAt(state.sourceFile, node.pos, `Expected ${label} to be an array.`, sourcePath);
|
|
574
814
|
}
|
|
815
|
+
function requireSelectOption(value, node, state, sourcePath) {
|
|
816
|
+
if (typeof value === 'string') {
|
|
817
|
+
return requireNonEmptyString(value, 'Select option', node, state, sourcePath);
|
|
818
|
+
}
|
|
819
|
+
const option = requireObject(value, 'Select option', node, state, sourcePath);
|
|
820
|
+
rejectUnknown(option, ['label', 'value'], 'Select option', node, state, sourcePath);
|
|
821
|
+
return {
|
|
822
|
+
label: requireNonEmptyString(option.label, 'Select option label', node, state, sourcePath),
|
|
823
|
+
value: requireNonEmptyString(option.value, 'Select option value', node, state, sourcePath),
|
|
824
|
+
};
|
|
825
|
+
}
|
|
826
|
+
function requireSkillOptionValues(value, node, state, sourcePath) {
|
|
827
|
+
const skills = requireArray(value, 'useProfileSkill skills', node, state, sourcePath)
|
|
828
|
+
.map(option => selectOptionValue(requireSelectOption(option, node, state, sourcePath)));
|
|
829
|
+
if (skills.length === 0) {
|
|
830
|
+
throw invalidAt(state.sourceFile, node.pos, 'useProfileSkill.skills cannot be empty.', sourcePath);
|
|
831
|
+
}
|
|
832
|
+
for (const skill of skills) {
|
|
833
|
+
if (!SAFE_SKILL_NAME.test(skill)) {
|
|
834
|
+
throw invalidAt(state.sourceFile, node.pos, `Invalid profile skill name '${skill}'.`, sourcePath);
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
if (new Set(skills).size !== skills.length) {
|
|
838
|
+
throw invalidAt(state.sourceFile, node.pos, 'useProfileSkill.skills must be unique.', sourcePath);
|
|
839
|
+
}
|
|
840
|
+
return skills;
|
|
841
|
+
}
|
|
842
|
+
function selectOptionValue(option) {
|
|
843
|
+
return typeof option === 'string' ? option : option.value;
|
|
844
|
+
}
|
|
575
845
|
function requireString(value, label, node, state, sourcePath) {
|
|
576
846
|
if (typeof value === 'string')
|
|
577
847
|
return value;
|