@json-layout/core 2.4.0 → 2.5.1
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/package.json +8 -1
- package/src/i18n/de.js +0 -19
- package/src/i18n/en.js +0 -19
- package/src/i18n/fr.js +0 -19
- package/src/i18n/nl.js +0 -19
- package/src/i18n/types.ts +0 -19
- package/src/state/index.js +12 -11
- package/src/state/state-node.js +19 -13
- package/src/state/state-tree.js +13 -1
- package/src/state/types.ts +5 -2
- package/src/webmcp/index.js +283 -0
- package/src/webmcp/project.js +148 -0
- package/src/webmcp/resolve.js +25 -0
- package/src/webmcp/tools/describe-state.js +74 -0
- package/src/webmcp/tools/fill-form-skill.js +67 -0
- package/src/webmcp/tools/get-data.js +36 -0
- package/src/webmcp/tools/get-field-suggestions.js +84 -0
- package/src/webmcp/tools/set-data.js +56 -0
- package/src/webmcp/tools/set-field-value.js +71 -0
- package/types/i18n/de.d.ts +0 -19
- package/types/i18n/en.d.ts +0 -19
- package/types/i18n/fr.d.ts +0 -19
- package/types/i18n/nl.d.ts +0 -19
- package/types/i18n/types.d.ts +0 -19
- package/types/i18n/types.d.ts.map +1 -1
- package/types/state/index.d.ts.map +1 -1
- package/types/state/state-node.d.ts +2 -1
- package/types/state/state-node.d.ts.map +1 -1
- package/types/state/state-tree.d.ts.map +1 -1
- package/types/state/types.d.ts +5 -2
- package/types/state/types.d.ts.map +1 -1
- package/types/webmcp/index.d.ts +78 -0
- package/types/webmcp/index.d.ts.map +1 -0
- package/types/webmcp/project.d.ts +59 -0
- package/types/webmcp/project.d.ts.map +1 -0
- package/types/webmcp/resolve.d.ts +11 -0
- package/types/webmcp/resolve.d.ts.map +1 -0
- package/types/webmcp/tools/describe-state.d.ts +70 -0
- package/types/webmcp/tools/describe-state.d.ts.map +1 -0
- package/types/webmcp/tools/fill-form-skill.d.ts +24 -0
- package/types/webmcp/tools/fill-form-skill.d.ts.map +1 -0
- package/types/webmcp/tools/get-data.d.ts +31 -0
- package/types/webmcp/tools/get-data.d.ts.map +1 -0
- package/types/webmcp/tools/get-field-suggestions.d.ts +66 -0
- package/types/webmcp/tools/get-field-suggestions.d.ts.map +1 -0
- package/types/webmcp/tools/set-data.d.ts +65 -0
- package/types/webmcp/tools/set-data.d.ts.map +1 -0
- package/types/webmcp/tools/set-field-value.d.ts +73 -0
- package/types/webmcp/tools/set-field-value.d.ts.map +1 -0
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file WebMCP integration for JSON Layout StatefulLayout
|
|
3
|
+
* @description Provides MCP tool descriptors that work with browser's navigator.modelContext
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import debug from 'debug'
|
|
7
|
+
|
|
8
|
+
import * as describeState from './tools/describe-state.js'
|
|
9
|
+
import * as setFieldValue from './tools/set-field-value.js'
|
|
10
|
+
import * as setData from './tools/set-data.js'
|
|
11
|
+
import * as getData from './tools/get-data.js'
|
|
12
|
+
import * as getFieldSuggestions from './tools/get-field-suggestions.js'
|
|
13
|
+
import * as fillFormSkill from './tools/fill-form-skill.js'
|
|
14
|
+
|
|
15
|
+
/** @typedef {import('@mcp-b/webmcp-types').ToolDescriptor} ToolDescriptor */
|
|
16
|
+
|
|
17
|
+
const log = debug('jl:webmcp')
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @typedef {object} WebMCPOptions
|
|
21
|
+
* @property {string} [prefixName] - Prefix for all tool names
|
|
22
|
+
* @property {string} [dataTitle] - Title used in descriptions (default: 'form')
|
|
23
|
+
* @property {object} [schema] - The original JSON schema
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* WebMCP class that provides MCP tool descriptors for a StatefulLayout instance
|
|
28
|
+
*/
|
|
29
|
+
export class WebMCP {
|
|
30
|
+
/**
|
|
31
|
+
* @readonly
|
|
32
|
+
* @type {import('../state/index.js').StatefulLayout}
|
|
33
|
+
*/
|
|
34
|
+
_statefulLayout
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @readonly
|
|
38
|
+
* @type {string}
|
|
39
|
+
*/
|
|
40
|
+
_prefixName
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @readonly
|
|
44
|
+
* @type {string}
|
|
45
|
+
*/
|
|
46
|
+
_dataTitle
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* @readonly
|
|
50
|
+
* @type {string}
|
|
51
|
+
*/
|
|
52
|
+
_skill
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @readonly
|
|
56
|
+
* @type {object | null}
|
|
57
|
+
*/
|
|
58
|
+
_schema = null
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* @type {string[]}
|
|
62
|
+
*/
|
|
63
|
+
_registeredTools = []
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* @param {import('../state/index.js').StatefulLayout} statefulLayout
|
|
67
|
+
* @param {WebMCPOptions} [options]
|
|
68
|
+
*/
|
|
69
|
+
constructor (statefulLayout, options = {}) {
|
|
70
|
+
this._statefulLayout = statefulLayout
|
|
71
|
+
this._prefixName = options.prefixName || ''
|
|
72
|
+
this._dataTitle = options.dataTitle || 'form'
|
|
73
|
+
this._schema = options.schema || null
|
|
74
|
+
this._skill = fillFormSkill.generateSkill(this._dataTitle, this._prefixName, !!this._schema, this._statefulLayout)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* @param {string} name
|
|
79
|
+
* @returns {string}
|
|
80
|
+
*/
|
|
81
|
+
_toolName (name) {
|
|
82
|
+
return this._prefixName + name
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* @returns {ToolDescriptor[]}
|
|
87
|
+
*/
|
|
88
|
+
getTools () {
|
|
89
|
+
const dataTitle = this._dataTitle
|
|
90
|
+
|
|
91
|
+
/** @type {ToolDescriptor[]} */
|
|
92
|
+
const tools = [
|
|
93
|
+
{
|
|
94
|
+
name: this._toolName('fillFormSkill'),
|
|
95
|
+
description: fillFormSkill.getDescription(dataTitle),
|
|
96
|
+
outputSchema: { type: 'string' },
|
|
97
|
+
execute: async (args) => {
|
|
98
|
+
try {
|
|
99
|
+
return {
|
|
100
|
+
content: [{ type: 'text', text: this._skill }]
|
|
101
|
+
}
|
|
102
|
+
} catch (err) {
|
|
103
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
104
|
+
return {
|
|
105
|
+
content: [{ type: 'text', text: `Error: ${message}` }],
|
|
106
|
+
isError: true
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
name: this._toolName('getData'),
|
|
113
|
+
description: getData.getDescription(dataTitle),
|
|
114
|
+
inputSchema: getData.inputSchema,
|
|
115
|
+
outputSchema: getData.outputSchema,
|
|
116
|
+
execute: async (args) => {
|
|
117
|
+
try {
|
|
118
|
+
const result = getData.execute(this._statefulLayout, args || {})
|
|
119
|
+
return {
|
|
120
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }]
|
|
121
|
+
}
|
|
122
|
+
} catch (err) {
|
|
123
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
124
|
+
return {
|
|
125
|
+
content: [{ type: 'text', text: `Error: ${message}` }],
|
|
126
|
+
isError: true
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
name: this._toolName('setData'),
|
|
133
|
+
description: setData.getDescription(dataTitle),
|
|
134
|
+
inputSchema: setData.inputSchema,
|
|
135
|
+
outputSchema: setData.outputSchema,
|
|
136
|
+
execute: async (args) => {
|
|
137
|
+
try {
|
|
138
|
+
if (!args?.data) {
|
|
139
|
+
throw new Error('data is required')
|
|
140
|
+
}
|
|
141
|
+
const result = setData.execute(
|
|
142
|
+
this._statefulLayout,
|
|
143
|
+
/** @type {{ data: unknown }} */(args)
|
|
144
|
+
)
|
|
145
|
+
return {
|
|
146
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }]
|
|
147
|
+
}
|
|
148
|
+
} catch (err) {
|
|
149
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
150
|
+
return {
|
|
151
|
+
content: [{ type: 'text', text: `Error: ${message}` }],
|
|
152
|
+
isError: true
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
name: this._toolName('describeState'),
|
|
159
|
+
description: describeState.getDescription(dataTitle),
|
|
160
|
+
inputSchema: describeState.inputSchema,
|
|
161
|
+
outputSchema: describeState.outputSchema,
|
|
162
|
+
execute: async (args) => {
|
|
163
|
+
try {
|
|
164
|
+
const result = describeState.execute(this._statefulLayout, args || {})
|
|
165
|
+
return {
|
|
166
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }]
|
|
167
|
+
}
|
|
168
|
+
} catch (err) {
|
|
169
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
170
|
+
return {
|
|
171
|
+
content: [{ type: 'text', text: `Error: ${message}` }],
|
|
172
|
+
isError: true
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
name: this._toolName('setFieldValue'),
|
|
179
|
+
description: setFieldValue.getDescription(dataTitle),
|
|
180
|
+
inputSchema: setFieldValue.inputSchema,
|
|
181
|
+
outputSchema: setFieldValue.outputSchema,
|
|
182
|
+
execute: async (args) => {
|
|
183
|
+
try {
|
|
184
|
+
if (!args?.path) {
|
|
185
|
+
throw new Error('path is required')
|
|
186
|
+
}
|
|
187
|
+
const result = setFieldValue.execute(
|
|
188
|
+
this._statefulLayout,
|
|
189
|
+
/** @type {{ path: string, value: unknown }} */(args)
|
|
190
|
+
)
|
|
191
|
+
return {
|
|
192
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }]
|
|
193
|
+
}
|
|
194
|
+
} catch (err) {
|
|
195
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
196
|
+
return {
|
|
197
|
+
content: [{ type: 'text', text: `Error: ${message}` }],
|
|
198
|
+
isError: true
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
name: this._toolName('getFieldSuggestions'),
|
|
205
|
+
description: getFieldSuggestions.getDescription(dataTitle),
|
|
206
|
+
inputSchema: getFieldSuggestions.inputSchema,
|
|
207
|
+
outputSchema: getFieldSuggestions.outputSchema,
|
|
208
|
+
execute: async (args) => {
|
|
209
|
+
try {
|
|
210
|
+
if (!args?.path) {
|
|
211
|
+
throw new Error('path is required')
|
|
212
|
+
}
|
|
213
|
+
const result = await getFieldSuggestions.execute(
|
|
214
|
+
this._statefulLayout,
|
|
215
|
+
/** @type {{ path: string, query?: string }} */(args)
|
|
216
|
+
)
|
|
217
|
+
return {
|
|
218
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }]
|
|
219
|
+
}
|
|
220
|
+
} catch (err) {
|
|
221
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
222
|
+
return {
|
|
223
|
+
content: [{ type: 'text', text: `Error: ${message}` }],
|
|
224
|
+
isError: true
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
]
|
|
230
|
+
|
|
231
|
+
if (this._schema) {
|
|
232
|
+
tools.push({
|
|
233
|
+
name: this._toolName('getSchema'),
|
|
234
|
+
description: `Get the the JSON schema that governs "${dataTitle}" form.`,
|
|
235
|
+
outputSchema: {
|
|
236
|
+
type: 'object',
|
|
237
|
+
properties: {
|
|
238
|
+
jsonSchema: {},
|
|
239
|
+
}
|
|
240
|
+
},
|
|
241
|
+
execute: async (args) => {
|
|
242
|
+
return {
|
|
243
|
+
jsonSchema: this._schema
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
})
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return tools
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* @returns {Promise<void>}
|
|
254
|
+
*/
|
|
255
|
+
async registerTools () {
|
|
256
|
+
if (typeof navigator === 'undefined' || !navigator.modelContext) {
|
|
257
|
+
throw new Error('navigator.modelContext is not available')
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const tools = this.getTools()
|
|
261
|
+
for (const tool of tools) {
|
|
262
|
+
log('registering tool:', tool.name)
|
|
263
|
+
// @ts-ignore - complex generic types from webmcp-types
|
|
264
|
+
await navigator.modelContext.registerTool(tool)
|
|
265
|
+
this._registeredTools.push(tool.name)
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* @returns {Promise<void>}
|
|
271
|
+
*/
|
|
272
|
+
async unregisterTools () {
|
|
273
|
+
if (typeof navigator === 'undefined' || !navigator.modelContext) {
|
|
274
|
+
throw new Error('navigator.modelContext is not available')
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
for (const name of this._registeredTools) {
|
|
278
|
+
log('unregistering tool:', name)
|
|
279
|
+
await navigator.modelContext.unregisterTool(name)
|
|
280
|
+
}
|
|
281
|
+
this._registeredTools = []
|
|
282
|
+
}
|
|
283
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Projection functions for webmcp tools
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { isItemsLayout } from '@json-layout/vocabulary'
|
|
6
|
+
|
|
7
|
+
const constraintKeys = {
|
|
8
|
+
'number-field': ['min', 'max', 'step', 'precision'],
|
|
9
|
+
slider: ['min', 'max', 'step'],
|
|
10
|
+
'date-picker': ['min', 'max', 'format'],
|
|
11
|
+
'date-time-picker': ['min', 'max'],
|
|
12
|
+
'time-picker': ['min', 'max'],
|
|
13
|
+
combobox: ['separator'],
|
|
14
|
+
'number-combobox': ['separator']
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @param {string} comp
|
|
19
|
+
* @returns {string[]|undefined}
|
|
20
|
+
*/
|
|
21
|
+
function getConstraintKeys (comp) {
|
|
22
|
+
// @ts-ignore - complex union type not fully represented
|
|
23
|
+
return constraintKeys[comp]
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @param {import('../state/types.js').StateNode} node
|
|
28
|
+
* @param {import('../state/index.js').StatefulLayout} statefulLayout
|
|
29
|
+
* @returns {{
|
|
30
|
+
* key: string|number,
|
|
31
|
+
* path: string,
|
|
32
|
+
* comp: string,
|
|
33
|
+
* data: unknown,
|
|
34
|
+
* title?: string,
|
|
35
|
+
* label?: string,
|
|
36
|
+
* help?: string,
|
|
37
|
+
* error?: string,
|
|
38
|
+
* childError?: boolean,
|
|
39
|
+
* required?: boolean,
|
|
40
|
+
* readOnly?: boolean,
|
|
41
|
+
* constraints?: Record<string, unknown>,
|
|
42
|
+
* oneOfItems?: Array<{key: number, title: string}>,
|
|
43
|
+
* children?: Array<any>
|
|
44
|
+
* getSuffections?: boolean
|
|
45
|
+
* }}
|
|
46
|
+
*/
|
|
47
|
+
export function projectNode (node, statefulLayout) {
|
|
48
|
+
/**
|
|
49
|
+
* @type {{
|
|
50
|
+
* key: string|number,
|
|
51
|
+
* path: string,
|
|
52
|
+
* comp: string,
|
|
53
|
+
* data: unknown,
|
|
54
|
+
* title?: string,
|
|
55
|
+
* label?: string,
|
|
56
|
+
* help?: string,
|
|
57
|
+
* error?: string,
|
|
58
|
+
* childError?: boolean,
|
|
59
|
+
* required?: boolean,
|
|
60
|
+
* readOnly?: boolean,
|
|
61
|
+
* constraints?: Record<string, unknown>,
|
|
62
|
+
* oneOfItems?: Array<{key: number, title: string}>,
|
|
63
|
+
* children?: Array<any>
|
|
64
|
+
* getSuggestions?: boolean
|
|
65
|
+
* }}
|
|
66
|
+
*/
|
|
67
|
+
const out = {
|
|
68
|
+
key: node.key,
|
|
69
|
+
path: node.fullKey,
|
|
70
|
+
comp: node.layout.comp,
|
|
71
|
+
data: node.data
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const layout = /** @type {Record<string, unknown>} */(node.layout)
|
|
75
|
+
if (typeof layout.title === 'string') out.title = layout.title
|
|
76
|
+
if (typeof layout.label === 'string') out.label = layout.label
|
|
77
|
+
if (node.layout.help) out.help = node.layout.help
|
|
78
|
+
|
|
79
|
+
if (node.error) out.error = node.error
|
|
80
|
+
if (node.childError) out.childError = true
|
|
81
|
+
|
|
82
|
+
if (node.skeleton.required) out.required = true
|
|
83
|
+
if (node.options.readOnly) out.readOnly = true
|
|
84
|
+
if (isItemsLayout(node.layout, statefulLayout.compiledLayout.components)) out.getSuggestions = true
|
|
85
|
+
|
|
86
|
+
const keys = getConstraintKeys(node.layout.comp)
|
|
87
|
+
if (keys) {
|
|
88
|
+
/** @type {Record<string, unknown>} */
|
|
89
|
+
const constraints = {}
|
|
90
|
+
for (const k of keys) {
|
|
91
|
+
const v = layout[k]
|
|
92
|
+
if (v !== undefined && v !== null) constraints[k] = v
|
|
93
|
+
}
|
|
94
|
+
if (Object.keys(constraints).length > 0) out.constraints = constraints
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (node.layout.comp === 'one-of-select' && Array.isArray(layout.oneOfItems)) {
|
|
98
|
+
out.oneOfItems = layout.oneOfItems
|
|
99
|
+
.filter((item) => !item.header)
|
|
100
|
+
.map((item) => ({ key: item.key, title: item.title }))
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (node.children) {
|
|
104
|
+
out.children = node.children
|
|
105
|
+
.filter((c) => c.layout.comp !== 'none')
|
|
106
|
+
.map(node => projectNode(node, statefulLayout))
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return out
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* @param {import('../state/types.js').StateTree} stateTree
|
|
114
|
+
* @param {import('../state/index.js').StatefulLayout} statefulLayout
|
|
115
|
+
* @returns {{ root: ReturnType<typeof projectNode>, valid: boolean }}
|
|
116
|
+
*/
|
|
117
|
+
export function projectStateTree (stateTree, statefulLayout) {
|
|
118
|
+
return {
|
|
119
|
+
root: projectNode(stateTree.root, statefulLayout),
|
|
120
|
+
valid: stateTree.valid
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* @param {import('../state/types.js').StateNode} node
|
|
126
|
+
* @returns {Array<{path: string, message: string}>}
|
|
127
|
+
*/
|
|
128
|
+
export function collectErrors (node) {
|
|
129
|
+
/** @type {Array<{path: string, message: string}>} */
|
|
130
|
+
const errors = []
|
|
131
|
+
collectErrorsRecurse(node, errors)
|
|
132
|
+
return errors
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* @param {import('../state/types.js').StateNode} node
|
|
137
|
+
* @param {Array<{path: string, message: string}>} errors
|
|
138
|
+
*/
|
|
139
|
+
function collectErrorsRecurse (node, errors) {
|
|
140
|
+
if (node.error) {
|
|
141
|
+
errors.push({ path: node.fullKey, message: node.error })
|
|
142
|
+
}
|
|
143
|
+
if (node.children) {
|
|
144
|
+
for (const child of node.children) {
|
|
145
|
+
collectErrorsRecurse(child, errors)
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Node resolution for webmcp tools
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Navigate from a root StateNode to a descendant node by path.
|
|
7
|
+
* @param {import('../state/types.js').StateNode} root
|
|
8
|
+
* @param {string} path
|
|
9
|
+
* @returns {import('../state/types.js').StateNode|undefined}
|
|
10
|
+
*/
|
|
11
|
+
export function resolveNode (root, path) {
|
|
12
|
+
if (!path || path === '/') return root
|
|
13
|
+
|
|
14
|
+
const segments = path.replace(/^\//, '').split('/')
|
|
15
|
+
/** @type {import('../state/types.js').StateNode|undefined} */
|
|
16
|
+
let current = root
|
|
17
|
+
|
|
18
|
+
for (const segment of segments) {
|
|
19
|
+
if (!current?.children) return undefined
|
|
20
|
+
const key = /^\d+$/.test(segment) ? parseInt(segment, 10) : segment
|
|
21
|
+
current = current.children.find((c) => c.key === key)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return current
|
|
25
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file describeState tool
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { projectStateTree, projectNode, collectErrors } from '../project.js'
|
|
6
|
+
import { resolveNode } from '../resolve.js'
|
|
7
|
+
|
|
8
|
+
export const inputSchema = {
|
|
9
|
+
type: 'object',
|
|
10
|
+
properties: {
|
|
11
|
+
path: {
|
|
12
|
+
type: 'string',
|
|
13
|
+
description: 'Path to a specific node (e.g. "/address/city"). Omit for full tree.'
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const outputSchema = {
|
|
19
|
+
type: 'object',
|
|
20
|
+
properties: {
|
|
21
|
+
state: {
|
|
22
|
+
type: 'object',
|
|
23
|
+
description: 'Projected state tree or single node'
|
|
24
|
+
},
|
|
25
|
+
valid: {
|
|
26
|
+
type: 'boolean'
|
|
27
|
+
},
|
|
28
|
+
errors: {
|
|
29
|
+
type: 'array',
|
|
30
|
+
items: {
|
|
31
|
+
type: 'object',
|
|
32
|
+
properties: {
|
|
33
|
+
path: { type: 'string' },
|
|
34
|
+
message: { type: 'string' }
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @param {string} dataTitle
|
|
43
|
+
* @returns {string}
|
|
44
|
+
*/
|
|
45
|
+
export function getDescription (dataTitle) {
|
|
46
|
+
return `Describe the current "${dataTitle}" state tree. Optionally focus on a subtree by path to reduce output size.`
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @param {import('../../state/index.js').StatefulLayout} statefulLayout
|
|
51
|
+
* @param {{ path?: string }} args
|
|
52
|
+
* @returns {{state: ReturnType<typeof projectStateTree>|ReturnType<typeof projectNode>, valid: boolean, errors: Array<{path: string, message: string}>}}
|
|
53
|
+
*/
|
|
54
|
+
export function execute (statefulLayout, args) {
|
|
55
|
+
const errors = collectErrors(statefulLayout.stateTree.root)
|
|
56
|
+
|
|
57
|
+
if (args.path) {
|
|
58
|
+
const node = resolveNode(statefulLayout.stateTree.root, args.path)
|
|
59
|
+
if (!node) {
|
|
60
|
+
throw new Error(`node not found at path: ${args.path}`)
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
state: projectNode(node, statefulLayout),
|
|
64
|
+
valid: statefulLayout.valid,
|
|
65
|
+
errors
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
state: projectStateTree(statefulLayout.stateTree, statefulLayout),
|
|
71
|
+
valid: statefulLayout.valid,
|
|
72
|
+
errors
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file fillFormSkill tool
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export const outputSchema = {
|
|
6
|
+
type: 'object',
|
|
7
|
+
properties: {
|
|
8
|
+
content: { type: 'string' }
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @param {string} dataTitle
|
|
14
|
+
* @returns {string}
|
|
15
|
+
*/
|
|
16
|
+
export function getDescription (dataTitle) {
|
|
17
|
+
return `Get guidance on how to interact with the form "${dataTitle}" using the available tools.`
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Generate skill content with dataTitle injected
|
|
22
|
+
* @param {string} dataTitle
|
|
23
|
+
* @param {string} prefixName
|
|
24
|
+
* @param {boolean} hasSchema
|
|
25
|
+
* @param {import('../../state/index.js').StatefulLayout} statefulLayout
|
|
26
|
+
* @returns {string}
|
|
27
|
+
*/
|
|
28
|
+
export function generateSkill (dataTitle, prefixName, hasSchema, statefulLayout) {
|
|
29
|
+
/** @type {"small" | "medium" | "large"} */
|
|
30
|
+
let complexity = 'small'
|
|
31
|
+
const nbNormalizedLayouts = Object.keys(statefulLayout.compiledLayout.normalizedLayouts).length
|
|
32
|
+
if (nbNormalizedLayouts > 15) complexity = 'medium'
|
|
33
|
+
if (nbNormalizedLayouts > 50) complexity = 'large'
|
|
34
|
+
let skill = `# JSON ${dataTitle.charAt(0).toUpperCase() + dataTitle.slice(1)} Form-Filling Guide
|
|
35
|
+
|
|
36
|
+
This guide teaches you how to use tools to fill the data of a form in the user's page.
|
|
37
|
+
|
|
38
|
+
Always start by getting the current data using ${prefixName}getData.
|
|
39
|
+
`
|
|
40
|
+
|
|
41
|
+
if (complexity === 'small') {
|
|
42
|
+
skill += `
|
|
43
|
+
Given the small complexity of this form you should start by reading the full schema definition using ${prefixName}${hasSchema ? 'getSchema' : 'describeState'} and attempt updating the whole data using ${prefixName}setData.
|
|
44
|
+
Only use ${prefixName}describeState and iterate with ${prefixName}setFieldValue if you encounter some difficulties with ${prefixName}setData.
|
|
45
|
+
`
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (complexity === 'small') {
|
|
49
|
+
skill += `
|
|
50
|
+
Given the medium complexity of this form you should start by reading the full schema definition using ${prefixName}${hasSchema ? 'getSchema' : 'describeState'}, if you have a satisfying understanding of the schema you can attempt updating the whole data using ${prefixName}setData at least once.
|
|
51
|
+
Then use ${prefixName}describeState and iterate with ${prefixName}setFieldValue.
|
|
52
|
+
`
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (complexity === 'large') {
|
|
56
|
+
skill += `
|
|
57
|
+
Given the large complexity of this form you should avoid reading the full schema definition using ${prefixName}${hasSchema ? 'getSchema' : 'describeState'}.
|
|
58
|
+
Prefer using ${prefixName}describeState and iterating with ${prefixName}setFieldValue.
|
|
59
|
+
`
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
skill += `
|
|
63
|
+
If you encounter getItems definitions in the schema or getSuggestions flags in the state. You must use ${prefixName}getFieldSuggestions to fetch the accepted values.
|
|
64
|
+
`
|
|
65
|
+
|
|
66
|
+
return skill
|
|
67
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file getData tool
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export const inputSchema = {
|
|
6
|
+
type: 'object',
|
|
7
|
+
properties: {}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const outputSchema = {
|
|
11
|
+
type: 'object',
|
|
12
|
+
properties: {
|
|
13
|
+
data: {},
|
|
14
|
+
valid: { type: 'boolean' }
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @param {string} dataTitle
|
|
20
|
+
* @returns {string}
|
|
21
|
+
*/
|
|
22
|
+
export function getDescription (dataTitle) {
|
|
23
|
+
return `Get the current "${dataTitle}" data and validity status.`
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @param {import('../../state/index.js').StatefulLayout} statefulLayout
|
|
28
|
+
* @param {{}} _args
|
|
29
|
+
* @returns {{ data: unknown, valid: boolean }}
|
|
30
|
+
*/
|
|
31
|
+
export function execute (statefulLayout, _args) {
|
|
32
|
+
return {
|
|
33
|
+
data: statefulLayout.data,
|
|
34
|
+
valid: statefulLayout.valid
|
|
35
|
+
}
|
|
36
|
+
}
|