@webmcp-auto-ui/sdk 2.5.37 → 2.5.39
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 +4 -1
- package/src/canvas-to-notebook.ts +220 -0
- package/src/index.ts +14 -1
- package/src/recipes/fence.ts +12 -0
- package/src/recipes/index.ts +2 -1
- package/src/recipes/parse.ts +6 -3
- package/src/stores/canvas.ts +1 -0
- package/src/unwrap.ts +11 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webmcp-auto-ui/sdk",
|
|
3
|
-
"version": "2.5.
|
|
3
|
+
"version": "2.5.39",
|
|
4
4
|
"description": "Skills CRUD, HyperSkill format, Svelte 5 stores",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"type": "module",
|
|
@@ -8,14 +8,17 @@
|
|
|
8
8
|
"main": "./src/index.ts",
|
|
9
9
|
"exports": {
|
|
10
10
|
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
11
12
|
"svelte": "./src/index.ts",
|
|
12
13
|
"import": "./src/index.ts"
|
|
13
14
|
},
|
|
14
15
|
"./canvas": {
|
|
16
|
+
"types": "./dist/canvas.d.ts",
|
|
15
17
|
"svelte": "./src/canvas.ts",
|
|
16
18
|
"import": "./src/canvas.ts"
|
|
17
19
|
},
|
|
18
20
|
"./canvas-vanilla": {
|
|
21
|
+
"types": "./dist/canvas-vanilla.d.ts",
|
|
19
22
|
"import": "./src/canvas-vanilla.ts"
|
|
20
23
|
}
|
|
21
24
|
},
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canvas → HyperSkill notebook serializer.
|
|
3
|
+
* Each block becomes one or more SQL/JS/MD cells. Dynamic when lineage is
|
|
4
|
+
* present, snapshot fallback otherwise.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { pickFence } from './recipes/fence.js';
|
|
8
|
+
|
|
9
|
+
export interface WidgetLineage {
|
|
10
|
+
widgetType: string;
|
|
11
|
+
widgetParams: Record<string, unknown>;
|
|
12
|
+
toolCalls: Array<{
|
|
13
|
+
serverName: string;
|
|
14
|
+
toolName: string;
|
|
15
|
+
args: Record<string, unknown>;
|
|
16
|
+
resultPreview?: string;
|
|
17
|
+
}>;
|
|
18
|
+
originRecipe?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface CanvasToNotebookOptions {
|
|
22
|
+
title?: string;
|
|
23
|
+
description?: string;
|
|
24
|
+
/** Active MCP servers (name, url) — copied as `servers:` in frontmatter. */
|
|
25
|
+
servers?: Array<{ name: string; url: string }>;
|
|
26
|
+
/** Active bundled WebMCP server ids (e.g. ['autoui', 'deckgl']). */
|
|
27
|
+
webmcpServers?: string[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface NotebookCell {
|
|
31
|
+
kind: 'sql' | 'js' | 'md';
|
|
32
|
+
content: string;
|
|
33
|
+
varname?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface CanvasBlock {
|
|
37
|
+
id: string;
|
|
38
|
+
type: string;
|
|
39
|
+
data: Record<string, unknown>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const SQL_TOOL_RE = /(^|[._-])query_sql$|^(query|run|execute)(_sql)?$/i;
|
|
43
|
+
const ARRAY_PARAM_KEYS = ['rows', 'data', 'items', 'cards'] as const;
|
|
44
|
+
|
|
45
|
+
/** Convert a single block + lineage into one or more notebook cells. */
|
|
46
|
+
export function lineageToCells(
|
|
47
|
+
block: CanvasBlock,
|
|
48
|
+
lineage: WidgetLineage | null,
|
|
49
|
+
): NotebookCell[] {
|
|
50
|
+
const cells: NotebookCell[] = [];
|
|
51
|
+
|
|
52
|
+
if (lineage?.originRecipe) {
|
|
53
|
+
cells.push({ kind: 'md', content: `> Generated from recipe: \`${lineage.originRecipe}\`` });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Snapshot fallback — no lineage.
|
|
57
|
+
if (!lineage || !lineage.toolCalls || lineage.toolCalls.length === 0) {
|
|
58
|
+
cells.push({
|
|
59
|
+
kind: 'js',
|
|
60
|
+
content: `await widget('${block.type}', ${JSON.stringify(block.data, null, 2)});`,
|
|
61
|
+
});
|
|
62
|
+
return cells;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Single SQL tool call → SQL cell + JS widget cell.
|
|
66
|
+
if (lineage.toolCalls.length === 1 && SQL_TOOL_RE.test(lineage.toolCalls[0]!.toolName)) {
|
|
67
|
+
const tc = lineage.toolCalls[0]!;
|
|
68
|
+
const sql = typeof tc.args['sql'] === 'string' ? (tc.args['sql'] as string) : '';
|
|
69
|
+
const varname = 'rows';
|
|
70
|
+
cells.push({ kind: 'sql', content: sql, varname });
|
|
71
|
+
|
|
72
|
+
const arrayKey = pickArrayParamKey(lineage.widgetParams);
|
|
73
|
+
let widgetCall: string;
|
|
74
|
+
if (arrayKey) {
|
|
75
|
+
const others = stripKeys(lineage.widgetParams, [arrayKey]);
|
|
76
|
+
const merged = { ...others, [arrayKey]: '__ROWS__' };
|
|
77
|
+
const json = JSON.stringify(merged, null, 2).replace('"__ROWS__"', varname);
|
|
78
|
+
widgetCall = `await widget('${block.type}', ${json});`;
|
|
79
|
+
} else {
|
|
80
|
+
widgetCall = `// TODO: map rows -> widget params\nawait widget('${block.type}', ${JSON.stringify(lineage.widgetParams, null, 2)});`;
|
|
81
|
+
}
|
|
82
|
+
cells.push({ kind: 'js', content: widgetCall });
|
|
83
|
+
return cells;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// General case — replay tool calls in order.
|
|
87
|
+
const lines: string[] = [];
|
|
88
|
+
lines.push('const unwrap = (r) => (r?.data ?? r?.results ?? r?.items ?? r ?? []);');
|
|
89
|
+
lineage.toolCalls.forEach((tc, i) => {
|
|
90
|
+
lines.push(
|
|
91
|
+
`const r${i} = await call('${tc.serverName}', '${tc.toolName}', ${JSON.stringify(tc.args, null, 2)});`,
|
|
92
|
+
);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const lastIdx = lineage.toolCalls.length - 1;
|
|
96
|
+
const arrayKey = pickArrayParamKey(lineage.widgetParams);
|
|
97
|
+
if (arrayKey) {
|
|
98
|
+
lines.push(`const ${arrayKey} = unwrap(r${lastIdx});`);
|
|
99
|
+
const others = stripKeys(lineage.widgetParams, [arrayKey]);
|
|
100
|
+
const merged = { ...others, [arrayKey]: '__ITEMS__' };
|
|
101
|
+
const json = JSON.stringify(merged, null, 2).replace('"__ITEMS__"', arrayKey);
|
|
102
|
+
lines.push(`await widget('${block.type}', ${json});`);
|
|
103
|
+
} else {
|
|
104
|
+
lines.push(`await widget('${block.type}', ${JSON.stringify(lineage.widgetParams, null, 2)});`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
cells.push({ kind: 'js', content: lines.join('\n') });
|
|
108
|
+
return cells;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Build the full notebook markdown from a snapshot of the canvas. */
|
|
112
|
+
export function canvasToNotebookMarkdown(
|
|
113
|
+
blocks: CanvasBlock[],
|
|
114
|
+
getLineage: (blockId: string) => WidgetLineage | null,
|
|
115
|
+
opts: CanvasToNotebookOptions = {},
|
|
116
|
+
): string {
|
|
117
|
+
const fm: string[] = ['---'];
|
|
118
|
+
fm.push(`title: ${yamlQuote(opts.title ?? 'Canvas snapshot')}`);
|
|
119
|
+
if (opts.description) fm.push(`description: ${yamlQuote(opts.description)}`);
|
|
120
|
+
if (opts.servers && opts.servers.length > 0) {
|
|
121
|
+
fm.push('servers:');
|
|
122
|
+
for (const s of opts.servers) {
|
|
123
|
+
fm.push(` - name: ${yamlQuote(s.name)}`);
|
|
124
|
+
fm.push(` url: ${yamlQuote(s.url)}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (opts.webmcpServers && opts.webmcpServers.length > 0) {
|
|
128
|
+
fm.push(`webmcp_servers: [${opts.webmcpServers.map((s) => yamlQuote(s)).join(', ')}]`);
|
|
129
|
+
}
|
|
130
|
+
fm.push('---', '');
|
|
131
|
+
|
|
132
|
+
const body: string[] = [];
|
|
133
|
+
for (const block of blocks) {
|
|
134
|
+
const cells = lineageToCells(block, getLineage(block.id));
|
|
135
|
+
for (const cell of cells) {
|
|
136
|
+
body.push(renderCell(cell), '');
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return fm.join('\n') + '\n' + body.join('\n').replace(/\n+$/, '') + '\n';
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export interface CanvasToNotebookData {
|
|
144
|
+
title?: string;
|
|
145
|
+
description?: string;
|
|
146
|
+
/** Active MCP servers (name, url) — to seed the notebook widget's data. */
|
|
147
|
+
servers?: Array<{ name: string; url: string }>;
|
|
148
|
+
/** Active bundled WebMCP server ids. */
|
|
149
|
+
webmcpServers?: string[];
|
|
150
|
+
/** Cells ready to feed into a NotebookState. */
|
|
151
|
+
cells: Array<{
|
|
152
|
+
id: string;
|
|
153
|
+
type: 'md' | 'sql' | 'js';
|
|
154
|
+
content: string;
|
|
155
|
+
varname?: string;
|
|
156
|
+
status?: 'idle';
|
|
157
|
+
}>;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Same logic as canvasToNotebookMarkdown but returns structured cells for direct widget mounting. */
|
|
161
|
+
export function canvasToNotebookCells(
|
|
162
|
+
blocks: CanvasBlock[],
|
|
163
|
+
getLineage: (blockId: string) => WidgetLineage | null,
|
|
164
|
+
opts: CanvasToNotebookOptions = {},
|
|
165
|
+
): CanvasToNotebookData {
|
|
166
|
+
let counter = 0;
|
|
167
|
+
const cells: CanvasToNotebookData['cells'] = [];
|
|
168
|
+
|
|
169
|
+
for (const block of blocks) {
|
|
170
|
+
const raw = lineageToCells(block, getLineage(block.id));
|
|
171
|
+
for (const cell of raw) {
|
|
172
|
+
const entry: CanvasToNotebookData['cells'][number] = {
|
|
173
|
+
id: `c${counter++}`,
|
|
174
|
+
type: cell.kind,
|
|
175
|
+
content: cell.content,
|
|
176
|
+
status: 'idle',
|
|
177
|
+
};
|
|
178
|
+
if (cell.varname !== undefined) entry.varname = cell.varname;
|
|
179
|
+
cells.push(entry);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const out: CanvasToNotebookData = { cells };
|
|
184
|
+
if (opts.title) out.title = opts.title;
|
|
185
|
+
if (opts.description) out.description = opts.description;
|
|
186
|
+
if (opts.servers && opts.servers.length > 0) out.servers = opts.servers;
|
|
187
|
+
if (opts.webmcpServers && opts.webmcpServers.length > 0) out.webmcpServers = opts.webmcpServers;
|
|
188
|
+
return out;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function renderCell(cell: NotebookCell): string {
|
|
192
|
+
if (cell.kind === 'md') return cell.content;
|
|
193
|
+
if (cell.kind === 'sql') {
|
|
194
|
+
const meta = cell.varname ? `-- @meta {"varname": "${cell.varname}"}\n` : '';
|
|
195
|
+
const body = meta + cell.content;
|
|
196
|
+
const fence = pickFence(body);
|
|
197
|
+
return fence + 'sql\n' + body + '\n' + fence;
|
|
198
|
+
}
|
|
199
|
+
const fence = pickFence(cell.content);
|
|
200
|
+
return fence + 'js\n' + cell.content + '\n' + fence;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function pickArrayParamKey(params: Record<string, unknown>): string | null {
|
|
204
|
+
for (const k of ARRAY_PARAM_KEYS) {
|
|
205
|
+
if (k in params && Array.isArray(params[k])) return k;
|
|
206
|
+
}
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function stripKeys(obj: Record<string, unknown>, keys: string[]): Record<string, unknown> {
|
|
211
|
+
const out: Record<string, unknown> = {};
|
|
212
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
213
|
+
if (!keys.includes(k)) out[k] = v;
|
|
214
|
+
}
|
|
215
|
+
return out;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function yamlQuote(s: string): string {
|
|
219
|
+
return '"' + s.replace(/\\/g, '\\\\').replace(/"/g, '\\"') + '"';
|
|
220
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -91,7 +91,7 @@ export type { RemoteMcpRegistryEntry } from './remote-mcp-registry.js';
|
|
|
91
91
|
// import { canvas } from '@webmcp-auto-ui/sdk/canvas'
|
|
92
92
|
|
|
93
93
|
// Recipe runner — markdown-fence parser + JS/TS/SQL/etc executor over MCP
|
|
94
|
-
export { parseBody, runCode, estimateTokens, safeStringify, findCodeParamName, buildToolArgs } from './recipes/index.js';
|
|
94
|
+
export { parseBody, pickFence, runCode, estimateTokens, safeStringify, findCodeParamName, buildToolArgs, parseWidgetDisplayCall } from './recipes/index.js';
|
|
95
95
|
export type { ParsedSegment, RunResult, RunLog, RunTab, RecipeData } from './recipes/index.js';
|
|
96
96
|
|
|
97
97
|
// Short URL — domain-dependent compact token
|
|
@@ -99,3 +99,16 @@ export { buildShortUrl, getShortToken } from './short-url.js';
|
|
|
99
99
|
|
|
100
100
|
// Widget sample-data extractor — parses the `## Example` block of a recipe
|
|
101
101
|
export { extractSampleData } from './widget-sample-data.js';
|
|
102
|
+
|
|
103
|
+
// MCP response normalizer — flattens common envelope shapes to array
|
|
104
|
+
export { unwrap } from './unwrap.js';
|
|
105
|
+
|
|
106
|
+
// Canvas → HyperSkill notebook serializer
|
|
107
|
+
export { lineageToCells, canvasToNotebookMarkdown, canvasToNotebookCells } from './canvas-to-notebook.js';
|
|
108
|
+
export type {
|
|
109
|
+
WidgetLineage,
|
|
110
|
+
CanvasToNotebookOptions,
|
|
111
|
+
CanvasToNotebookData,
|
|
112
|
+
NotebookCell,
|
|
113
|
+
CanvasBlock,
|
|
114
|
+
} from './canvas-to-notebook.js';
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pick a backtick fence longer than any run of backticks inside `content`.
|
|
3
|
+
* CommonMark rule: a fenced code block opens with N≥3 backticks; the content
|
|
4
|
+
* must not contain a closing fence of the same length, and the closing fence
|
|
5
|
+
* must be at least as long as the opening. Choosing N = max(3, longestRun+1)
|
|
6
|
+
* makes round-trips safe even when the cell content embeds Markdown fences.
|
|
7
|
+
*/
|
|
8
|
+
export function pickFence(content: string): string {
|
|
9
|
+
const runs = content.match(/`+/g) ?? [];
|
|
10
|
+
const longest = runs.reduce((m, r) => Math.max(m, r.length), 0);
|
|
11
|
+
return '`'.repeat(Math.max(3, longest + 1));
|
|
12
|
+
}
|
package/src/recipes/index.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
export { parseBody } from './parse.js';
|
|
2
|
-
export {
|
|
2
|
+
export { pickFence } from './fence.js';
|
|
3
|
+
export { runCode, estimateTokens, safeStringify, findCodeParamName, buildToolArgs, parseWidgetDisplayCall } from './runner.js';
|
|
3
4
|
export type { ParsedSegment, RunResult, RunLog, RunTab, RecipeData } from './types.js';
|
package/src/recipes/parse.ts
CHANGED
|
@@ -16,14 +16,17 @@ export function parseBody(body: string): ParsedSegment[] {
|
|
|
16
16
|
|
|
17
17
|
const segments: ParsedSegment[] = [];
|
|
18
18
|
// Match fenced code blocks with optional language tag.
|
|
19
|
-
//
|
|
20
|
-
|
|
19
|
+
// Variable-length fence (≥3 backticks, CommonMark §4.5): the closing fence
|
|
20
|
+
// must use the same number of backticks as the opening — captured via \1.
|
|
21
|
+
// This allows cells whose content embeds ``` (e.g. widget('text', { content: "...```js..." }))
|
|
22
|
+
// to be encoded with a longer fence (4+ backticks) without premature closure.
|
|
23
|
+
const re = /(`{3,})([a-zA-Z0-9_+-]*)\r?\n([\s\S]*?)\r?\n\1[ \t]*(?=\r?\n|$)/g;
|
|
21
24
|
|
|
22
25
|
let lastIndex = 0;
|
|
23
26
|
let match: RegExpExecArray | null;
|
|
24
27
|
|
|
25
28
|
while ((match = re.exec(body)) !== null) {
|
|
26
|
-
const [full, langRaw, codeRaw] = match;
|
|
29
|
+
const [full, , langRaw, codeRaw] = match;
|
|
27
30
|
const start = match.index;
|
|
28
31
|
|
|
29
32
|
if (start > lastIndex) {
|
package/src/stores/canvas.ts
CHANGED
|
@@ -290,6 +290,7 @@ function createCanvasVanilla() {
|
|
|
290
290
|
|
|
291
291
|
if (srv.enabled && !srv.connected && !liveUrls.has(srv.url)) {
|
|
292
292
|
inFlight.add(name);
|
|
293
|
+
setDataServerMeta(name, { connecting: true, error: undefined });
|
|
293
294
|
try {
|
|
294
295
|
const opts = srv.headers ? { headers: srv.headers } : undefined;
|
|
295
296
|
const { name: actualName, tools } = await multiClient.addServer(srv.url, opts);
|
package/src/unwrap.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalize MCP tool responses to an array. Handles common envelopes.
|
|
3
|
+
* Tries: direct array → .data → .results → .rows → .entries → .items → .feed.entry → .properties.parameter → []
|
|
4
|
+
*/
|
|
5
|
+
export function unwrap(r: unknown): any[] {
|
|
6
|
+
if (Array.isArray(r)) return r;
|
|
7
|
+
if (r == null || typeof r !== 'object') return [];
|
|
8
|
+
const o = r as any;
|
|
9
|
+
return o.data ?? o.results ?? o.rows ?? o.entries ?? o.items
|
|
10
|
+
?? o.feed?.entry ?? o.properties?.parameter ?? [];
|
|
11
|
+
}
|