@webmcp-auto-ui/sdk 0.5.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 +11 -3
- package/package.json +2 -2
- package/src/canvas-vanilla.ts +1 -1
- package/src/canvas.ts +1 -1
- package/src/hyperskills.ts +47 -0
- package/src/index.ts +2 -2
- package/src/mcp-demo-servers.ts +9 -3
- package/src/skills/registry.ts +9 -3
- package/src/stores/canvas.svelte.ts +76 -242
- package/src/stores/canvas.ts +62 -59
- package/tests/registry.test.ts +1 -1
package/README.md
CHANGED
|
@@ -54,12 +54,20 @@ 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.
|
|
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
|
|
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_`).
|
|
63
71
|
|
|
64
72
|
## MCP Demo Servers
|
|
65
73
|
|
|
@@ -69,7 +77,7 @@ A built-in registry of 7 demo MCP server endpoints for testing and showcasing:
|
|
|
69
77
|
import { MCP_DEMO_SERVERS } from '@webmcp-auto-ui/sdk';
|
|
70
78
|
|
|
71
79
|
// MCP_DEMO_SERVERS: Array<{ url: string; name: string; description: string }>
|
|
72
|
-
// Includes:
|
|
80
|
+
// Includes: tricoteuses, weather, finance, etc.
|
|
73
81
|
```
|
|
74
82
|
|
|
75
83
|
Used by the `RemoteMCPserversDemo` component in `@webmcp-auto-ui/ui` to let users discover and connect to available servers.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webmcp-auto-ui/sdk",
|
|
3
|
-
"version": "
|
|
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.
|
|
30
|
+
"hyperskills": "^0.1.4"
|
|
31
31
|
},
|
|
32
32
|
"peerDependencies": {
|
|
33
33
|
"svelte": "^5.0.0"
|
package/src/canvas-vanilla.ts
CHANGED
|
@@ -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, diff } 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');
|
package/src/mcp-demo-servers.ts
CHANGED
|
@@ -11,13 +11,13 @@ export interface McpDemoServer {
|
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* MCP demo servers available for webmcp-auto-ui demos.
|
|
14
|
-
*
|
|
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: '
|
|
20
|
-
name: '
|
|
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
|
];
|
package/src/skills/registry.ts
CHANGED
|
@@ -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: '
|
|
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: '
|
|
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: '
|
|
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
|
-
*
|
|
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 {
|
|
9
|
+
import { canvasVanilla } from './canvas.js';
|
|
10
|
+
import type { Widget, WidgetType, Mode, LLMId, ChatMsg, McpToolInfo } from './canvas.js';
|
|
7
11
|
|
|
8
|
-
export
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
// ──
|
|
46
|
-
let blocks = $state<
|
|
47
|
-
let mode = $state<Mode>(
|
|
48
|
-
let llm = $state<LLMId>(
|
|
49
|
-
let mcpUrl = $state(
|
|
50
|
-
let mcpConnected = $state(
|
|
51
|
-
let mcpConnecting = $state(
|
|
52
|
-
let mcpName = $state(
|
|
53
|
-
let mcpTools = $state<McpToolInfo[]>(
|
|
54
|
-
let messages = $state<ChatMsg[]>(
|
|
55
|
-
let generating = $state(
|
|
56
|
-
let statusText = $state(
|
|
57
|
-
let statusColor = $state(
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
// ──
|
|
64
|
-
|
|
65
|
-
const
|
|
66
|
-
blocks =
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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) {
|
|
261
|
-
setLlm(l: LLMId) {
|
|
262
|
-
setMcpUrl(u: string) {
|
|
263
|
-
setGenerating(g: boolean) {
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
package/src/stores/canvas.ts
CHANGED
|
@@ -1,29 +1,38 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Canvas state store — Vanilla (framework-agnostic)
|
|
3
|
-
* Manages
|
|
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
|
|
7
|
-
*
|
|
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
|
|
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
|
|
27
|
+
export interface Widget {
|
|
22
28
|
id: string;
|
|
23
|
-
type:
|
|
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:
|
|
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 '
|
|
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:
|
|
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
|
-
// ──
|
|
89
|
-
function
|
|
90
|
-
const
|
|
91
|
-
_blocks = [..._blocks,
|
|
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
|
|
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:
|
|
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
|
|
204
|
-
|
|
205
|
-
|
|
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
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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:
|
|
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
|
-
//
|
|
329
|
-
|
|
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,
|
package/tests/registry.test.ts
CHANGED
|
@@ -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 === '
|
|
78
|
+
expect(skills.every(s => s.mcpName === 'tricoteuses')).toBe(true);
|
|
79
79
|
});
|
|
80
80
|
|
|
81
81
|
it('does not overwrite existing skills', () => {
|