@webmcp-auto-ui/sdk 0.4.0 → 2.5.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/README.md CHANGED
@@ -54,12 +54,55 @@ Svelte 5 runes state for the composer canvas. Import from the `/canvas` subpath
54
54
  ```ts
55
55
  import { canvas } from '@webmcp-auto-ui/sdk/canvas';
56
56
 
57
- canvas.addBlock('stat', { label: 'Revenue', value: '€142K' });
57
+ canvas.addWidget('stat', { label: 'Revenue', value: '€142K' });
58
+ // → returns widget with id prefixed 'w_'
58
59
  canvas.setMcpConnected(true, 'my-server', tools);
59
60
  const param = canvas.buildHyperskillParam(); // base64 for ?hs=
60
61
  ```
61
62
 
62
- The canvas store manages blocks, mode (`auto` | `drag` | `chat`), MCP connection state, chat messages, and generating flag.
63
+ The canvas store manages widgets, mode (`auto` | `drag` | `chat`), MCP connection state, chat messages, and generating flag.
64
+
65
+ ### Types
66
+
67
+ - **`Widget`** — a rendered UI element with `id` (prefixed `w_`), `type`, and `data`
68
+ - **`WidgetType`** — union of all supported widget type strings
69
+
70
+ > **Migration**: `Block`, `BlockType`, and `addBlock()` are still exported as deprecated aliases. New code should use `Widget`, `WidgetType`, and `addWidget()`. Widget IDs use the `w_` prefix (previously `b_`).
71
+
72
+ ## MCP Demo Servers
73
+
74
+ A built-in registry of 7 demo MCP server endpoints for testing and showcasing:
75
+
76
+ ```ts
77
+ import { MCP_DEMO_SERVERS } from '@webmcp-auto-ui/sdk';
78
+
79
+ // MCP_DEMO_SERVERS: Array<{ url: string; name: string; description: string }>
80
+ // Includes: tricoteuses, weather, finance, etc.
81
+ ```
82
+
83
+ Used by the `RemoteMCPserversDemo` component in `@webmcp-auto-ui/ui` to let users discover and connect to available servers.
84
+
85
+ ## HyperSkillMeta extensions
86
+
87
+ Two new fields in `HyperSkillMeta`:
88
+
89
+ - **`chatSummary`** — anonymized summary of the chat that produced the skill (generated via `summarizeChat()` from `@webmcp-auto-ui/agent`)
90
+ - **`provenance`** — records the LLM model, MCP server, and timestamp that created the skill
91
+
92
+ These fields enable traceability without exposing raw chat history.
93
+
94
+ ## Vanilla canvas store
95
+
96
+ A framework-agnostic canvas store for non-Svelte environments:
97
+
98
+ ```ts
99
+ import { canvas } from '@webmcp-auto-ui/sdk/canvas-vanilla';
100
+
101
+ canvas.addBlock('stat', { label: 'Revenue', value: '$142K' });
102
+ canvas.subscribe(() => console.log('state changed'));
103
+ ```
104
+
105
+ Same API as the Svelte 5 store but uses plain callbacks instead of runes. Useful for vanilla JS, React, or Vue integrations.
63
106
 
64
107
  ## Install
65
108
 
@@ -67,7 +110,7 @@ The canvas store manages blocks, mode (`auto` | `drag` | `chat`), MCP connection
67
110
  npm install @webmcp-auto-ui/sdk
68
111
  ```
69
112
 
70
- Requires Svelte 5 for the canvas store. The HyperSkill utilities and registry are plain TypeScript with no framework dependency.
113
+ The Svelte 5 canvas store requires Svelte 5. The vanilla canvas store, HyperSkill utilities, skills registry, and MCP demo servers are plain TypeScript with no framework dependency.
71
114
 
72
115
  ## License
73
116
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webmcp-auto-ui/sdk",
3
- "version": "0.4.0",
3
+ "version": "2.5.0",
4
4
  "description": "Skills CRUD, HyperSkill format, Svelte 5 stores",
5
5
  "license": "AGPL-3.0-or-later",
6
6
  "type": "module",
@@ -27,7 +27,7 @@
27
27
  "build": "svelte-package -i src"
28
28
  },
29
29
  "dependencies": {
30
- "hyperskills": "^0.1.0"
30
+ "hyperskills": "^0.1.4"
31
31
  },
32
32
  "peerDependencies": {
33
33
  "svelte": "^5.0.0"
@@ -1,3 +1,3 @@
1
1
  // Canvas store — Vanilla (framework-agnostic), no Svelte dependency
2
2
  export { canvasVanilla } from './stores/canvas.js';
3
- export type { Block, BlockType, Mode, LLMId, ChatMsg, McpToolInfo, CanvasSnapshot } from './stores/canvas.js';
3
+ export type { Widget, WidgetType, Block, BlockType, Mode, LLMId, ChatMsg, McpToolInfo, CanvasSnapshot } from './stores/canvas.js';
package/src/canvas.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  // Canvas store — Svelte 5 runes (browser-only, must be imported in Svelte components)
2
2
  export { canvas } from './stores/canvas.svelte.js';
3
- export type { Block, BlockType, Mode, LLMId, ChatMsg, McpToolInfo } from './stores/canvas.svelte.js';
3
+ export type { Widget, WidgetType, Block, BlockType, Mode, LLMId, ChatMsg, McpToolInfo } from './stores/canvas.svelte.js';
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Typed wrapper around the pure-JS `hyperskills` package.
3
+ * This avoids "no declaration file" errors in strict TS
4
+ * without requiring hyperskills to ship its own types.
5
+ */
6
+
7
+ // @ts-ignore — hyperskills is intentionally pure JS
8
+ import * as hs from 'hyperskills';
9
+
10
+ export const encode: (
11
+ sourceUrl: string,
12
+ content: string,
13
+ options?: { compress?: 'gz' | 'br' },
14
+ ) => Promise<string> = hs.encode;
15
+
16
+ export const decode: (
17
+ urlOrParam: string,
18
+ ) => Promise<{ sourceUrl: string; content: string }> = hs.decode;
19
+
20
+ export const hash: (
21
+ sourceUrl: string,
22
+ content: string,
23
+ previousHash?: string,
24
+ ) => Promise<string> = hs.hash;
25
+
26
+ export const diff: (prev: unknown, next: unknown) => unknown = hs.diff;
27
+
28
+ export const getHsParam: () => string | null = hs.getHsParam;
29
+
30
+ export const createVersion: (
31
+ sourceUrl: string,
32
+ content: string,
33
+ previousHash?: string,
34
+ ) => Promise<{ hash: string; content: string }> = hs.createVersion;
35
+
36
+ export const sign: (hashHex: string, privateKey: CryptoKey) => Promise<string> = hs.sign;
37
+
38
+ export const verify: (
39
+ hashHex: string,
40
+ signatureB64: string,
41
+ publicKey: CryptoKey,
42
+ ) => Promise<boolean> = hs.verify;
43
+
44
+ export const generateKeyPair: () => Promise<{
45
+ publicKey: CryptoKey;
46
+ privateKey: CryptoKey;
47
+ }> = hs.generateKeyPair;
package/src/index.ts CHANGED
@@ -36,10 +36,10 @@ export interface HyperSkillVersion {
36
36
 
37
37
  // HyperSkill encoding — powered by the `hyperskills` NPM package.
38
38
  // Raw functions re-exported for direct access:
39
- export { encode, decode, hash, diff, getHsParam } from 'hyperskills';
39
+ export { encode, decode, hash, diff, getHsParam } from './hyperskills.js';
40
40
 
41
41
  // Typed convenience wrappers — prefer these in apps:
42
- import { encode, decode, hash } from 'hyperskills';
42
+ import { encode, decode, hash, diff } from './hyperskills.js';
43
43
 
44
44
  export async function encodeHyperSkill(skill: HyperSkill, sourceUrl?: string): Promise<string> {
45
45
  const base = sourceUrl ?? (typeof window !== 'undefined' ? window.location.href.split('?')[0] : 'https://example.com');
@@ -11,13 +11,13 @@ export interface McpDemoServer {
11
11
 
12
12
  /**
13
13
  * MCP demo servers available for webmcp-auto-ui demos.
14
- * code4code has its own domain; others follow the pattern
14
+ * tricoteuses has its own domain; others follow the pattern
15
15
  * https://demos.hyperskills.net/<id>/mcp
16
16
  */
17
17
  export const MCP_DEMO_SERVERS: McpDemoServer[] = [
18
18
  {
19
- id: 'code4code',
20
- name: 'Code4Code',
19
+ id: 'tricoteuses',
20
+ name: 'Tricoteuses',
21
21
  description: 'Base de données parlementaire française — amendements, scrutins, députés, groupes politiques.',
22
22
  url: 'https://mcp.code4code.eu/mcp',
23
23
  tags: ['politique', 'france', 'parlement', 'open-data'],
@@ -64,4 +64,10 @@ export const MCP_DEMO_SERVERS: McpDemoServer[] = [
64
64
  url: 'https://demos.hyperskills.net/mcp-datagouv/mcp',
65
65
  tags: ['open-data', 'france', 'gouvernement', 'statistiques'],
66
66
  },
67
+ {
68
+ id: 'nasa',
69
+ name: 'NASA',
70
+ description: 'NASA — images spatiales, données astronomiques, rovers Mars, astéroïdes.',
71
+ url: 'https://demos.hyperskills.net/mcp-nasa/mcp',
72
+ },
67
73
  ];
@@ -22,6 +22,12 @@ export interface Skill {
22
22
  tags?: string[];
23
23
  theme?: ThemeOverrides;
24
24
  blocks: SkillBlock[];
25
+ /** Target Svelte component (from former Core SkillDef) */
26
+ component?: string;
27
+ /** Presentation hints for the component */
28
+ presentation?: string;
29
+ /** Expected block types this skill can produce */
30
+ expectedBlockTypes?: string[];
25
31
  createdAt: number;
26
32
  updatedAt: number;
27
33
  }
@@ -94,7 +100,7 @@ const DEMO_SKILLS: Omit<Skill, 'id' | 'createdAt' | 'updatedAt'>[] = [
94
100
  {
95
101
  name: 'weather-dashboard',
96
102
  mcp: 'https://mcp.code4code.eu/mcp',
97
- mcpName: 'code4code',
103
+ mcpName: 'tricoteuses',
98
104
  description: 'Météo locale avec température, conditions et prévisions',
99
105
  tags: ['météo', 'dashboard'],
100
106
  blocks: [
@@ -106,7 +112,7 @@ const DEMO_SKILLS: Omit<Skill, 'id' | 'createdAt' | 'updatedAt'>[] = [
106
112
  {
107
113
  name: 'kpi-overview',
108
114
  mcp: 'https://mcp.code4code.eu/mcp',
109
- mcpName: 'code4code',
115
+ mcpName: 'tricoteuses',
110
116
  description: 'Vue KPIs : revenus, utilisateurs, churn',
111
117
  tags: ['kpi', 'dashboard'],
112
118
  blocks: [
@@ -119,7 +125,7 @@ const DEMO_SKILLS: Omit<Skill, 'id' | 'createdAt' | 'updatedAt'>[] = [
119
125
  {
120
126
  name: 'status-monitor',
121
127
  mcp: 'https://mcp.code4code.eu/mcp',
122
- mcpName: 'code4code',
128
+ mcpName: 'tricoteuses',
123
129
  description: 'Monitoring état des services',
124
130
  tags: ['ops', 'monitoring'],
125
131
  blocks: [
@@ -1,282 +1,116 @@
1
1
  /**
2
- * Canvas state store — Svelte 5 runes
3
- * Manages blocks on the canvas, mode, MCP connection, chat history
2
+ * Canvas state store — Svelte 5 runes wrapper
3
+ * Thin reactive wrapper around the framework-agnostic canvas store (canvas.ts).
4
+ *
5
+ * All state mutations go through the vanilla store; this file only adds
6
+ * Svelte 5 $state/$derived reactivity via subscribe().
4
7
  */
5
8
 
6
- import { decode } from 'hyperskills';
9
+ import { canvasVanilla } from './canvas.js';
10
+ import type { Widget, WidgetType, Mode, LLMId, ChatMsg, McpToolInfo } from './canvas.js';
7
11
 
8
- export type BlockType =
9
- | 'stat' | 'kv' | 'list' | 'chart' | 'alert' | 'code' | 'text' | 'actions' | 'tags'
10
- | 'stat-card' | 'data-table' | 'timeline' | 'profile' | 'trombinoscope' | 'json-viewer'
11
- | 'hemicycle' | 'chart-rich' | 'cards' | 'grid-data' | 'sankey' | 'map' | 'log'
12
- | 'gallery' | 'carousel' | 'd3';
13
-
14
- export type Mode = 'auto' | 'drag' | 'chat';
15
- export type LLMId = 'haiku' | 'sonnet' | 'gemma-e2b' | 'gemma-e4b';
16
-
17
- export interface Block {
18
- id: string;
19
- type: BlockType;
20
- data: Record<string, unknown>;
21
- }
22
-
23
- export interface ChatMsg {
24
- id: string;
25
- role: 'user' | 'assistant' | 'system';
26
- content: string;
27
- thinking?: boolean;
28
- }
29
-
30
- export interface McpToolInfo {
31
- name: string;
32
- description: string;
33
- inputSchema?: Record<string, unknown>;
34
- }
35
-
36
- function uuid() {
37
- return 'b_' + Date.now().toString(36) + Math.random().toString(36).slice(2, 5);
38
- }
39
-
40
- function msgId() {
41
- return 'm_' + Date.now().toString(36) + Math.random().toString(36).slice(2, 5);
42
- }
12
+ // Re-export types (including deprecated aliases)
13
+ export type { Widget, WidgetType, Mode, LLMId, ChatMsg, McpToolInfo };
14
+ export type { Block, BlockType, CanvasSnapshot } from './canvas.js';
43
15
 
44
16
  function createCanvas() {
45
- // ── State ────────────────────────────────────────────────────────────────
46
- let blocks = $state<Block[]>([]);
47
- let mode = $state<Mode>('drag');
48
- let llm = $state<LLMId>('haiku');
49
- let mcpUrl = $state('');
50
- let mcpConnected = $state(false);
51
- let mcpConnecting = $state(false);
52
- let mcpName = $state('');
53
- let mcpTools = $state<McpToolInfo[]>([]);
54
- let messages = $state<ChatMsg[]>([]);
55
- let generating = $state(false);
56
- let statusText = $state('● aucun MCP connecté');
57
- let statusColor = $state('text-zinc-600');
58
-
59
- // ── Derived ──────────────────────────────────────────────────────────────
17
+ // ── Reactive mirror of vanilla state ────────────────────────────────────
18
+ let blocks = $state<Widget[]>(canvasVanilla.blocks);
19
+ let mode = $state<Mode>(canvasVanilla.mode);
20
+ let llm = $state<LLMId>(canvasVanilla.llm);
21
+ let mcpUrl = $state(canvasVanilla.mcpUrl);
22
+ let mcpConnected = $state(canvasVanilla.mcpConnected);
23
+ let mcpConnecting = $state(canvasVanilla.mcpConnecting);
24
+ let mcpName = $state(canvasVanilla.mcpName);
25
+ let mcpTools = $state<McpToolInfo[]>(canvasVanilla.mcpTools);
26
+ let messages = $state<ChatMsg[]>(canvasVanilla.messages);
27
+ let generating = $state(canvasVanilla.generating);
28
+ let statusText = $state(canvasVanilla.statusText);
29
+ let statusColor = $state(canvasVanilla.statusColor);
30
+ let themeOverrides = $state<Record<string, string>>(canvasVanilla.themeOverrides);
31
+
32
+ // ── Derived ─────────────────────────────────────────────────────────────
60
33
  const blockCount = $derived(blocks.length);
61
34
  const isEmpty = $derived(blocks.length === 0);
62
35
 
63
- // ── Block actions ────────────────────────────────────────────────────────
64
- function addBlock(type: BlockType, data: Record<string, unknown> = {}): Block {
65
- const block: Block = { id: uuid(), type, data };
66
- blocks = [...blocks, block];
67
- return block;
68
- }
69
-
70
- function removeBlock(id: string) {
71
- blocks = blocks.filter((b) => b.id !== id);
72
- }
73
-
74
- function updateBlock(id: string, data: Partial<Record<string, unknown>>) {
75
- blocks = blocks.map((b) => b.id === id ? { ...b, data: { ...b.data, ...data } } : b);
76
- }
77
-
78
- function moveBlock(fromId: string, toId: string) {
79
- const fi = blocks.findIndex((b) => b.id === fromId);
80
- const ti = blocks.findIndex((b) => b.id === toId);
81
- if (fi < 0 || ti < 0 || fi === ti) return;
82
- const next = [...blocks];
83
- const [moved] = next.splice(fi, 1);
84
- next.splice(ti, 0, moved);
85
- blocks = next;
86
- }
87
-
88
- function clearBlocks() {
89
- blocks = [];
90
- }
91
-
92
- function setBlocks(newBlocks: Block[]) {
93
- blocks = newBlocks;
94
- }
95
-
96
- // ── Chat ─────────────────────────────────────────────────────────────────
97
- function addMsg(role: ChatMsg['role'], content: string, thinking = false): ChatMsg {
98
- const msg: ChatMsg = { id: msgId(), role, content, thinking };
99
- messages = [...messages, msg];
100
- return msg;
101
- }
102
-
103
- function updateMsg(id: string, content: string, thinking = false) {
104
- messages = messages.map((m) => m.id === id ? { ...m, content, thinking } : m);
105
- }
106
-
107
- function clearMessages() {
108
- messages = [];
109
- }
110
-
111
- // ── MCP ──────────────────────────────────────────────────────────────────
112
- function setMcpConnecting(connecting: boolean) {
113
- mcpConnecting = connecting;
114
- if (connecting) {
115
- statusText = '● connexion…';
116
- statusColor = 'text-amber-400';
117
- }
118
- }
119
-
120
- function setMcpConnected(
121
- connected: boolean,
122
- name?: string,
123
- tools?: McpToolInfo[]
124
- ) {
125
- mcpConnected = connected;
126
- if (name) mcpName = name;
127
- if (tools) mcpTools = tools;
128
- if (connected) {
129
- statusText = `● ${name} · ${tools?.length ?? 0} tools`;
130
- statusColor = 'text-teal-400';
131
- } else {
132
- statusText = '● aucun MCP connecté';
133
- statusColor = 'text-zinc-600';
134
- }
135
- }
136
-
137
- function setMcpError(err: string) {
138
- mcpConnected = false;
139
- mcpConnecting = false;
140
- statusText = `● erreur: ${err}`;
141
- statusColor = 'text-red-400';
142
- }
143
-
144
- // ── Theme ────────────────────────────────────────────────────────────────
145
- let themeOverrides = $state<Record<string, string>>({});
146
-
147
- function setThemeOverrides(overrides: Record<string, string>) {
148
- themeOverrides = overrides;
149
- }
150
-
151
- // ── HyperSkill ───────────────────────────────────────────────────────────
152
- function buildSkillJSON() {
153
- const skill: Record<string, unknown> = {
154
- version: '1.0',
155
- name: 'skill-' + Date.now(),
156
- created: new Date().toISOString(),
157
- mcp: mcpUrl,
158
- llm,
159
- blocks: blocks.map((b) => ({ type: b.type, data: b.data })),
160
- };
161
- if (Object.keys(themeOverrides).length > 0) skill.theme = themeOverrides;
162
- return skill;
163
- }
164
-
165
- async function buildHyperskillParam(): Promise<string> {
166
- const json = JSON.stringify(buildSkillJSON());
167
- const bytes = new TextEncoder().encode(json);
168
- // Auto-compress with gzip when payload exceeds 6 KB to keep URLs under nginx limits
169
- if (bytes.length > 6144) {
170
- const cs = new CompressionStream('gzip');
171
- const writer = cs.writable.getWriter();
172
- writer.write(bytes);
173
- writer.close();
174
- const compressed = new Uint8Array(await new Response(cs.readable).arrayBuffer());
175
- const b64 = btoa(String.fromCharCode(...compressed))
176
- .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
177
- return 'gz.' + b64;
178
- }
179
- // Small payloads: plain base64url
180
- return btoa(unescape(encodeURIComponent(json)))
181
- .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
182
- }
183
-
184
- async function loadFromParam(param: string): Promise<boolean> {
185
- try {
186
- let json: string;
187
-
188
- if (param.startsWith('gz.')) {
189
- // Compressed: gz.<base64url-encoded gzip data>
190
- let b64 = param.slice(3).replace(/-/g, '+').replace(/_/g, '/');
191
- while (b64.length % 4) b64 += '=';
192
- const compressed = Uint8Array.from(atob(b64), c => c.charCodeAt(0));
193
- const ds = new DecompressionStream('gzip');
194
- const writer = ds.writable.getWriter();
195
- writer.write(compressed);
196
- writer.close();
197
- json = await new Response(ds.readable).text();
198
- } else {
199
- // Plain base64url
200
- let b64 = param.replace(/ /g, '+').replace(/-/g, '+').replace(/_/g, '/');
201
- while (b64.length % 4) b64 += '=';
202
- json = decodeURIComponent(escape(atob(b64)));
203
- }
204
-
205
- const skill = JSON.parse(json) as {
206
- mcp?: string; llm?: LLMId;
207
- theme?: Record<string, string>;
208
- blocks?: { type: BlockType; data: Record<string, unknown> }[];
209
- };
210
- if (skill.mcp) mcpUrl = skill.mcp;
211
- if (skill.llm) llm = skill.llm;
212
- if (skill.theme) themeOverrides = skill.theme;
213
- if (skill.blocks) {
214
- blocks = skill.blocks.map((b) => ({ id: uuid(), type: b.type, data: b.data }));
215
- }
216
- return true;
217
- } catch {
218
- return false;
219
- }
220
- }
221
-
222
- // ── loadFromUrl ──────────────────────────────────────────────────────────
223
- async function loadFromUrl(url: string): Promise<boolean> {
224
- try {
225
- const { content: raw } = await decode(url);
226
- const decoded = JSON.parse(raw) as { meta?: Record<string, unknown>; content?: { blocks?: { type: BlockType; data: Record<string, unknown> }[] } };
227
- if (decoded.meta?.mcp) mcpUrl = decoded.meta.mcp as string;
228
- if (decoded.meta?.llm) llm = decoded.meta.llm as LLMId;
229
- if (decoded.meta?.theme) themeOverrides = decoded.meta.theme as Record<string, string>;
230
- if (decoded.content?.blocks) blocks = decoded.content.blocks.map((b) => ({ id: uuid(), type: b.type, data: b.data }));
231
- return true;
232
- } catch {
233
- return false;
234
- }
235
- }
236
-
237
- // ── Return public API ────────────────────────────────────────────────────
36
+ // ── Sync from vanilla store on every change ─────────────────────────────
37
+ canvasVanilla.subscribe(() => {
38
+ const s = canvasVanilla.getSnapshot();
39
+ blocks = s.blocks;
40
+ mode = s.mode;
41
+ llm = s.llm;
42
+ mcpUrl = s.mcpUrl;
43
+ mcpConnected = s.mcpConnected;
44
+ mcpConnecting = s.mcpConnecting;
45
+ mcpName = s.mcpName;
46
+ mcpTools = s.mcpTools;
47
+ messages = s.messages;
48
+ generating = s.generating;
49
+ statusText = s.statusText;
50
+ statusColor = s.statusColor;
51
+ themeOverrides = s.themeOverrides;
52
+ });
53
+
54
+ // ── Return public API ───────────────────────────────────────────────────
238
55
  return {
239
56
  // State getters + setters (reactive — supports bind:)
240
57
  get blocks() { return blocks; },
241
58
  get mode() { return mode; },
242
- set mode(v: Mode) { mode = v; },
59
+ set mode(v: Mode) { canvasVanilla.mode = v; },
243
60
  get llm() { return llm; },
244
- set llm(v: LLMId) { llm = v; },
61
+ set llm(v: LLMId) { canvasVanilla.llm = v; },
245
62
  get mcpUrl() { return mcpUrl; },
246
- set mcpUrl(v: string) { mcpUrl = v; },
63
+ set mcpUrl(v: string) { canvasVanilla.mcpUrl = v; },
247
64
  get mcpConnected() { return mcpConnected; },
248
65
  get mcpConnecting() { return mcpConnecting; },
249
66
  get mcpName() { return mcpName; },
250
67
  get mcpTools() { return mcpTools; },
251
68
  get messages() { return messages; },
252
69
  get generating() { return generating; },
253
- set generating(v: boolean) { generating = v; },
70
+ set generating(v: boolean) { canvasVanilla.generating = v; },
254
71
  get statusText() { return statusText; },
255
72
  get statusColor() { return statusColor; },
256
73
  get blockCount() { return blockCount; },
257
74
  get isEmpty() { return isEmpty; },
258
75
 
259
76
  // Setters (kept for backward compat)
260
- setMode(m: Mode) { mode = m; },
261
- setLlm(l: LLMId) { llm = l; },
262
- setMcpUrl(u: string) { mcpUrl = u; },
263
- setGenerating(g: boolean) { generating = g; },
77
+ setMode(m: Mode) { canvasVanilla.setMode(m); },
78
+ setLlm(l: LLMId) { canvasVanilla.setLlm(l); },
79
+ setMcpUrl(u: string) { canvasVanilla.setMcpUrl(u); },
80
+ setGenerating(g: boolean) { canvasVanilla.setGenerating(g); },
81
+
82
+ // Widget actions (primary name)
83
+ addWidget: canvasVanilla.addWidget.bind(canvasVanilla),
84
+
85
+ // Backward compat alias
86
+ addBlock: canvasVanilla.addBlock.bind(canvasVanilla),
264
87
 
265
- // Block actions
266
- addBlock, removeBlock, updateBlock, moveBlock, clearBlocks, setBlocks,
88
+ // Block actions (kept as-is)
89
+ removeBlock: canvasVanilla.removeBlock.bind(canvasVanilla),
90
+ updateBlock: canvasVanilla.updateBlock.bind(canvasVanilla),
91
+ moveBlock: canvasVanilla.moveBlock.bind(canvasVanilla),
92
+ clearBlocks: canvasVanilla.clearBlocks.bind(canvasVanilla),
93
+ setBlocks: canvasVanilla.setBlocks.bind(canvasVanilla),
267
94
 
268
95
  // Chat
269
- addMsg, updateMsg, clearMessages,
96
+ addMsg: canvasVanilla.addMsg.bind(canvasVanilla),
97
+ updateMsg: canvasVanilla.updateMsg.bind(canvasVanilla),
98
+ clearMessages: canvasVanilla.clearMessages.bind(canvasVanilla),
270
99
 
271
100
  // MCP
272
- setMcpConnecting, setMcpConnected, setMcpError,
101
+ setMcpConnecting: canvasVanilla.setMcpConnecting.bind(canvasVanilla),
102
+ setMcpConnected: canvasVanilla.setMcpConnected.bind(canvasVanilla),
103
+ setMcpError: canvasVanilla.setMcpError.bind(canvasVanilla),
273
104
 
274
105
  // Theme
275
106
  get themeOverrides() { return themeOverrides; },
276
- setThemeOverrides,
107
+ setThemeOverrides: canvasVanilla.setThemeOverrides.bind(canvasVanilla),
277
108
 
278
109
  // HyperSkill
279
- buildSkillJSON, buildHyperskillParam, loadFromParam, loadFromUrl,
110
+ buildSkillJSON: canvasVanilla.buildSkillJSON.bind(canvasVanilla),
111
+ buildHyperskillParam: canvasVanilla.buildHyperskillParam.bind(canvasVanilla),
112
+ loadFromParam: canvasVanilla.loadFromParam.bind(canvasVanilla),
113
+ loadFromUrl: canvasVanilla.loadFromUrl.bind(canvasVanilla),
280
114
  };
281
115
  }
282
116
 
@@ -1,29 +1,38 @@
1
1
  /**
2
2
  * Canvas state store — Vanilla (framework-agnostic)
3
- * Manages blocks on the canvas, mode, MCP connection, chat history
3
+ * Manages widgets on the canvas, mode, MCP connection, chat history
4
4
  *
5
- * This is the framework-agnostic version of the canvas store.
6
- * For Svelte 5 reactivity, use @webmcp-auto-ui/sdk/canvas (Svelte runes)
7
- * or @webmcp-auto-ui/ui/canvas (adapter).
5
+ * This is the canonical, framework-agnostic version of the canvas store.
6
+ * For Svelte 5 reactivity, use the Svelte wrapper (canvas.svelte.ts)
7
+ * which re-exports this store with $state/$derived reactivity.
8
+ *
9
+ * Reactivity: subscribe(fn) / getSnapshot() pattern (useSyncExternalStore compatible).
8
10
  */
9
11
 
10
- import { decode } from 'hyperskills';
12
+ import { encode, decode } from '../hyperskills.js';
11
13
 
12
- export type BlockType =
14
+ export type WidgetType =
13
15
  | 'stat' | 'kv' | 'list' | 'chart' | 'alert' | 'code' | 'text' | 'actions' | 'tags'
14
16
  | 'stat-card' | 'data-table' | 'timeline' | 'profile' | 'trombinoscope' | 'json-viewer'
15
17
  | 'hemicycle' | 'chart-rich' | 'cards' | 'grid-data' | 'sankey' | 'map' | 'log'
16
- | 'gallery' | 'carousel' | 'd3';
18
+ | 'gallery' | 'carousel' | 'd3' | 'js-sandbox'
19
+ | (string & {}); // accept arbitrary widget types from widget packs while keeping autocompletion
20
+
21
+ /** @deprecated Use WidgetType */
22
+ export type BlockType = WidgetType;
17
23
 
18
24
  export type Mode = 'auto' | 'drag' | 'chat';
19
25
  export type LLMId = 'haiku' | 'sonnet' | 'gemma-e2b' | 'gemma-e4b';
20
26
 
21
- export interface Block {
27
+ export interface Widget {
22
28
  id: string;
23
- type: BlockType;
29
+ type: WidgetType;
24
30
  data: Record<string, unknown>;
25
31
  }
26
32
 
33
+ /** @deprecated Use Widget */
34
+ export type Block = Widget;
35
+
27
36
  export interface ChatMsg {
28
37
  id: string;
29
38
  role: 'user' | 'assistant' | 'system';
@@ -38,7 +47,7 @@ export interface McpToolInfo {
38
47
  }
39
48
 
40
49
  export interface CanvasSnapshot {
41
- blocks: Block[];
50
+ blocks: Widget[];
42
51
  mode: Mode;
43
52
  llm: LLMId;
44
53
  mcpUrl: string;
@@ -58,7 +67,7 @@ export interface CanvasSnapshot {
58
67
  type Listener = () => void;
59
68
 
60
69
  function uuid() {
61
- return 'b_' + Date.now().toString(36) + Math.random().toString(36).slice(2, 5);
70
+ return 'w_' + Date.now().toString(36) + Math.random().toString(36).slice(2, 5);
62
71
  }
63
72
 
64
73
  function msgId() {
@@ -71,7 +80,7 @@ function createCanvasVanilla() {
71
80
  function notify() { listeners.forEach(fn => fn()); }
72
81
 
73
82
  // ── State ──────────────────────────────────────────────────────────────
74
- let _blocks: Block[] = [];
83
+ let _blocks: Widget[] = [];
75
84
  let _mode: Mode = 'drag';
76
85
  let _llm: LLMId = 'haiku';
77
86
  let _mcpUrl = '';
@@ -85,14 +94,17 @@ function createCanvasVanilla() {
85
94
  let _statusColor = 'text-zinc-600';
86
95
  let _themeOverrides: Record<string, string> = {};
87
96
 
88
- // ── Block actions ──────────────────────────────────────────────────────
89
- function addBlock(type: BlockType, data: Record<string, unknown> = {}): Block {
90
- const block: Block = { id: uuid(), type, data };
91
- _blocks = [..._blocks, block];
97
+ // ── Widget actions ─────────────────────────────────────────────────────
98
+ function addWidget(type: WidgetType, data: Record<string, unknown> = {}): Widget {
99
+ const widget: Widget = { id: uuid(), type, data };
100
+ _blocks = [..._blocks, widget];
92
101
  notify();
93
- return block;
102
+ return widget;
94
103
  }
95
104
 
105
+ /** @deprecated Use addWidget */
106
+ const addBlock = addWidget;
107
+
96
108
  function removeBlock(id: string) {
97
109
  _blocks = _blocks.filter((b) => b.id !== id);
98
110
  notify();
@@ -119,7 +131,7 @@ function createCanvasVanilla() {
119
131
  notify();
120
132
  }
121
133
 
122
- function setBlocks(newBlocks: Block[]) {
134
+ function setBlocks(newBlocks: Widget[]) {
123
135
  _blocks = newBlocks;
124
136
  notify();
125
137
  }
@@ -200,47 +212,17 @@ function createCanvasVanilla() {
200
212
 
201
213
  async function buildHyperskillParam(): Promise<string> {
202
214
  const json = JSON.stringify(buildSkillJSON());
203
- const bytes = new TextEncoder().encode(json);
204
- // Auto-compress with gzip when payload exceeds 6 KB to keep URLs under nginx limits
205
- if (bytes.length > 6144) {
206
- const cs = new CompressionStream('gzip');
207
- const writer = cs.writable.getWriter();
208
- writer.write(bytes);
209
- writer.close();
210
- const compressed = new Uint8Array(await new Response(cs.readable).arrayBuffer());
211
- const b64 = btoa(String.fromCharCode(...compressed))
212
- .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
213
- return 'gz.' + b64;
214
- }
215
- // Small payloads: plain base64url
216
- return btoa(unescape(encodeURIComponent(json)))
217
- .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
215
+ const compress = json.length > 6144 ? 'gz' as const : undefined;
216
+ const url = await encode('https://x.local', json, compress ? { compress } : {});
217
+ return new URL(url).searchParams.get('hs')!;
218
218
  }
219
219
 
220
220
  async function loadFromParam(param: string): Promise<boolean> {
221
- try {
222
- let json: string;
223
-
224
- if (param.startsWith('gz.')) {
225
- let b64 = param.slice(3).replace(/-/g, '+').replace(/_/g, '/');
226
- while (b64.length % 4) b64 += '=';
227
- const compressed = Uint8Array.from(atob(b64), c => c.charCodeAt(0));
228
- const ds = new DecompressionStream('gzip');
229
- const writer = ds.writable.getWriter();
230
- writer.write(compressed);
231
- writer.close();
232
- json = await new Response(ds.readable).text();
233
- } else {
234
- let b64 = param.replace(/ /g, '+').replace(/-/g, '+').replace(/_/g, '/');
235
- while (b64.length % 4) b64 += '=';
236
- json = decodeURIComponent(escape(atob(b64)));
237
- }
238
-
239
- const skill = JSON.parse(json) as {
240
- mcp?: string; llm?: LLMId;
241
- theme?: Record<string, string>;
242
- blocks?: { type: BlockType; data: Record<string, unknown> }[];
243
- };
221
+ function applySkill(skill: {
222
+ mcp?: string; llm?: LLMId;
223
+ theme?: Record<string, string>;
224
+ blocks?: { type: WidgetType; data: Record<string, unknown> }[];
225
+ }) {
244
226
  if (skill.mcp) _mcpUrl = skill.mcp;
245
227
  if (skill.llm) _llm = skill.llm;
246
228
  if (skill.theme) _themeOverrides = skill.theme;
@@ -248,6 +230,21 @@ function createCanvasVanilla() {
248
230
  _blocks = skill.blocks.map((b) => ({ id: uuid(), type: b.type, data: b.data }));
249
231
  }
250
232
  notify();
233
+ }
234
+
235
+ // Try hyperskills NPM decode (handles gz., br., and plain base64)
236
+ try {
237
+ const { content: json } = await decode(param);
238
+ applySkill(JSON.parse(json));
239
+ return true;
240
+ } catch { /* fall through to legacy */ }
241
+
242
+ // Fallback: legacy format (plain base64 with escape/unescape)
243
+ try {
244
+ let b64 = param.replace(/ /g, '+').replace(/-/g, '+').replace(/_/g, '/');
245
+ while (b64.length % 4) b64 += '=';
246
+ const json = decodeURIComponent(escape(atob(b64)));
247
+ applySkill(JSON.parse(json));
251
248
  return true;
252
249
  } catch {
253
250
  return false;
@@ -258,7 +255,7 @@ function createCanvasVanilla() {
258
255
  async function loadFromUrl(url: string): Promise<boolean> {
259
256
  try {
260
257
  const { content: raw } = await decode(url);
261
- const decoded = JSON.parse(raw) as { meta?: Record<string, unknown>; content?: { blocks?: { type: BlockType; data: Record<string, unknown> }[] } };
258
+ const decoded = JSON.parse(raw) as { meta?: Record<string, unknown>; content?: { blocks?: { type: WidgetType; data: Record<string, unknown> }[] } };
262
259
  if (decoded.meta?.mcp) _mcpUrl = decoded.meta.mcp as string;
263
260
  if (decoded.meta?.llm) _llm = decoded.meta.llm as LLMId;
264
261
  if (decoded.meta?.theme) _themeOverrides = decoded.meta.theme as Record<string, string>;
@@ -325,8 +322,14 @@ function createCanvasVanilla() {
325
322
  setMcpUrl(u: string) { _mcpUrl = u; notify(); },
326
323
  setGenerating(g: boolean) { _generating = g; notify(); },
327
324
 
328
- // Block actions
329
- addBlock, removeBlock, updateBlock, moveBlock, clearBlocks, setBlocks,
325
+ // Widget actions (primary name)
326
+ addWidget,
327
+
328
+ // Backward compat alias
329
+ addBlock,
330
+
331
+ // Block actions (kept as-is)
332
+ removeBlock, updateBlock, moveBlock, clearBlocks, setBlocks,
330
333
 
331
334
  // Chat
332
335
  addMsg, updateMsg, clearMessages,
@@ -75,7 +75,7 @@ describe('loadDemoSkills', () => {
75
75
  const skills = listSkills();
76
76
  expect(skills.length).toBe(3);
77
77
  expect(skills.every(s => s.mcp === 'https://mcp.code4code.eu/mcp')).toBe(true);
78
- expect(skills.every(s => s.mcpName === 'code4code')).toBe(true);
78
+ expect(skills.every(s => s.mcpName === 'tricoteuses')).toBe(true);
79
79
  });
80
80
 
81
81
  it('does not overwrite existing skills', () => {