@imenam/mcp-design 1.0.8 → 1.0.10
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 +334 -322
- package/dist/src/core/clone.d.ts +2 -0
- package/dist/src/core/clone.js +1 -1
- package/dist/src/core/design-store.d.ts +9 -0
- package/dist/src/core/design-store.js +26 -0
- package/dist/src/gui-worker.js +3 -2
- package/dist/src/index.js +32 -4
- package/dist/src/tools/duplicate-design.d.ts +4 -0
- package/dist/src/tools/duplicate-design.js +18 -0
- package/dist/src/tools/duplicate-nodes.d.ts +5 -3
- package/dist/src/tools/duplicate-nodes.js +81 -24
- package/dist/src/version.d.ts +1 -1
- package/dist/src/version.js +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,322 +1,334 @@
|
|
|
1
|
-
# @imenam/mcp-design
|
|
2
|
-
|
|
3
|
-
Un canvas de design pilotable par un agent, avec une interface graphique où l'utilisateur reprend la main.
|
|
4
|
-
|
|
5
|
-
L'agent compose une planche (formes, texte) via des outils MCP. L'utilisateur ouvre la GUI, déplace, retaille, recolore, réécrit. L'agent relit ensuite **exactement ce qui a changé** et l'intègre dans le code de l'application.
|
|
6
|
-
|
|
7
|
-
```
|
|
8
|
-
Agent ──add_nodes/update_nodes──▶ Design ◀──glisser-déposer── Utilisateur (GUI)
|
|
9
|
-
▲ │
|
|
10
|
-
└──────────── get_changes ──── diff depuis la dernière baseline ──────┘
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
---
|
|
14
|
-
|
|
15
|
-
## Installation
|
|
16
|
-
|
|
17
|
-
```bash
|
|
18
|
-
npm install -g @imenam/mcp-design
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
Ou directement via `npx`, sans installation.
|
|
22
|
-
|
|
23
|
-
### Déclaration dans un projet
|
|
24
|
-
|
|
25
|
-
```bash
|
|
26
|
-
npx -y @imenam/mcp-design --claude-setup-mcp
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
La commande crée (ou complète) le `.mcp.json` du répertoire courant avec une entrée `mcp-design` dont les variables sont pré-remplies. Ajoutez `--force` pour remplacer une entrée existante ; les autres serveurs sont préservés.
|
|
30
|
-
|
|
31
|
-
Deux flags permettent de choisir directement comment la GUI sera exposée, sans éditer le JSON ensuite :
|
|
32
|
-
|
|
33
|
-
```bash
|
|
34
|
-
# GUI derrière le proxy central
|
|
35
|
-
npx -y @imenam/mcp-design --claude-setup-mcp --proxy_url=http://localhost:3000
|
|
36
|
-
|
|
37
|
-
# GUI en accès direct, sans proxy : http://localhost:4500
|
|
38
|
-
npx -y @imenam/mcp-design --claude-setup-mcp --app_port=4500
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
Les deux flags sont exclusifs — la commande échoue si les deux sont passés.
|
|
42
|
-
|
|
43
|
-
Exemple d'entrée générée :
|
|
44
|
-
|
|
45
|
-
```json
|
|
46
|
-
{
|
|
47
|
-
"mcpServers": {
|
|
48
|
-
"mcp-design": {
|
|
49
|
-
"command": "npx",
|
|
50
|
-
"args": ["-y", "@imenam/mcp-design"],
|
|
51
|
-
"env": {
|
|
52
|
-
"PROXY_URL": "http://localhost:3000",
|
|
53
|
-
"APP_PORT": "",
|
|
54
|
-
"APP_GROUP": "Mon Projet",
|
|
55
|
-
"APP_NAME": "Design Mon Projet",
|
|
56
|
-
"APP_PATH": "/design-mon-projet",
|
|
57
|
-
"MCP_DESIGN_DATA_DIR": "C:/dev/mon-projet/.design-data",
|
|
58
|
-
"MCP_LOG_DIR": ""
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
### Variables d'environnement
|
|
66
|
-
|
|
67
|
-
| Variable | Rôle |
|
|
68
|
-
|---|---|
|
|
69
|
-
| `PROXY_URL` | URL du [proxy central](https://github.com/imenam/proxy-setup) : le proxy attribue le port et monte la GUI sous `APP_PATH`. |
|
|
70
|
-
| `APP_PORT` | Port local utilisé **quand `PROXY_URL` est absent** : la GUI est servie directement sur `http://localhost:<port>`, à la racine. Permet d'utiliser l'interface sans le hub de centralisation. |
|
|
71
|
-
| `APP_PATH` | Chemin de montage de la GUI derrière le proxy (défaut `/design-gui`). Ignoré en mode `APP_PORT`. `PROXY_APP_PATH` puis `GUI_PATH` sont acceptés en repli. |
|
|
72
|
-
| `APP_NAME` | Nom affiché dans le dashboard du proxy. `PROXY_APP_NAME` puis `GUI_NAME` sont acceptés en repli. |
|
|
73
|
-
| `APP_PATH_PREFIX` | Préfixe de namespace ajouté au chemin **par défaut** (ex. `/abc123` → `/abc123/design-gui`). Sert à isoler plusieurs instances sous un même proxy. |
|
|
74
|
-
| `APP_GROUP` | Section repliable du dashboard du proxy (facultatif). |
|
|
75
|
-
| `MCP_DESIGN_DATA_DIR` | Répertoire de stockage des designs. `MCP_DATA_DIR` est accepté en repli. Défaut : `<package>/.design-data`. |
|
|
76
|
-
| `MCP_LOG_DIR` | Répertoire des logs. Défaut : `<data>/logs`. |
|
|
77
|
-
|
|
78
|
-
`PROXY_URL` et `APP_PORT` s'excluent : `PROXY_URL` prime si les deux sont renseignés. **Si aucune des deux n'est définie, la GUI est simplement désactivée** — les outils MCP continuent de fonctionner.
|
|
79
|
-
|
|
80
|
-
`APP_PATH` / `APP_NAME` sont les noms canoniques ; `PROXY_APP_*` puis `GUI_*` restent acceptés, dans cet ordre de priorité décroissante. La règle de `APP_PATH_PREFIX` est simple : **il ne préfixe que le chemin par défaut**. Dès que `APP_PATH` (ou l'un de ses replis) est renseigné, cette valeur est prise telle quelle, sans préfixe — la saisie manuelle l'emporte toujours. Les chemins sont normalisés (un seul `/` de séparation, pas de `/` final) et le préfixage est idempotent. Le `<base href>` de l'interface suit le chemin réellement enregistré, assets compris.
|
|
81
|
-
|
|
82
|
-
La résolution est assurée par le paquet partagé [`@imenam/mcp-gui-interface`](https://www.npmjs.com/package/@imenam/mcp-gui-interface), qui documente la règle en détail.
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
| `
|
|
105
|
-
| `
|
|
106
|
-
| `
|
|
107
|
-
| `
|
|
108
|
-
| `
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
|
118
|
-
|
|
119
|
-
| `
|
|
120
|
-
| `
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
//
|
|
191
|
-
{ "type": "
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
| `
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
1
|
+
# @imenam/mcp-design
|
|
2
|
+
|
|
3
|
+
Un canvas de design pilotable par un agent, avec une interface graphique où l'utilisateur reprend la main.
|
|
4
|
+
|
|
5
|
+
L'agent compose une planche (formes, texte) via des outils MCP. L'utilisateur ouvre la GUI, déplace, retaille, recolore, réécrit. L'agent relit ensuite **exactement ce qui a changé** et l'intègre dans le code de l'application.
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
Agent ──add_nodes/update_nodes──▶ Design ◀──glisser-déposer── Utilisateur (GUI)
|
|
9
|
+
▲ │
|
|
10
|
+
└──────────── get_changes ──── diff depuis la dernière baseline ──────┘
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install -g @imenam/mcp-design
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Ou directement via `npx`, sans installation.
|
|
22
|
+
|
|
23
|
+
### Déclaration dans un projet
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npx -y @imenam/mcp-design --claude-setup-mcp
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
La commande crée (ou complète) le `.mcp.json` du répertoire courant avec une entrée `mcp-design` dont les variables sont pré-remplies. Ajoutez `--force` pour remplacer une entrée existante ; les autres serveurs sont préservés.
|
|
30
|
+
|
|
31
|
+
Deux flags permettent de choisir directement comment la GUI sera exposée, sans éditer le JSON ensuite :
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
# GUI derrière le proxy central
|
|
35
|
+
npx -y @imenam/mcp-design --claude-setup-mcp --proxy_url=http://localhost:3000
|
|
36
|
+
|
|
37
|
+
# GUI en accès direct, sans proxy : http://localhost:4500
|
|
38
|
+
npx -y @imenam/mcp-design --claude-setup-mcp --app_port=4500
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Les deux flags sont exclusifs — la commande échoue si les deux sont passés.
|
|
42
|
+
|
|
43
|
+
Exemple d'entrée générée :
|
|
44
|
+
|
|
45
|
+
```json
|
|
46
|
+
{
|
|
47
|
+
"mcpServers": {
|
|
48
|
+
"mcp-design": {
|
|
49
|
+
"command": "npx",
|
|
50
|
+
"args": ["-y", "@imenam/mcp-design"],
|
|
51
|
+
"env": {
|
|
52
|
+
"PROXY_URL": "http://localhost:3000",
|
|
53
|
+
"APP_PORT": "",
|
|
54
|
+
"APP_GROUP": "Mon Projet",
|
|
55
|
+
"APP_NAME": "Design Mon Projet",
|
|
56
|
+
"APP_PATH": "/design-mon-projet",
|
|
57
|
+
"MCP_DESIGN_DATA_DIR": "C:/dev/mon-projet/.design-data",
|
|
58
|
+
"MCP_LOG_DIR": ""
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Variables d'environnement
|
|
66
|
+
|
|
67
|
+
| Variable | Rôle |
|
|
68
|
+
|---|---|
|
|
69
|
+
| `PROXY_URL` | URL du [proxy central](https://github.com/imenam/proxy-setup) : le proxy attribue le port et monte la GUI sous `APP_PATH`. |
|
|
70
|
+
| `APP_PORT` | Port local utilisé **quand `PROXY_URL` est absent** : la GUI est servie directement sur `http://localhost:<port>`, à la racine. Permet d'utiliser l'interface sans le hub de centralisation. |
|
|
71
|
+
| `APP_PATH` | Chemin de montage de la GUI derrière le proxy (défaut `/design-gui`). Ignoré en mode `APP_PORT`. `PROXY_APP_PATH` puis `GUI_PATH` sont acceptés en repli. |
|
|
72
|
+
| `APP_NAME` | Nom affiché dans le dashboard du proxy. `PROXY_APP_NAME` puis `GUI_NAME` sont acceptés en repli. |
|
|
73
|
+
| `APP_PATH_PREFIX` | Préfixe de namespace ajouté au chemin **par défaut** (ex. `/abc123` → `/abc123/design-gui`). Sert à isoler plusieurs instances sous un même proxy. |
|
|
74
|
+
| `APP_GROUP` | Section repliable du dashboard du proxy (facultatif). |
|
|
75
|
+
| `MCP_DESIGN_DATA_DIR` | Répertoire de stockage des designs. `MCP_DATA_DIR` est accepté en repli. Défaut : `<package>/.design-data`. |
|
|
76
|
+
| `MCP_LOG_DIR` | Répertoire des logs. Défaut : `<data>/logs`. |
|
|
77
|
+
|
|
78
|
+
`PROXY_URL` et `APP_PORT` s'excluent : `PROXY_URL` prime si les deux sont renseignés. **Si aucune des deux n'est définie, la GUI est simplement désactivée** — les outils MCP continuent de fonctionner.
|
|
79
|
+
|
|
80
|
+
`APP_PATH` / `APP_NAME` sont les noms canoniques ; `PROXY_APP_*` puis `GUI_*` restent acceptés, dans cet ordre de priorité décroissante. La règle de `APP_PATH_PREFIX` est simple : **il ne préfixe que le chemin par défaut**. Dès que `APP_PATH` (ou l'un de ses replis) est renseigné, cette valeur est prise telle quelle, sans préfixe — la saisie manuelle l'emporte toujours. Les chemins sont normalisés (un seul `/` de séparation, pas de `/` final) et le préfixage est idempotent. Le `<base href>` de l'interface suit le chemin réellement enregistré, assets compris.
|
|
81
|
+
|
|
82
|
+
La résolution est assurée par le paquet partagé [`@imenam/mcp-gui-interface`](https://www.npmjs.com/package/@imenam/mcp-gui-interface), qui documente la règle en détail.
|
|
83
|
+
|
|
84
|
+
### Exposition réseau
|
|
85
|
+
|
|
86
|
+
La GUI écoute sur `127.0.0.1` uniquement, en mode proxy comme en mode `APP_PORT` : elle n'est pas joignable directement depuis une autre machine. Pour y accéder à distance, passez par le [proxy central](https://github.com/imenam/proxy-setup) ou le gateway manager, qui relaient vers `localhost:<port>`.
|
|
87
|
+
|
|
88
|
+
Chaque requête traverse la garde de [`@imenam/mcp-gui-interface`](https://www.npmjs.com/package/@imenam/mcp-gui-interface) (`honoGuiGuard`), qui répond `403` :
|
|
89
|
+
|
|
90
|
+
- à tout en-tête `Host` autre que `localhost`, `127.0.0.1` ou `::1` (protection contre le DNS rebinding) ;
|
|
91
|
+
- aux modifications (méthodes autres que `GET`, `HEAD`, `OPTIONS`) venues d'un autre site, d'après `Sec-Fetch-Site` ou, à défaut, `Origin` (sont admis : la machine elle-même, le proxy de `PROXY_URL`).
|
|
92
|
+
|
|
93
|
+
Un design est un simple fichier JSON dans `<data>/designs/<id>.json` — lisible, diffable, versionnable.
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## Les outils
|
|
98
|
+
|
|
99
|
+
### Planches
|
|
100
|
+
|
|
101
|
+
| Outil | Effet |
|
|
102
|
+
|---|---|
|
|
103
|
+
| `list_designs` | Liste les planches, leur taille, leur nombre d'éléments et le nombre de modifications non encore récupérées. |
|
|
104
|
+
| `create_design` | Crée une planche vide (`name`, et facultativement `width`, `height`, `background`). |
|
|
105
|
+
| `duplicate_design` | Crée une nouvelle planche reprenant toute une planche existante — dimensions, fond, éléments, groupes et instances. Les identifiants des éléments sont conservés ; `name` et `group` sont facultatifs (par défaut, le nom d'origine suivi de « (copy) » et le même groupe de pages). La copie part sans modification en attente. |
|
|
106
|
+
| `get_design` | Lit une planche : `format: "summary"` (inventaire compact, défaut) ou `format: "json"` (valeurs exactes). |
|
|
107
|
+
| `update_design` | Renomme la planche, change ses dimensions ou son fond. |
|
|
108
|
+
| `delete_design` | Supprime définitivement une planche. |
|
|
109
|
+
|
|
110
|
+
### Éléments
|
|
111
|
+
|
|
112
|
+
| Outil | Effet |
|
|
113
|
+
|---|---|
|
|
114
|
+
| `add_nodes` | Ajoute un ou plusieurs éléments par-dessus les autres. |
|
|
115
|
+
| `update_nodes` | Patche des éléments existants — seuls les champs fournis changent. |
|
|
116
|
+
| `delete_nodes` | Supprime des éléments par identifiant. Supprimer un groupe emporte son contenu. |
|
|
117
|
+
| `duplicate_nodes` | Copie des éléments ou des groupes avec tout leur contenu, sous de nouveaux identifiants. `offset_x`/`offset_y` et `count` disposent une série de copies, `parent_id` les dépose dans un groupe. Avec `target_design_id`, la copie va dans une **autre planche** : les éléments gardent leur position à l'écran, et les composants reproduits par les instances copiées sont copiés avec elles, une seule fois, à la racine. |
|
|
118
|
+
| `group_nodes` | Enveloppe des éléments dans un nouveau groupe, sans rien déplacer à l'écran. |
|
|
119
|
+
| `ungroup_nodes` | Dissout des groupes : leur contenu remonte d'un niveau, toujours sans bouger. |
|
|
120
|
+
| `detach_instances` | Rompt le lien d'une instance avec son composant : son contenu devient des éléments ordinaires. |
|
|
121
|
+
|
|
122
|
+
Sept types d'éléments : `rect`, `ellipse`, `line`, `text`, `icon`, `group` et `instance`. Seul `type` est obligatoire — sauf pour `icon`, qui a besoin du nom de l'icône ; tout le reste tombe sur des valeurs par défaut visibles, donc `{ "type": "rect" }` suffit à créer quelque chose qu'on voit à l'écran.
|
|
123
|
+
|
|
124
|
+
**Champs communs** — `name`, `parentId`, `x`, `y`, `width`, `height` (en pixels, origine en haut à gauche), `rotation` (degrés, autour du centre de la boîte), `opacity` (0–1).
|
|
125
|
+
|
|
126
|
+
**Toute longueur est en pixels**, sans exception : `x`, `y`, `width`, `height`, `strokeWidth`, `radius`, `fontSize` et `lineHeight`. Chaque champ porte sa description et son unité dans [`shared/node-fields.ts`](shared/node-fields.ts), d'où le schéma zod **et** l'`inputSchema` MCP les tirent tous les deux — une unité n'est écrite qu'à un seul endroit.
|
|
127
|
+
|
|
128
|
+
| Type | Champs propres |
|
|
129
|
+
|---|---|
|
|
130
|
+
| `rect` | `fill`, `stroke`, `strokeWidth`, `radius` |
|
|
131
|
+
| `ellipse` | `fill`, `stroke`, `strokeWidth` |
|
|
132
|
+
| `line` | `stroke`, `strokeWidth` — le trait va de `(x, y)` à `(x + width, y + height)`, donc les deltas négatifs encodent la direction |
|
|
133
|
+
| `text` | `text`, `color`, `fontSize`, `fontFamily`, `fontWeight`, `fontStyle`, `textAlign`, `lineHeight` (en px) |
|
|
134
|
+
| `icon` | `icon` (obligatoire — le nom du dessin), `fill`, `stroke`, `strokeWidth` |
|
|
135
|
+
| `group` | aucun — c'est un pur conteneur |
|
|
136
|
+
| `instance` | `componentId` (obligatoire — le groupe reproduit) ; rien d'autre, son apparence vient de ce groupe |
|
|
137
|
+
|
|
138
|
+
### Icônes
|
|
139
|
+
|
|
140
|
+
Un agent qui n'a que des formes à sa disposition finit par approximer une icône avec trois ellipses et deux traits : le rendu est médiocre, et la maquette relue plus tard ne dit plus ce que ce tas de formes représentait. Le type `icon` répond exactement à ça — **1791 icônes [Lucide](https://lucide.dev) (licence ISC) sont embarquées** dans le paquet.
|
|
141
|
+
|
|
142
|
+
```json
|
|
143
|
+
{ "type": "icon", "icon": "shopping-cart", "x": 24, "y": 18, "width": 24, "height": 24, "stroke": "#1a1a18" }
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Le nœud stocke le **nom**, jamais le tracé : le JSON reste lisible — `shopping-cart` dit à l'agent qui le relit ce que la forme représente —, un nœud `icon` pèse moins qu'un `rect`, et changer d'icône est un `update_nodes` d'un seul champ. Le SVG est résolu au rendu depuis [`shared/icons/`](shared/icons/), donc le canvas, l'export et la capture dessinent rigoureusement la même chose.
|
|
147
|
+
|
|
148
|
+
| Outil | Effet |
|
|
149
|
+
|---|---|
|
|
150
|
+
| `search_icons` | Cherche une icône par description (`query`), en anglais et par synonymes — « basket » sort `shopping-cart`. Renvoie les noms **et** une planche de vignettes, sauf si `preview: false`. |
|
|
151
|
+
|
|
152
|
+
Les icônes les plus courantes sont listées directement dans la description d'`add_nodes` (`CORE_ICONS` dans [`shared/icons/index.ts`](shared/icons/index.ts)) : habiller une interface ordinaire ne demande aucun aller-retour. `search_icons` sert à la traîne longue. Un nom refusé repart avec des suggestions issues des synonymes, ce qui rattrape aussi bien une faute qu'un renommage de la bibliothèque (`home` → `house`).
|
|
153
|
+
|
|
154
|
+
Trois règles à connaître :
|
|
155
|
+
|
|
156
|
+
- **`strokeWidth` est la seule exception à la règle du pixel** : il s'exprime dans la grille 24 × 24 de l'icône (2 = graisse normale, 1.5 = fine, 2.5 = grasse). Une icône de 96 px avec `strokeWidth: 2` dessine un trait de 8 px à l'écran — le trait suit la taille, il n'y a rien à compenser.
|
|
157
|
+
- **La boîte est carrée** : `width` et `height` sont ramenés au plus petit des deux, et le tracé est mis à l'échelle uniformément puis centré. Jamais déformé.
|
|
158
|
+
- **Un nom inconnu ne casse rien** : l'icône est remplacée par un carré en pointillés avec un `?`. Le même SVG servant au canvas, à l'export et à la capture, une exception y serait fatale partout à la fois — un design ouvert après une mise à jour de la bibliothèque doit rester ouvrable, et montrer *où* est le problème.
|
|
159
|
+
|
|
160
|
+
Côté GUI, l'outil « Icône » de la barre d'outils (raccourci `I`) ouvre un sélecteur cherchable, et l'inspecteur permet de remplacer l'icône d'un élément existant. L'index de synonymes y est chargé à la demande, pour ne pas peser sur le premier affichage du canvas.
|
|
161
|
+
|
|
162
|
+
La bibliothèque est figée dans le dépôt par `npm run generate-icons` ([`scripts/generate-icons.js`](scripts/generate-icons.js)) : ni le serveur publié ni le bundle de la GUI ne dépendent de Lucide à l'exécution, et la géométrie d'un design ne peut pas changer au gré d'un `npm install`.
|
|
163
|
+
|
|
164
|
+
### Hiérarchie
|
|
165
|
+
|
|
166
|
+
Chaque élément porte un `parentId` : `null` à la racine de la planche, sinon l'identifiant d'un `group`. **Les coordonnées sont exprimées dans l'espace du parent** : un élément à la racine se positionne par rapport à la planche, un élément dans un groupe par rapport à l'origine de ce groupe.
|
|
167
|
+
|
|
168
|
+
C'est ce qui rend un déplacement de groupe lisible côté agent — un seul champ change, pas les coordonnées de chaque membre :
|
|
169
|
+
|
|
170
|
+
```
|
|
171
|
+
- g_a1b2c3 — "Carte produit" (group)
|
|
172
|
+
x: 100 -> 260
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
La taille d'un groupe est **déduite de son contenu** : `width` et `height` sont recalculés à chaque mutation, ne peuvent pas être écrits, et n'apparaissent jamais dans un diff. Le stockage reste une liste plate, ordonnée en parcours préfixe — chaque parent suivi de son sous-arbre — ce qui garde le fichier JSON lisible et diffable.
|
|
176
|
+
|
|
177
|
+
Le texte n'est **jamais re-wrappé automatiquement** : seuls les `\n` explicites créent une nouvelle ligne. Le comportement est ainsi identique à l'écran et à l'export.
|
|
178
|
+
|
|
179
|
+
`lineHeight` est une **distance en pixels entre deux lignes de base**, pas un multiplicateur de `fontSize` : avec `fontSize: 64`, on écrit `76`, pas `1.2`. C'est l'unité que les agents emploient spontanément, et la seule qui se lise dans la même échelle que le reste. Une valeur sous 4 est donc refusée à l'écriture avec un message explicite, et les fichiers écrits avant ce changement sont convertis à la lecture (`ratio x fontSize`) par [`src/core/migrate.ts`](src/core/migrate.ts). Champ omis = `fontSize x 1.35`.
|
|
180
|
+
|
|
181
|
+
#### La boîte d'un texte épouse son texte
|
|
182
|
+
|
|
183
|
+
Le bloc de lignes est **centré verticalement dans la boîte** du nœud : c'est ce qui permet de centrer un libellé dans un bouton sans calculer de décalage. Encore faut-il que la boîte soit celle qu'on croit — une hauteur par défaut plus grande que le texte le fait descendre sous le point posé.
|
|
184
|
+
|
|
185
|
+
Donc : **un nœud `text` créé sans `height` prend la hauteur exacte de son texte** (`lignes x lineHeight`). Le centrage n'a alors plus rien à décaler, et la première ligne commence bien à `y`, à gauche de `x`. Ajouter une ligne agrandit la boîte au lieu de la faire déborder : une boîte qui épousait son texte continue de l'épouser, à chaque changement de `text`, `fontSize` ou `lineHeight`, à l'écriture par l'agent comme à l'édition dans la GUI.
|
|
186
|
+
|
|
187
|
+
Une hauteur **explicite**, elle, n'est jamais retouchée — c'est le geste qui dit « centre ce libellé » :
|
|
188
|
+
|
|
189
|
+
```jsonc
|
|
190
|
+
{ "type": "text", "x": 40, "y": 120, "text": "Titre", "fontSize": 32 } // boîte = 43.2px, texte posé à y
|
|
191
|
+
{ "type": "text", "x": 40, "y": 100, "height": 26, "text": "OK", "fontSize": 12 } // libellé centré dans son bouton
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### Groupes réutilisables
|
|
195
|
+
|
|
196
|
+
Quand un même bloc revient plusieurs fois — une carte, une ligne de liste, un bouton —, il ne se recopie pas : il se **construit une fois** en groupe, puis s'**instancie**. Une `instance` ne stocke aucun contenu, seulement l'identifiant du groupe qu'elle reproduit ; le sous-arbre est reconstitué au rendu. Éditer le groupe met donc à jour **toutes** ses instances d'un coup, et un bloc répété reste une seule chose à lire et à modifier. C'est le fonctionnement d'un composant principal dans Figma.
|
|
197
|
+
|
|
198
|
+
```jsonc
|
|
199
|
+
// 1. le composant : un groupe ordinaire
|
|
200
|
+
{ "type": "group", "id": "card", "name": "Carte" }
|
|
201
|
+
// 2. ses copies liées
|
|
202
|
+
{ "type": "instance", "componentId": "card", "x": 320, "y": 80 }
|
|
203
|
+
{ "type": "instance", "componentId": "card", "x": 640, "y": 80 }
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Règles, volontairement peu nombreuses :
|
|
207
|
+
|
|
208
|
+
- **Rien ne se surcharge dans une instance** — c'est ce qui garantit qu'elles restent identiques. Sa position, sa rotation et son opacité lui appartiennent ; tout le reste vient du composant. Une copie qui doit diverger devient un second groupe, ou se détache (`detach_instances`).
|
|
209
|
+
- La **taille** d'une instance est déduite de son composant, comme celle d'un groupe.
|
|
210
|
+
- Changer `componentId` **échange** l'instance de composant (le *swap* de Figma).
|
|
211
|
+
- Les instances s'**imbriquent** (une instance de carte contenant une instance de bouton), mais un composant ne peut pas se contenir lui-même : le cas est refusé à l'écriture.
|
|
212
|
+
- Un groupe encore instancié ne peut être ni supprimé ni dissous — il faut d'abord détacher ou supprimer ses instances.
|
|
213
|
+
- Détacher une instance ne détache **pas** les instances imbriquées dans son composant : elles restent liées.
|
|
214
|
+
|
|
215
|
+
Côté agent, le gain se lit dans `get_changes` : une retouche du composant apparaît comme **un** changement, pas comme N copies modifiées.
|
|
216
|
+
|
|
217
|
+
### Ordre d'empilement
|
|
218
|
+
|
|
219
|
+
L'index dans la liste **est** l'ordre d'empilement : le dernier élément d'une fratrie est celui du dessus. `add_nodes` empile au-dessus, et `reorder_nodes` déplace ensuite ce qui existe déjà.
|
|
220
|
+
|
|
221
|
+
| Outil | Effet |
|
|
222
|
+
|---|---|
|
|
223
|
+
| `reorder_nodes` | `position: front / back / forward / backward` sur un ou plusieurs éléments. Un groupe emporte son contenu ; plusieurs éléments gardent leur ordre relatif. |
|
|
224
|
+
|
|
225
|
+
L'empilement est **relatif à la fratrie** : un élément ne passe jamais devant le contenu d'un autre groupe — c'est ce groupe qu'il faut alors réordonner, ou l'élément qu'il faut en sortir via `parentId`. La logique vit dans [`shared/tree.ts`](shared/tree.ts) et sert aussi bien l'outil que le panneau de calques de la GUI : les deux ne peuvent pas diverger.
|
|
226
|
+
|
|
227
|
+
Un lot est intégralement validé avant la moindre écriture : une entrée invalide ne laisse jamais un design à moitié modifié.
|
|
228
|
+
|
|
229
|
+
### Récupérer le travail de l'utilisateur
|
|
230
|
+
|
|
231
|
+
| Outil | Effet |
|
|
232
|
+
|---|---|
|
|
233
|
+
| `get_changes` | Liste tout ce qui a été ajouté, modifié, supprimé ou réordonné depuis la dernière baseline, champ par champ (`x: 40 -> 240`). |
|
|
234
|
+
| `mark_synced` | Fige l'état courant comme nouvelle baseline. |
|
|
235
|
+
| `export_design` | Rend la planche en `html`, `jsx`, `svg`, `json` ou `summary`. `destination_path` + `filename` écrivent un fichier **sur la machine de l'agent** au lieu de renvoyer le contenu. |
|
|
236
|
+
| `screenshot_design` | Rasterise la planche en PNG et le renvoie **en image** à l'agent. `node_id` cadre la capture sur un groupe (ou n'importe quel élément), `scale` zoome, `padding` ajoute de la marge, `destination_path` + `filename` écrivent un `.png` **sur la machine de l'agent** au lieu de renvoyer l'image. |
|
|
237
|
+
|
|
238
|
+
> **Où le fichier est écrit.** `destination_path` est un **dossier absolu** sur la machine de l'agent, `filename` le nom que le fichier y prend. Omis, les deux outils se comportent comme avant : le contenu (ou l'image) revient dans la réponse, sans fichier.
|
|
239
|
+
>
|
|
240
|
+
> En stdio, ce MCP tourne à côté de l'agent et écrit lui-même. Derrière [`@imenam/mcp-http-gateway`](https://www.npmjs.com/package/@imenam/mcp-http-gateway), il tourne sur la machine du gateway et ne peut pas atteindre le disque de l'agent : il déclare alors la paire `destination_path` / `destination_relay`, renvoie les octets, et le relais — qui tourne, lui, sur la machine de l'agent — les y écrit. `destination_relay` est rempli par le relais, jamais par l'agent, et cette voie exige `GATEWAY_INLINE_WRITE_ROOTS` dans le bloc `env` du relais.
|
|
241
|
+
|
|
242
|
+
**`get_changes` n'acquitte jamais ce qu'il montre.** Seul `mark_synced` déplace la baseline, et il ne doit être appelé qu'une fois les modifications réellement reportées dans le code. Un simple appel de lecture ne peut donc pas faire disparaître un diff que l'agent n'a pas encore traité.
|
|
243
|
+
|
|
244
|
+
### GUI
|
|
245
|
+
|
|
246
|
+
| Outil | Effet |
|
|
247
|
+
|---|---|
|
|
248
|
+
| `reconnect_gui` | Relance le worker GUI et le réenregistre auprès du proxy. `force: true` redémarre même s'il semble vivant. |
|
|
249
|
+
|
|
250
|
+
---
|
|
251
|
+
|
|
252
|
+
## Boucle de travail type
|
|
253
|
+
|
|
254
|
+
```
|
|
255
|
+
create_design { name: "Landing" }
|
|
256
|
+
add_nodes { design_id: "landing", nodes: [...] }
|
|
257
|
+
mark_synced { design_id: "landing" } ← point de départ de la comparaison
|
|
258
|
+
|
|
259
|
+
… l'utilisateur ouvre la GUI et retouche la maquette …
|
|
260
|
+
|
|
261
|
+
get_changes { design_id: "landing" } ← ce qu'il a changé, champ par champ
|
|
262
|
+
screenshot_design { design_id: "landing" } ← ce que ça donne réellement, en image
|
|
263
|
+
export_design { design_id: "landing", format: "html" }
|
|
264
|
+
… l'agent reporte les valeurs dans le code de l'application …
|
|
265
|
+
mark_synced { design_id: "landing" } ← acquittement
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
---
|
|
269
|
+
|
|
270
|
+
## L'interface
|
|
271
|
+
|
|
272
|
+
Trois colonnes : la liste des planches, le canvas, l'inspecteur et les calques.
|
|
273
|
+
|
|
274
|
+
- **Outils** — sélection, rectangle, ellipse, ligne, texte, icône. Glisser pour dimensionner, cliquer pour poser à la taille par défaut. L'outil icône ouvre d'abord son sélecteur : poser une icône suppose d'avoir choisi laquelle.
|
|
275
|
+
- **Édition** — déplacement au glisser, 8 poignées de redimensionnement (2 extrémités pour une ligne), sélection multiple au rectangle d'encadrement ou au Maj+clic.
|
|
276
|
+
- **Groupes** — `Ctrl+G` groupe la sélection, `Ctrl+Maj+G` la dissout. Un clic dans le canvas sélectionne le **groupe le plus haut** ; `Alt+clic` vise l'élément exact à l'intérieur. Déplacer un groupe déplace tout son contenu.
|
|
277
|
+
- **Texte** — double-clic pour éditer en place, avec la même métrique qu'au rendu final. Une boîte à la taille de son texte le reste : ajouter une ligne l'agrandit d'autant.
|
|
278
|
+
- **Inspecteur** — position, taille, rotation, opacité, couleurs, épaisseur, arrondi, contenu, taille et graisse de police, interligne (en px), alignement, choix de l'icône. Pour un groupe, la taille est affichée en lecture seule puisqu'elle suit son contenu ; pour une icône, un seul champ de taille, puisqu'elle est carrée.
|
|
279
|
+
- **Calques** — arbre repliable, ordre d'empilement, sélection, montée/descente d'un cran.
|
|
280
|
+
|
|
281
|
+
### Naviguer dans la planche
|
|
282
|
+
|
|
283
|
+
- **Zoom** : `Ctrl`/`Cmd` + molette, ancré sous le curseur — le point visé ne bouge pas.
|
|
284
|
+
- **Déplacement** : molette seule, ou **barre espace maintenue + glisser**. Tant que la barre espace est enfoncée, aucun geste ne peut déplacer un élément, même en partant de l'un d'eux. Le clic molette fait la même chose.
|
|
285
|
+
- Le sélecteur de zoom de la barre d'outils reste disponible pour revenir à une valeur ronde.
|
|
286
|
+
|
|
287
|
+
Raccourcis : `V` `R` `O` `L` `T` `I` (outils), `Ctrl+Z` / `Ctrl+Y` (annuler/rétablir), `Ctrl+G` / `Ctrl+Maj+G` (grouper / dissoudre), `Ctrl+D` (dupliquer), `Ctrl+A` (tout sélectionner), `Suppr`, flèches (déplacer d'1 px, 10 px avec `Maj`), `Échap`.
|
|
288
|
+
|
|
289
|
+
La sauvegarde est automatique et différée. Une modification faite par l'agent pendant que la GUI est ouverte y apparaît en direct, via SSE ; une modification faite dans la GUI est enregistrée comme venant de l'utilisateur, et c'est ce que `get_changes` restitue.
|
|
290
|
+
|
|
291
|
+
Le badge « n à synchroniser » d'une planche indique le nombre de modifications que l'agent n'a pas encore acquittées.
|
|
292
|
+
|
|
293
|
+
---
|
|
294
|
+
|
|
295
|
+
## Architecture
|
|
296
|
+
|
|
297
|
+
Deux processus, conformément au standard de l'écosystème (`@imenam/mcp-gui-interface`) :
|
|
298
|
+
|
|
299
|
+
- **MCP Master** (`src/index.ts`) — protocole JSON-RPC sur stdio, stockage des designs, cycle de vie du worker via `GuiLauncher`.
|
|
300
|
+
- **GUI Worker** (`src/gui-worker.ts`) — serveur Hono, enregistrement auprès du proxy via `ProxyClient`, API REST + SSE, sert la SPA React.
|
|
301
|
+
|
|
302
|
+
Les deux communiquent par IPC. Le worker se termine proprement quand le master disparaît (canal IPC fermé, PID parent absent) et quand une autre instance est déjà enregistrée (HTTP 409), ce qui évite les GUI orphelines.
|
|
303
|
+
|
|
304
|
+
`shared/render.ts` contient le rendu SVG d'un élément, `shared/tree.ts` la résolution de la hiérarchie en coordonnées document, `shared/instances.ts` l'expansion des instances en groupes, `shared/node-defaults.ts` les valeurs par défaut et `shared/icons/` la bibliothèque d'icônes. Tous sont importés **à la fois** par l'exporteur côté serveur et par le canvas côté navigateur : ce que l'utilisateur voit est exactement ce que `export_design` — et donc `screenshot_design`, qui rasterise ce même SVG via resvg — produit.
|
|
305
|
+
|
|
306
|
+
Les invariants de hiérarchie — parent existant et de type `group`, absence de cycle (y compris dans le graphe composant/instance), taille des groupes et des instances recalculée, ordre préfixe — sont appliqués dans `DesignStore.mutate`, donc sur **tout** chemin d'écriture : outil MCP comme sauvegarde depuis la GUI.
|
|
307
|
+
|
|
308
|
+
**Limite assumée de cette version** : un groupe se déplace, se pivote et change d'opacité, mais ne se redimensionne pas — mettre son contenu à l'échelle demanderait de propager un facteur dans tout le sous-arbre, ce qui dépasse le cadre d'un premier jet sobre.
|
|
309
|
+
|
|
310
|
+
---
|
|
311
|
+
|
|
312
|
+
## Développement
|
|
313
|
+
|
|
314
|
+
```bash
|
|
315
|
+
npm install
|
|
316
|
+
npm run build # backend (tsc) + GUI (vite)
|
|
317
|
+
npm test # suite complète
|
|
318
|
+
npm run generate-icons # refige shared/icons/ après une mise à jour de lucide-static
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
La suite comprend :
|
|
322
|
+
|
|
323
|
+
- des tests unitaires du store, de la fabrique d'éléments, du moteur de diff et de l'export ;
|
|
324
|
+
- des tests des outils MCP appelés directement ;
|
|
325
|
+
- des tests d'exposition réseau (`test/network-exposure.test.ts`) qui lancent le worker GUI en mode `APP_PORT` et vérifient l'écoute sur `127.0.0.1` seul, le refus d'un `Host` étranger et des modifications cross-site, et l'acceptation des requêtes relayées par le proxy ;
|
|
326
|
+
- une suite de bout en bout (`test/e2e.test.ts`) qui lance un vrai serveur MCP sur stdio, attend l'enregistrement de son worker auprès du proxy, puis vérifie l'aller-retour complet agent → GUI → agent.
|
|
327
|
+
|
|
328
|
+
La suite de bout en bout a besoin du proxy (`http://localhost:3000`, ou `E2E_PROXY_URL`). S'il est absent, elle est ignorée plutôt qu'en échec.
|
|
329
|
+
|
|
330
|
+
Pour inspecter le worker sans traverser le proxy, ajoutez `?__direct=1` à son port local : les assets ne sont alors pas préfixés par le chemin de montage.
|
|
331
|
+
|
|
332
|
+
### Méta-configurations du proxy
|
|
333
|
+
|
|
334
|
+
`mcp-design` n'expose pas `GET /api/configurations` : il n'a pas de configuration métier (ni token, ni identifiant de ressource) qui justifierait des profils. Le proxy ignore simplement les MCPs qui n'implémentent pas ces routes.
|
package/dist/src/core/clone.d.ts
CHANGED
|
@@ -14,6 +14,8 @@ export interface CloneResult {
|
|
|
14
14
|
copies: DesignNode[];
|
|
15
15
|
/** Ids des copies correspondant aux racines demandees. */
|
|
16
16
|
rootIds: string[];
|
|
17
|
+
/** Id d'origine -> id de la copie, pour chaque noeud copie. */
|
|
18
|
+
idMap: Map<string, string>;
|
|
17
19
|
}
|
|
18
20
|
/**
|
|
19
21
|
* Copie les sous-arbres donnes. Les liens parent/enfant sont recables sur les
|
package/dist/src/core/clone.js
CHANGED
|
@@ -30,5 +30,5 @@ export function cloneSubtrees(nodes, rootIds) {
|
|
|
30
30
|
id: idMap.get(node.id),
|
|
31
31
|
parentId: roots.has(node.id) ? node.parentId : (idMap.get(node.parentId ?? "") ?? node.parentId),
|
|
32
32
|
}));
|
|
33
|
-
return { copies, rootIds: rootIds.map((id) => idMap.get(id)) };
|
|
33
|
+
return { copies, rootIds: rootIds.map((id) => idMap.get(id)), idMap };
|
|
34
34
|
}
|
|
@@ -50,6 +50,15 @@ export declare class DesignStore {
|
|
|
50
50
|
has(id: string): boolean;
|
|
51
51
|
get(id: string): Design;
|
|
52
52
|
create(input: CreateDesignInput): Design;
|
|
53
|
+
/**
|
|
54
|
+
* Nouveau design reprenant la planche et tous les éléments d'un design
|
|
55
|
+
* existant, identifiants compris. La copie part synchronisée : sa référence
|
|
56
|
+
* de comparaison est son propre contenu.
|
|
57
|
+
*/
|
|
58
|
+
duplicate(sourceId: string, input?: {
|
|
59
|
+
name?: string;
|
|
60
|
+
group?: string | null;
|
|
61
|
+
}): Design;
|
|
53
62
|
/**
|
|
54
63
|
* Applique une mutation et incrémente la révision. `editor` distingue une
|
|
55
64
|
* modification de l'agent d'une modification faite à la main dans la GUI —
|
|
@@ -240,6 +240,32 @@ export class DesignStore {
|
|
|
240
240
|
this.write(design);
|
|
241
241
|
return design;
|
|
242
242
|
}
|
|
243
|
+
/**
|
|
244
|
+
* Nouveau design reprenant la planche et tous les éléments d'un design
|
|
245
|
+
* existant, identifiants compris. La copie part synchronisée : sa référence
|
|
246
|
+
* de comparaison est son propre contenu.
|
|
247
|
+
*/
|
|
248
|
+
duplicate(sourceId, input = {}) {
|
|
249
|
+
const source = this.get(sourceId);
|
|
250
|
+
const name = input.name?.trim() || `${source.name} (copy)`;
|
|
251
|
+
const now = new Date().toISOString();
|
|
252
|
+
const design = {
|
|
253
|
+
id: this.allocateId(name),
|
|
254
|
+
name,
|
|
255
|
+
group: input.group === undefined ? source.group : normalizeDesignGroup(input.group),
|
|
256
|
+
width: source.width,
|
|
257
|
+
height: source.height,
|
|
258
|
+
background: source.background,
|
|
259
|
+
nodes: structuredClone(source.nodes),
|
|
260
|
+
createdAt: now,
|
|
261
|
+
updatedAt: now,
|
|
262
|
+
revision: 0,
|
|
263
|
+
lastEditor: "agent",
|
|
264
|
+
baseline: { revision: 0, at: now, nodes: structuredClone(source.nodes) },
|
|
265
|
+
};
|
|
266
|
+
this.write(design);
|
|
267
|
+
return design;
|
|
268
|
+
}
|
|
243
269
|
/**
|
|
244
270
|
* Applique une mutation et incrémente la révision. `editor` distingue une
|
|
245
271
|
* modification de l'agent d'une modification faite à la main dans la GUI —
|
package/dist/src/gui-worker.js
CHANGED
|
@@ -2,7 +2,7 @@ import { serve } from "@hono/node-server";
|
|
|
2
2
|
import { serveStatic } from "@hono/node-server/serve-static";
|
|
3
3
|
import { Hono } from "hono";
|
|
4
4
|
import { streamSSE } from "hono/streaming";
|
|
5
|
-
import { ProxyClient, createLogger, setupLogging } from "@imenam/mcp-gui-interface";
|
|
5
|
+
import { GUI_LISTEN_HOST, ProxyClient, createLogger, honoGuiGuard, setupLogging } from "@imenam/mcp-gui-interface";
|
|
6
6
|
import path from "path";
|
|
7
7
|
import fs from "fs";
|
|
8
8
|
import { fileURLToPath } from "url";
|
|
@@ -77,6 +77,7 @@ const parentCheckInterval = setInterval(() => {
|
|
|
77
77
|
}
|
|
78
78
|
}, 2000);
|
|
79
79
|
const app = new Hono();
|
|
80
|
+
app.use("*", honoGuiGuard());
|
|
80
81
|
let proxyClient = null;
|
|
81
82
|
const sseControllers = new Set();
|
|
82
83
|
const pendingRequests = new Map();
|
|
@@ -301,7 +302,7 @@ app.get("/*", (c, next) => {
|
|
|
301
302
|
* l'utilisateur doit ouvrir : via le proxy, ou directement en localhost.
|
|
302
303
|
*/
|
|
303
304
|
function listen(port, publicUrl, isProxied) {
|
|
304
|
-
const serverInstance = serve({ fetch: app.fetch, port, hostname:
|
|
305
|
+
const serverInstance = serve({ fetch: app.fetch, port, hostname: GUI_LISTEN_HOST }, (info) => {
|
|
305
306
|
logger.info(`Design canvas server listening on port ${info.port}`);
|
|
306
307
|
safeSend(IPCMessageSchema.parse({
|
|
307
308
|
type: "READY",
|
package/dist/src/index.js
CHANGED
|
@@ -19,6 +19,7 @@ import { REORDER_DIRECTIONS } from "../shared/tree.js";
|
|
|
19
19
|
import { normalizeDesignGroup } from "../shared/schemas/design.schema.js";
|
|
20
20
|
import { listDesigns } from "./tools/list-designs.js";
|
|
21
21
|
import { createDesign } from "./tools/create-design.js";
|
|
22
|
+
import { duplicateDesign } from "./tools/duplicate-design.js";
|
|
22
23
|
import { getDesign } from "./tools/get-design.js";
|
|
23
24
|
import { updateDesign } from "./tools/update-design.js";
|
|
24
25
|
import { deleteDesign } from "./tools/delete-design.js";
|
|
@@ -433,6 +434,21 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
433
434
|
required: ["name"],
|
|
434
435
|
},
|
|
435
436
|
},
|
|
437
|
+
{
|
|
438
|
+
name: "duplicate_design",
|
|
439
|
+
description: "Copy a whole design into a new one: artboard size, background, every node, group and instance. " +
|
|
440
|
+
"Node ids are kept, so ids read from the original apply to the copy. Use it to start a variant of a page, " +
|
|
441
|
+
"or a new page that reuses an existing layout. The copy starts with no pending changes.",
|
|
442
|
+
inputSchema: {
|
|
443
|
+
type: "object",
|
|
444
|
+
properties: {
|
|
445
|
+
design_id: { type: "string", description: "Id of the design to copy." },
|
|
446
|
+
name: { type: "string", description: "Name of the copy (default: the original name followed by \"(copy)\")." },
|
|
447
|
+
group: { type: ["string", "null"], description: `Page group of the copy (default: the original's group). ${GROUP_PATH_DOC}` },
|
|
448
|
+
},
|
|
449
|
+
required: ["design_id"],
|
|
450
|
+
},
|
|
451
|
+
},
|
|
436
452
|
{
|
|
437
453
|
name: "get_design",
|
|
438
454
|
description: "Read a design. format=summary (default) gives a compact inventory of the nodes; format=json gives the exact values needed to build an update_nodes patch. Never acknowledges pending user edits — use get_changes for that.",
|
|
@@ -552,7 +568,10 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
552
568
|
description: "Copy elements or whole groups. A group is copied with everything inside it, and every copy gets fresh ids. " +
|
|
553
569
|
"Use it for a one-off copy that is MEANT TO DIVERGE. For a block repeated identically, do not copy it: " +
|
|
554
570
|
"instantiate it instead (add_nodes with type \"instance\"), so a single edit updates every occurrence. " +
|
|
555
|
-
"Copies land next to the originals by default; use offset_x/offset_y to place them, count to lay out a series, and parent_id to drop them into another group."
|
|
571
|
+
"Copies land next to the originals by default; use offset_x/offset_y to place them, count to lay out a series, and parent_id to drop them into another group. " +
|
|
572
|
+
"Pass target_design_id to copy into ANOTHER design: the copies keep their on-screen position (at the target's root, " +
|
|
573
|
+
"or in parent_id), and any component mirrored by a copied instance is copied along once, at the target's root, " +
|
|
574
|
+
"with the copied instances pointed at it. To copy a whole page, use duplicate_design instead.",
|
|
556
575
|
inputSchema: {
|
|
557
576
|
type: "object",
|
|
558
577
|
properties: {
|
|
@@ -563,12 +582,16 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
563
582
|
description: "Ids of the elements or groups to copy. A node whose ancestor is also listed is ignored: the ancestor already carries it.",
|
|
564
583
|
minItems: 1,
|
|
565
584
|
},
|
|
566
|
-
|
|
567
|
-
|
|
585
|
+
target_design_id: {
|
|
586
|
+
type: "string",
|
|
587
|
+
description: "Design the copies are written into. Omit to copy within design_id.",
|
|
588
|
+
},
|
|
589
|
+
offset_x: { type: "number", description: "Horizontal shift of each copy, in pixels (default 20, 0 into another design). Use the element width plus a gutter to lay copies out in a row." },
|
|
590
|
+
offset_y: { type: "number", description: "Vertical shift of each copy, in pixels (default 20, 0 into another design). Use the element height plus a gutter to stack copies in a column." },
|
|
568
591
|
count: { type: "number", description: `How many copies to make (default 1, max ${MAX_DUPLICATE_COPIES}). Copy n is shifted by n times the offset, which lays out a regular series in one call.` },
|
|
569
592
|
parent_id: {
|
|
570
593
|
type: ["string", "null"],
|
|
571
|
-
description: "Group the copies are dropped into. Their x/y are then read in that group's space. Omit to keep the original's parent, or pass null to place the copies at the root.",
|
|
594
|
+
description: "Group the copies are dropped into, in the target design. Their x/y are then read in that group's space. Omit to keep the original's parent (the root, into another design), or pass null to place the copies at the root.",
|
|
572
595
|
},
|
|
573
596
|
},
|
|
574
597
|
required: ["design_id", "node_ids"],
|
|
@@ -783,6 +806,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
783
806
|
broadcastToGUI({ type: "DESIGNS_UPDATED", data: { designs: store.list() }, timestamp: new Date().toISOString() });
|
|
784
807
|
return { ...result, content: [{ type: "text", text: result.content[0].text + guiHint(result.design.id) }] };
|
|
785
808
|
}
|
|
809
|
+
case "duplicate_design": {
|
|
810
|
+
const result = duplicateDesign(store, params);
|
|
811
|
+
broadcastToGUI({ type: "DESIGNS_UPDATED", data: { designs: store.list() }, timestamp: new Date().toISOString() });
|
|
812
|
+
return { ...result, content: [{ type: "text", text: result.content[0].text + guiHint(result.design.id) }] };
|
|
813
|
+
}
|
|
786
814
|
case "get_design":
|
|
787
815
|
return getDesign(store, params);
|
|
788
816
|
case "update_design": {
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { DesignStore } from "../core/design-store.js";
|
|
2
|
+
import type { CreateDesignResult } from "./create-design.js";
|
|
3
|
+
/** Copie complète d'un design : planche, éléments, groupes et instances. */
|
|
4
|
+
export declare function duplicateDesign(store: DesignStore, args: Record<string, unknown> | undefined): CreateDesignResult;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { requireString, textResult } from "./types.js";
|
|
2
|
+
/** Copie complète d'un design : planche, éléments, groupes et instances. */
|
|
3
|
+
export function duplicateDesign(store, args) {
|
|
4
|
+
const id = requireString(args, "design_id");
|
|
5
|
+
if (args?.name !== undefined && (typeof args.name !== "string" || args.name.trim() === "")) {
|
|
6
|
+
throw new Error(`"name" must be a non-empty string.`);
|
|
7
|
+
}
|
|
8
|
+
const design = store.duplicate(id, {
|
|
9
|
+
name: args?.name,
|
|
10
|
+
group: args?.group === undefined ? undefined : args.group,
|
|
11
|
+
});
|
|
12
|
+
return {
|
|
13
|
+
...textResult(`Design "${id}" copied to "${design.name}" with id "${design.id}" (${design.width}x${design.height}, ` +
|
|
14
|
+
`${design.nodes.length} node(s))${design.group === null ? "" : ` in page group "${design.group}"`}. ` +
|
|
15
|
+
`Node ids are the same as in the original, so ids read from it apply to the copy.`),
|
|
16
|
+
design,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
@@ -2,10 +2,12 @@ import type { DesignStore } from "../core/design-store.js";
|
|
|
2
2
|
import type { MutationResult } from "./add-nodes.js";
|
|
3
3
|
export declare const MAX_DUPLICATE_COPIES = 50;
|
|
4
4
|
/**
|
|
5
|
-
* Copie des éléments — un groupe emporte tout son contenu
|
|
5
|
+
* Copie des éléments — un groupe emporte tout son contenu — dans le même design
|
|
6
|
+
* ou dans un autre.
|
|
6
7
|
*
|
|
7
8
|
* Les identifiants sont réattribués et les liens parent/enfant recâblés sur les
|
|
8
|
-
* copies
|
|
9
|
-
*
|
|
9
|
+
* copies. Vers un autre design, les copies arrivent à la racine à leur position
|
|
10
|
+
* absolue d'origine (ou dans `parent_id`), et les composants reproduits par les
|
|
11
|
+
* instances copiées sont copiés avec elles, une seule fois, à la racine.
|
|
10
12
|
*/
|
|
11
13
|
export declare function duplicateNodes(store: DesignStore, args: Record<string, unknown> | undefined): MutationResult;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { withoutRedundantDescendants } from "../../shared/tree.js";
|
|
2
|
-
import {
|
|
1
|
+
import { computeLayout, spaceOriginOf, withoutRedundantDescendants } from "../../shared/tree.js";
|
|
2
|
+
import { expandInstances } from "../../shared/instances.js";
|
|
3
|
+
import { cloneSubtrees, subtreeNodes } from "../core/clone.js";
|
|
3
4
|
import { DesignError } from "../core/node-factory.js";
|
|
4
5
|
import { requireString, textResult } from "./types.js";
|
|
5
|
-
/** Décalage par défaut d'une copie, en pixels : assez pour la voir, assez peu pour la retrouver. */
|
|
6
|
+
/** Décalage par défaut d'une copie dans le même design, en pixels : assez pour la voir, assez peu pour la retrouver. */
|
|
6
7
|
const DEFAULT_OFFSET = 20;
|
|
7
8
|
export const MAX_DUPLICATE_COPIES = 50;
|
|
8
9
|
function readNumber(args, field, fallback) {
|
|
@@ -14,11 +15,43 @@ function readNumber(args, field, fallback) {
|
|
|
14
15
|
return value;
|
|
15
16
|
}
|
|
16
17
|
/**
|
|
17
|
-
*
|
|
18
|
+
* Composants à copier en plus des sous-arbres demandés : ceux que reproduit une
|
|
19
|
+
* instance copiée alors qu'ils n'ont pas de copie à portée, y compris ceux dont
|
|
20
|
+
* dépendent les composants ainsi ajoutés. Les instances des sous-arbres demandés
|
|
21
|
+
* voient les composants copiés avec elles ; celles d'un composant ajouté ne
|
|
22
|
+
* voient que les autres composants ajoutés.
|
|
23
|
+
*/
|
|
24
|
+
function missingComponents(nodes, roots) {
|
|
25
|
+
const inRoots = new Set(subtreeNodes(nodes, roots).map((node) => node.id));
|
|
26
|
+
const referenced = (scope, reachable) => subtreeNodes(nodes, scope)
|
|
27
|
+
.filter((node) => node.type === "instance" && node.componentId !== undefined && !reachable.has(node.componentId))
|
|
28
|
+
.map((node) => node.componentId);
|
|
29
|
+
const missing = [...new Set(referenced(roots, inRoots))];
|
|
30
|
+
for (;;) {
|
|
31
|
+
const inMissing = new Set(subtreeNodes(nodes, missing).map((node) => node.id));
|
|
32
|
+
const fresh = [...new Set(referenced(missing, inMissing))].filter((id) => !missing.includes(id));
|
|
33
|
+
if (fresh.length === 0)
|
|
34
|
+
return missing;
|
|
35
|
+
missing.push(...fresh);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/** Pointe chaque instance copiée vers la copie de son composant quand elle existe. */
|
|
39
|
+
function remapComponents(copies, ...idMaps) {
|
|
40
|
+
return copies.map((node) => {
|
|
41
|
+
if (node.type !== "instance" || node.componentId === undefined)
|
|
42
|
+
return node;
|
|
43
|
+
const target = idMaps.map((map) => map.get(node.componentId)).find((id) => id !== undefined);
|
|
44
|
+
return target === undefined ? node : { ...node, componentId: target };
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Copie des éléments — un groupe emporte tout son contenu — dans le même design
|
|
49
|
+
* ou dans un autre.
|
|
18
50
|
*
|
|
19
51
|
* Les identifiants sont réattribués et les liens parent/enfant recâblés sur les
|
|
20
|
-
* copies
|
|
21
|
-
*
|
|
52
|
+
* copies. Vers un autre design, les copies arrivent à la racine à leur position
|
|
53
|
+
* absolue d'origine (ou dans `parent_id`), et les composants reproduits par les
|
|
54
|
+
* instances copiées sont copiés avec elles, une seule fois, à la racine.
|
|
22
55
|
*/
|
|
23
56
|
export function duplicateNodes(store, args) {
|
|
24
57
|
const id = requireString(args, "design_id");
|
|
@@ -26,17 +59,21 @@ export function duplicateNodes(store, args) {
|
|
|
26
59
|
if (!Array.isArray(nodeIds) || nodeIds.length === 0) {
|
|
27
60
|
throw new Error(`"node_ids" is required and must be a non-empty array of node ids.`);
|
|
28
61
|
}
|
|
29
|
-
const
|
|
30
|
-
const
|
|
62
|
+
const targetId = args?.target_design_id === undefined ? id : requireString(args, "target_design_id");
|
|
63
|
+
const crossDesign = targetId !== id;
|
|
64
|
+
const source = store.get(id);
|
|
65
|
+
const target = crossDesign ? store.get(targetId) : source;
|
|
66
|
+
const byId = new Map(source.nodes.map((node) => [node.id, node]));
|
|
31
67
|
for (const raw of nodeIds) {
|
|
32
68
|
if (!byId.has(String(raw)))
|
|
33
69
|
throw new DesignError(`Node "${String(raw)}" not found in design "${id}".`);
|
|
34
70
|
}
|
|
35
71
|
// Un élément dont le groupe est aussi listé serait copié deux fois : une fois
|
|
36
72
|
// pour lui-même, une fois avec son groupe.
|
|
37
|
-
const roots = withoutRedundantDescendants(
|
|
38
|
-
const
|
|
39
|
-
const
|
|
73
|
+
const roots = withoutRedundantDescendants(source.nodes, nodeIds.map((raw) => String(raw)));
|
|
74
|
+
const defaultOffset = crossDesign ? 0 : DEFAULT_OFFSET;
|
|
75
|
+
const offsetX = readNumber(args, "offset_x", defaultOffset);
|
|
76
|
+
const offsetY = readNumber(args, "offset_y", defaultOffset);
|
|
40
77
|
const count = readNumber(args, "count", 1);
|
|
41
78
|
if (!Number.isInteger(count) || count < 1 || count > MAX_DUPLICATE_COPIES) {
|
|
42
79
|
throw new DesignError(`"count" must be an integer between 1 and ${MAX_DUPLICATE_COPIES}.`);
|
|
@@ -47,49 +84,69 @@ export function duplicateNodes(store, args) {
|
|
|
47
84
|
parentId = null;
|
|
48
85
|
}
|
|
49
86
|
else {
|
|
50
|
-
const parent =
|
|
87
|
+
const parent = target.nodes.find((node) => node.id === String(args.parent_id));
|
|
51
88
|
if (!parent)
|
|
52
|
-
throw new DesignError(`Group "${String(args.parent_id)}" not found in design "${
|
|
89
|
+
throw new DesignError(`Group "${String(args.parent_id)}" not found in design "${targetId}".`);
|
|
53
90
|
if (parent.type !== "group") {
|
|
54
91
|
throw new DesignError(`"parent_id" must reference a group, but "${parent.id}" is a "${parent.type}".`);
|
|
55
92
|
}
|
|
56
93
|
parentId = parent.id;
|
|
57
94
|
}
|
|
58
95
|
}
|
|
96
|
+
// Vers un autre design, un noeud posé à la racine garde sa position à l'écran :
|
|
97
|
+
// ses coordonnées locales sont ramenées dans l'espace de la planche.
|
|
98
|
+
const layout = crossDesign ? computeLayout(expandInstances(source.nodes)) : undefined;
|
|
99
|
+
const placeAtRoot = (node) => {
|
|
100
|
+
const origin = spaceOriginOf(layout, node.parentId);
|
|
101
|
+
return { ...node, parentId: null, x: node.x + origin.x, y: node.y + origin.y };
|
|
102
|
+
};
|
|
59
103
|
const copies = [];
|
|
60
104
|
const createdRootIds = [];
|
|
105
|
+
const components = crossDesign ? withoutRedundantDescendants(source.nodes, missingComponents(source.nodes, roots)) : [];
|
|
106
|
+
const componentClone = cloneSubtrees(source.nodes, components);
|
|
107
|
+
const componentRoots = new Set(componentClone.rootIds);
|
|
108
|
+
copies.push(...remapComponents(componentClone.copies, componentClone.idMap).map((copy) => componentRoots.has(copy.id) ? placeAtRoot(copy) : copy));
|
|
61
109
|
for (let copyIndex = 1; copyIndex <= count; copyIndex++) {
|
|
62
110
|
// Un sous-arbre complet par copie : les ids sont neufs et les liens
|
|
63
111
|
// parent/enfant recables (cf. src/core/clone.ts).
|
|
64
|
-
const clone = cloneSubtrees(
|
|
112
|
+
const clone = cloneSubtrees(source.nodes, roots);
|
|
65
113
|
const rootSet = new Set(clone.rootIds);
|
|
66
|
-
|
|
114
|
+
const cloned = crossDesign ? remapComponents(clone.copies, clone.idMap, componentClone.idMap) : clone.copies;
|
|
115
|
+
for (const copy of cloned) {
|
|
67
116
|
// Seules les racines sont decalees et reparentees : les descendants sont
|
|
68
117
|
// exprimes dans l'espace de leur parent et suivent donc tout seuls.
|
|
69
118
|
if (!rootSet.has(copy.id)) {
|
|
70
119
|
copies.push(copy);
|
|
71
120
|
continue;
|
|
72
121
|
}
|
|
122
|
+
const placed = crossDesign && (parentId === undefined || parentId === null) ? placeAtRoot(copy) : copy;
|
|
73
123
|
copies.push({
|
|
74
|
-
...
|
|
75
|
-
parentId: parentId === undefined ?
|
|
76
|
-
x:
|
|
77
|
-
y:
|
|
124
|
+
...placed,
|
|
125
|
+
parentId: parentId === undefined ? placed.parentId : parentId,
|
|
126
|
+
x: placed.x + offsetX * copyIndex,
|
|
127
|
+
y: placed.y + offsetY * copyIndex,
|
|
78
128
|
});
|
|
79
129
|
}
|
|
80
130
|
createdRootIds.push(...clone.rootIds);
|
|
81
131
|
}
|
|
82
|
-
const design = store.mutate(
|
|
132
|
+
const design = store.mutate(targetId, "agent", (draft) => {
|
|
83
133
|
draft.nodes.push(...copies);
|
|
84
134
|
});
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
135
|
+
const describe = (node) => `- ${node.id} — "${node.name}" (${node.type}) at ${Math.round(node.x)},${Math.round(node.y)}`;
|
|
136
|
+
const lines = copies.filter((node) => createdRootIds.includes(node.id)).map(describe);
|
|
137
|
+
const componentLines = copies.filter((node) => componentRoots.has(node.id)).map(describe);
|
|
138
|
+
const from = crossDesign ? ` from "${source.name}" into "${design.name}"` : ` in "${design.name}"`;
|
|
88
139
|
return {
|
|
89
140
|
...textResult([
|
|
90
|
-
`Copied ${roots.length} node(s)${count > 1 ? ` ${count} times` : ""}
|
|
141
|
+
`Copied ${roots.length} node(s)${count > 1 ? ` ${count} times` : ""}${from} ` +
|
|
91
142
|
`(${copies.length} node(s) created, content included). New top-level copies:`,
|
|
92
143
|
...lines,
|
|
144
|
+
...(componentLines.length > 0
|
|
145
|
+
? [
|
|
146
|
+
`Also copied ${componentLines.length} component(s) the copied instances mirror, now pointed at by those instances:`,
|
|
147
|
+
...componentLines,
|
|
148
|
+
]
|
|
149
|
+
: []),
|
|
93
150
|
].join("\n")),
|
|
94
151
|
design,
|
|
95
152
|
};
|
package/dist/src/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const APP_VERSION = "1.0.
|
|
1
|
+
export declare const APP_VERSION = "1.0.10";
|
package/dist/src/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Auto-generated version file - do not edit manually
|
|
2
|
-
export const APP_VERSION = '1.0.
|
|
2
|
+
export const APP_VERSION = '1.0.10';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@imenam/mcp-design",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.10",
|
|
4
4
|
"main": "dist/src/index.js",
|
|
5
5
|
"directories": {
|
|
6
6
|
"test": "test"
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
],
|
|
46
46
|
"dependencies": {
|
|
47
47
|
"@hono/node-server": "^2.1.1",
|
|
48
|
-
"@imenam/mcp-gui-interface": "^1.0.
|
|
48
|
+
"@imenam/mcp-gui-interface": "^1.0.14",
|
|
49
49
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
50
50
|
"@resvg/resvg-js": "^2.6.2",
|
|
51
51
|
"dotenv": "^17.4.2",
|