@imenam/mcp-design 1.0.6 → 1.0.8
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 +44 -6
- package/dist/shared/node-defaults.d.ts +1 -1
- package/dist/shared/node-defaults.js +9 -3
- package/dist/shared/node-fields.js +6 -2
- package/dist/shared/render.d.ts +18 -0
- package/dist/shared/render.js +22 -0
- package/dist/src/cli/setup-mcp.d.ts +20 -1
- package/dist/src/cli/setup-mcp.js +64 -6
- package/dist/src/core/node-factory.js +15 -0
- package/dist/src/gui-worker.js +71 -23
- package/dist/src/index.js +45 -21
- package/dist/src/tools/export-design.d.ts +2 -2
- package/dist/src/tools/export-design.js +28 -9
- package/dist/src/tools/relay.d.ts +27 -0
- package/dist/src/tools/relay.js +67 -0
- package/dist/src/tools/screenshot-design.js +14 -8
- package/dist/src/tools/types.d.ts +20 -1
- package/dist/src/version.d.ts +1 -1
- package/dist/src/version.js +1 -1
- package/gui/dist/assets/index-DBZNXgF_.js +10 -0
- package/gui/dist/assets/{search-C-KcHHKM.js → search-CSwiX0lv.js} +1 -1
- package/gui/dist/index.html +1 -1
- package/package.json +60 -60
- package/scripts/generate-icons.js +85 -85
- package/gui/dist/assets/index-B-q92Pfu.js +0 -10
package/dist/src/index.js
CHANGED
|
@@ -34,6 +34,7 @@ import { getChanges } from "./tools/get-changes.js";
|
|
|
34
34
|
import { markSynced } from "./tools/mark-synced.js";
|
|
35
35
|
import { exportDesignTool } from "./tools/export-design.js";
|
|
36
36
|
import { screenshotDesignTool } from "./tools/screenshot-design.js";
|
|
37
|
+
import { RELAY_SHAPE_DESCRIPTIONS } from "./tools/relay.js";
|
|
37
38
|
import { reorderNodesTool } from "./tools/reorder-nodes.js";
|
|
38
39
|
import { searchIconsTool, DEFAULT_ICON_RESULTS, MAX_ICON_RESULTS } from "./tools/search-icons.js";
|
|
39
40
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -41,8 +42,14 @@ const __dirname = path.dirname(__filename);
|
|
|
41
42
|
// Les flags CLI sont traités avant toute initialisation : ils s'exécutent dans le
|
|
42
43
|
// répertoire de l'utilisateur, pas dans un serveur MCP.
|
|
43
44
|
if (process.argv.slice(2).includes("--claude-setup-mcp")) {
|
|
44
|
-
const
|
|
45
|
-
|
|
45
|
+
const argv = process.argv.slice(2);
|
|
46
|
+
const { runSetupMcp, parseGuiExposure } = await import("./cli/setup-mcp.js");
|
|
47
|
+
const parsed = parseGuiExposure(argv);
|
|
48
|
+
if ("error" in parsed) {
|
|
49
|
+
console.error(`Erreur : ${parsed.error}`);
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
process.exit(runSetupMcp({ cwd: process.cwd(), force: argv.includes("--force"), exposure: parsed.exposure }));
|
|
46
53
|
}
|
|
47
54
|
function findProjectRoot(startDir) {
|
|
48
55
|
let current = startDir;
|
|
@@ -62,7 +69,7 @@ const logger = createLogger("mcp-design");
|
|
|
62
69
|
function getConfig() {
|
|
63
70
|
if (fs.existsSync(packageEnvPath))
|
|
64
71
|
dotenv.config({ path: packageEnvPath, override: true });
|
|
65
|
-
return { proxyUrl: process.env.PROXY_URL, version: APP_VERSION };
|
|
72
|
+
return { proxyUrl: process.env.PROXY_URL, appPort: process.env.APP_PORT, version: APP_VERSION };
|
|
66
73
|
}
|
|
67
74
|
getConfig();
|
|
68
75
|
const store = new DesignStore(rootDir);
|
|
@@ -224,9 +231,13 @@ guiLauncher = new GuiLauncher({
|
|
|
224
231
|
maxRestarts: 0,
|
|
225
232
|
onMessage: handleIPCMessage,
|
|
226
233
|
});
|
|
234
|
+
/** La GUI a besoin soit d'un proxy, soit d'un port local (mode standalone). */
|
|
235
|
+
function guiIsConfigured() {
|
|
236
|
+
return !!(process.env.PROXY_URL?.trim() || process.env.APP_PORT?.trim());
|
|
237
|
+
}
|
|
227
238
|
function launchGUI() {
|
|
228
|
-
if (!
|
|
229
|
-
logger.info("[MASTER] PROXY_URL
|
|
239
|
+
if (!guiIsConfigured()) {
|
|
240
|
+
logger.info("[MASTER] Ni PROXY_URL ni APP_PORT défini — GUI désactivée.");
|
|
230
241
|
guiStatus = "disabled";
|
|
231
242
|
return;
|
|
232
243
|
}
|
|
@@ -234,13 +245,13 @@ function launchGUI() {
|
|
|
234
245
|
startGuiWorker();
|
|
235
246
|
}
|
|
236
247
|
async function reconnectGui(args) {
|
|
237
|
-
getConfig(); // relit le .env au cas où PROXY_URL aurait été ajouté après le démarrage
|
|
238
|
-
if (!
|
|
248
|
+
getConfig(); // relit le .env au cas où PROXY_URL/APP_PORT aurait été ajouté après le démarrage
|
|
249
|
+
if (!guiIsConfigured()) {
|
|
239
250
|
return {
|
|
240
251
|
content: [
|
|
241
252
|
{
|
|
242
253
|
type: "text",
|
|
243
|
-
text: "Error: PROXY_URL is
|
|
254
|
+
text: "Error: neither PROXY_URL nor APP_PORT is set — the GUI is disabled. Add PROXY_URL (to expose the GUI through the proxy) or APP_PORT (to serve it directly on localhost) to the .env file, then call reconnect_gui again.",
|
|
244
255
|
},
|
|
245
256
|
],
|
|
246
257
|
isError: true,
|
|
@@ -253,13 +264,13 @@ async function reconnectGui(args) {
|
|
|
253
264
|
if (guiStatus === "starting") {
|
|
254
265
|
return {
|
|
255
266
|
content: [
|
|
256
|
-
{ type: "text", text: "GUI worker is already starting
|
|
267
|
+
{ type: "text", text: "GUI worker is already starting. Wait a few seconds, then call reconnect_gui again if needed." },
|
|
257
268
|
],
|
|
258
269
|
};
|
|
259
270
|
}
|
|
260
271
|
return {
|
|
261
272
|
content: [
|
|
262
|
-
{ type: "text", text: `GUI is already
|
|
273
|
+
{ type: "text", text: `GUI is already running${guiUrl ? ` at ${guiUrl}` : ""}. Nothing to do (use force=true to restart it anyway).` },
|
|
263
274
|
],
|
|
264
275
|
};
|
|
265
276
|
}
|
|
@@ -285,23 +296,25 @@ async function reconnectGui(args) {
|
|
|
285
296
|
};
|
|
286
297
|
newChild.on("exit", onExit);
|
|
287
298
|
});
|
|
299
|
+
const proxyUrl = process.env.PROXY_URL?.trim();
|
|
288
300
|
if (result.outcome === "ready") {
|
|
289
|
-
return { content: [{ type: "text", text: `GUI reconnected
|
|
301
|
+
return { content: [{ type: "text", text: `GUI reconnected. URL: ${result.url}` }] };
|
|
290
302
|
}
|
|
291
303
|
if (result.outcome === "already_running") {
|
|
292
|
-
return { content: [{ type: "text", text: `GUI is already
|
|
304
|
+
return { content: [{ type: "text", text: `GUI is already served by another MCP instance. URL: ${result.url ?? "unknown"}` }] };
|
|
293
305
|
}
|
|
294
306
|
if (result.outcome === "exited") {
|
|
307
|
+
const hint = proxyUrl
|
|
308
|
+
? `The proxy server is probably still unreachable at ${proxyUrl}. Start the proxy server, then call reconnect_gui again.`
|
|
309
|
+
: `Port ${process.env.APP_PORT} is probably already taken by another process. Free it or set another APP_PORT, then call reconnect_gui again.`;
|
|
295
310
|
return {
|
|
296
|
-
content: [
|
|
297
|
-
{ type: "text", text: `GUI worker exited before completing registration (code=${result.code}). The proxy server is probably still unreachable at ${process.env.PROXY_URL}. Start the proxy server, then call reconnect_gui again.` },
|
|
298
|
-
],
|
|
311
|
+
content: [{ type: "text", text: `GUI worker exited before it was ready (code=${result.code}). ${hint}` }],
|
|
299
312
|
isError: true,
|
|
300
313
|
};
|
|
301
314
|
}
|
|
302
315
|
return {
|
|
303
316
|
content: [
|
|
304
|
-
{ type: "text", text: `GUI worker started but did not confirm
|
|
317
|
+
{ type: "text", text: `GUI worker started but did not confirm it was ready within 15s. Check the logs (${resolveLogDir(rootDir)}), then retry.` },
|
|
305
318
|
],
|
|
306
319
|
isError: true,
|
|
307
320
|
};
|
|
@@ -323,6 +336,8 @@ const NODE_FIELDS_DOC = "Every length is in PIXELS (x, y, width, height, strokeW
|
|
|
323
336
|
"Fields accepted per type — rect: fill, stroke, strokeWidth, radius; ellipse: fill, stroke, strokeWidth; " +
|
|
324
337
|
"line: stroke, strokeWidth (the line goes from (x, y) to (x + width, y + height)); " +
|
|
325
338
|
"text: text, color, fontSize, fontFamily, fontWeight, fontStyle, textAlign, lineHeight; " +
|
|
339
|
+
"a text node given no height takes the exact height of its text, so its first line starts at y — " +
|
|
340
|
+
"pass a height only to centre a label inside a container (same y and height as the container); " +
|
|
326
341
|
"icon: icon (required — the name of the drawing), fill, stroke, strokeWidth; " +
|
|
327
342
|
"instance: componentId (required — the id of the group it mirrors); nothing else, its look comes from that group. " +
|
|
328
343
|
"Any field left out falls back to a sensible default, so a node can be created from its type alone.";
|
|
@@ -676,13 +691,19 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
676
691
|
},
|
|
677
692
|
{
|
|
678
693
|
name: "export_design",
|
|
679
|
-
description: "Render a design for integration. html = absolutely positioned divs + CSS (best starting point for real code), jsx = the same as a React component, svg = faithful preview, json = raw nodes, summary = textual inventory. Provide
|
|
694
|
+
description: "Render a design for integration. html = absolutely positioned divs + CSS (best starting point for real code), jsx = the same as a React component, svg = faithful preview, json = raw nodes, summary = textual inventory. Provide destination_path and filename to write a file on the agent machine instead of returning the content.",
|
|
680
695
|
inputSchema: {
|
|
681
696
|
type: "object",
|
|
682
697
|
properties: {
|
|
683
698
|
design_id: { type: "string", description: DESIGN_ID_DOC },
|
|
684
699
|
format: { type: "string", enum: [...EXPORT_FORMATS], description: "Output format (default html)." },
|
|
685
|
-
|
|
700
|
+
destination_path: {
|
|
701
|
+
type: "string",
|
|
702
|
+
description: `${RELAY_SHAPE_DESCRIPTIONS.destination_path}. When omitted, the content is returned directly instead of being written.`,
|
|
703
|
+
},
|
|
704
|
+
filename: { type: "string", description: `${RELAY_SHAPE_DESCRIPTIONS.filename}. Required when destination_path is provided.` },
|
|
705
|
+
destination_overwrite: { type: "boolean", description: RELAY_SHAPE_DESCRIPTIONS.destination_overwrite },
|
|
706
|
+
destination_relay: { type: "boolean", description: RELAY_SHAPE_DESCRIPTIONS.destination_relay },
|
|
686
707
|
},
|
|
687
708
|
required: ["design_id"],
|
|
688
709
|
},
|
|
@@ -691,7 +712,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
691
712
|
name: "screenshot_design",
|
|
692
713
|
description: "Render a design to a PNG image and look at it. Use it to check what the design actually looks like — proportions, overlaps, contrast, alignment — instead of inferring it from coordinates. " +
|
|
693
714
|
"Pass node_id to frame the capture on a single group (or any element) rather than the whole artboard. " +
|
|
694
|
-
"The image is returned inline; provide
|
|
715
|
+
"The image is returned inline; provide destination_path and filename to write a .png file on the agent machine instead.",
|
|
695
716
|
inputSchema: {
|
|
696
717
|
type: "object",
|
|
697
718
|
properties: {
|
|
@@ -708,10 +729,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
708
729
|
type: "number",
|
|
709
730
|
description: "Margin in design pixels around the captured node (default 16, ignored for a full-artboard capture).",
|
|
710
731
|
},
|
|
711
|
-
|
|
732
|
+
destination_path: {
|
|
712
733
|
type: "string",
|
|
713
|
-
description:
|
|
734
|
+
description: `${RELAY_SHAPE_DESCRIPTIONS.destination_path}. When omitted, the image is returned inline instead of being written.`,
|
|
714
735
|
},
|
|
736
|
+
filename: { type: "string", description: "Name of the .png file to create. Required when destination_path is provided. Must not contain path separators." },
|
|
737
|
+
destination_overwrite: { type: "boolean", description: RELAY_SHAPE_DESCRIPTIONS.destination_overwrite },
|
|
738
|
+
destination_relay: { type: "boolean", description: RELAY_SHAPE_DESCRIPTIONS.destination_relay },
|
|
715
739
|
},
|
|
716
740
|
required: ["design_id"],
|
|
717
741
|
},
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import type { DesignStore } from "../core/design-store.js";
|
|
2
|
-
import { type
|
|
3
|
-
export declare function exportDesignTool(store: DesignStore, args: Record<string, unknown> | undefined):
|
|
2
|
+
import { type FileToolResult } from "./types.js";
|
|
3
|
+
export declare function exportDesignTool(store: DesignStore, args: Record<string, unknown> | undefined): FileToolResult;
|
|
@@ -1,7 +1,13 @@
|
|
|
1
|
-
import fs from "fs";
|
|
2
|
-
import path from "path";
|
|
3
1
|
import { EXPORT_FORMATS, exportDesign as render } from "../core/export.js";
|
|
2
|
+
import { deliverFile } from "./relay.js";
|
|
4
3
|
import { requireString, textResult } from "./types.js";
|
|
4
|
+
const MIME_BY_FORMAT = {
|
|
5
|
+
html: "text/html",
|
|
6
|
+
jsx: "text/jsx",
|
|
7
|
+
svg: "image/svg+xml",
|
|
8
|
+
json: "application/json",
|
|
9
|
+
summary: "text/plain",
|
|
10
|
+
};
|
|
5
11
|
export function exportDesignTool(store, args) {
|
|
6
12
|
const id = requireString(args, "design_id");
|
|
7
13
|
const format = (args?.format ?? "html");
|
|
@@ -10,12 +16,25 @@ export function exportDesignTool(store, args) {
|
|
|
10
16
|
}
|
|
11
17
|
const design = store.get(id);
|
|
12
18
|
const content = render(design, format);
|
|
13
|
-
const
|
|
14
|
-
if (
|
|
15
|
-
|
|
16
|
-
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
17
|
-
fs.writeFileSync(target, content, "utf-8");
|
|
18
|
-
return textResult(`Design "${design.name}" exported as ${format} to ${target} (${content.length} characters).`);
|
|
19
|
+
const destination = typeof args?.destination_path === "string" ? args.destination_path.trim() : "";
|
|
20
|
+
if (!destination) {
|
|
21
|
+
return textResult(content);
|
|
19
22
|
}
|
|
20
|
-
|
|
23
|
+
// Le fichier part vers la machine de l'agent : ici on ne fait que produire les octets,
|
|
24
|
+
// `deliverFile` décide s'ils s'écrivent sur ce disque ou voyagent jusqu'au relais.
|
|
25
|
+
const blocks = deliverFile(Buffer.from(content, "utf-8"), MIME_BY_FORMAT[format] ?? "text/plain", {
|
|
26
|
+
destination_path: destination,
|
|
27
|
+
filename: requireString(args, "filename"),
|
|
28
|
+
destination_overwrite: args?.destination_overwrite === true,
|
|
29
|
+
destination_relay: args?.destination_relay === true,
|
|
30
|
+
});
|
|
31
|
+
return {
|
|
32
|
+
content: [
|
|
33
|
+
{
|
|
34
|
+
type: "text",
|
|
35
|
+
text: `Design "${design.name}" exported as ${format} (${content.length} characters).`,
|
|
36
|
+
},
|
|
37
|
+
...blocks,
|
|
38
|
+
],
|
|
39
|
+
};
|
|
21
40
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export declare const RELAY_SHAPE_DESCRIPTIONS: {
|
|
2
|
+
readonly destination_path: "ABSOLUTE path of the destination DIRECTORY on the agent machine. It is created if missing, and the file keeps the name given in filename";
|
|
3
|
+
readonly filename: "Name of the file to create, extension included (e.g. 'design.html'). Must not contain path separators";
|
|
4
|
+
readonly destination_overwrite: "Replace the destination file if it already exists. Defaults to false";
|
|
5
|
+
readonly destination_relay: "Filled in by the mcp-http-gateway relay, never by the agent. Signals that the destination lives on another machine and that the file content must travel in the response";
|
|
6
|
+
};
|
|
7
|
+
export interface DeliveryTarget {
|
|
8
|
+
destination_path: string;
|
|
9
|
+
filename: string;
|
|
10
|
+
destination_overwrite?: boolean;
|
|
11
|
+
destination_relay?: boolean;
|
|
12
|
+
}
|
|
13
|
+
import type { ResourceBlock } from "./types.js";
|
|
14
|
+
type ContentBlock = {
|
|
15
|
+
type: "text";
|
|
16
|
+
text: string;
|
|
17
|
+
} | ResourceBlock;
|
|
18
|
+
/**
|
|
19
|
+
* Livre un fichier produit par le MCP, du bon côté de la chaîne.
|
|
20
|
+
*
|
|
21
|
+
* Renvoie les blocs de contenu à faire figurer dans le résultat de l'outil : un bloc
|
|
22
|
+
* `resource` que le relais interceptera et retirera, ou une simple confirmation quand
|
|
23
|
+
* l'écriture a eu lieu ici. Toute impossibilité est une erreur franche : mieux vaut un
|
|
24
|
+
* fichier perdu qu'un fichier écrit sur la mauvaise machine.
|
|
25
|
+
*/
|
|
26
|
+
export declare function deliverFile(data: Buffer, mimeType: string, target: DeliveryTarget): ContentBlock[];
|
|
27
|
+
export {};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
// Un fichier produit par ce MCP n'a aucun moyen d'atteindre la machine de l'agent quand
|
|
4
|
+
// le MCP tourne derrière mcp-http-gateway : le protocole n'offre au serveur aucune
|
|
5
|
+
// primitive pour y écrire. La convention du relais règle ça — dès qu'un outil déclare la
|
|
6
|
+
// paire `destination_path` / `destination_relay`, le relais retient la destination, pose
|
|
7
|
+
// `destination_relay`, et écrit lui-même les octets qu'on lui renvoie.
|
|
8
|
+
//
|
|
9
|
+
// Ici, on ne sait pas si un relais est dans la chaîne : `destination_relay` le dit. Posé,
|
|
10
|
+
// on renvoie le contenu ; absent, les deux machines n'en font qu'une et on écrit nous-même.
|
|
11
|
+
/** Plafond de ce qui peut être encodé dans une réponse d'outil. Le relais applique sa
|
|
12
|
+
* propre limite, configurable ; celle-ci n'existe que pour qu'un fichier démesuré
|
|
13
|
+
* échoue proprement ici plutôt qu'en épuisant la mémoire pendant l'encodage base64. */
|
|
14
|
+
const RELAY_MAX_BYTES = 32 * 1024 * 1024;
|
|
15
|
+
export const RELAY_SHAPE_DESCRIPTIONS = {
|
|
16
|
+
destination_path: "ABSOLUTE path of the destination DIRECTORY on the agent machine. It is created if missing, and the file keeps the name given in filename",
|
|
17
|
+
filename: "Name of the file to create, extension included (e.g. 'design.html'). Must not contain path separators",
|
|
18
|
+
destination_overwrite: "Replace the destination file if it already exists. Defaults to false",
|
|
19
|
+
destination_relay: "Filled in by the mcp-http-gateway relay, never by the agent. Signals that the destination lives on another machine and that the file content must travel in the response",
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Livre un fichier produit par le MCP, du bon côté de la chaîne.
|
|
23
|
+
*
|
|
24
|
+
* Renvoie les blocs de contenu à faire figurer dans le résultat de l'outil : un bloc
|
|
25
|
+
* `resource` que le relais interceptera et retirera, ou une simple confirmation quand
|
|
26
|
+
* l'écriture a eu lieu ici. Toute impossibilité est une erreur franche : mieux vaut un
|
|
27
|
+
* fichier perdu qu'un fichier écrit sur la mauvaise machine.
|
|
28
|
+
*/
|
|
29
|
+
export function deliverFile(data, mimeType, target) {
|
|
30
|
+
const name = path.basename(target.filename);
|
|
31
|
+
if (name !== target.filename || name === "." || name === "..") {
|
|
32
|
+
throw new Error(`filename must be a bare file name, without path separators: ${target.filename}`);
|
|
33
|
+
}
|
|
34
|
+
if (data.length > RELAY_MAX_BYTES) {
|
|
35
|
+
throw new Error(`File too large to transfer: ${data.length} bytes, the limit is ${RELAY_MAX_BYTES}.`);
|
|
36
|
+
}
|
|
37
|
+
// Le relais tourne sur la machine de l'agent et a retenu la destination : il attend les
|
|
38
|
+
// octets, qu'il écrira lui-même. Écrire ici viserait le mauvais disque.
|
|
39
|
+
if (target.destination_relay) {
|
|
40
|
+
return [
|
|
41
|
+
{
|
|
42
|
+
type: "resource",
|
|
43
|
+
resource: {
|
|
44
|
+
uri: `file:///${name}`,
|
|
45
|
+
name,
|
|
46
|
+
mimeType,
|
|
47
|
+
blob: data.toString("base64"),
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
];
|
|
51
|
+
}
|
|
52
|
+
// Pas de relais : le MCP tourne à côté de l'agent, son disque est le bon.
|
|
53
|
+
if (!path.isAbsolute(target.destination_path)) {
|
|
54
|
+
throw new Error(`destination_path must be an absolute directory path: ${target.destination_path}`);
|
|
55
|
+
}
|
|
56
|
+
if (fs.existsSync(target.destination_path) &&
|
|
57
|
+
!fs.statSync(target.destination_path).isDirectory()) {
|
|
58
|
+
throw new Error(`destination_path must be a directory, but a file already exists there: ${target.destination_path}`);
|
|
59
|
+
}
|
|
60
|
+
const written = path.join(target.destination_path, name);
|
|
61
|
+
if (!target.destination_overwrite && fs.existsSync(written)) {
|
|
62
|
+
throw new Error(`Destination already exists: ${written}. Pass destination_overwrite to replace it.`);
|
|
63
|
+
}
|
|
64
|
+
fs.mkdirSync(target.destination_path, { recursive: true });
|
|
65
|
+
fs.writeFileSync(written, data);
|
|
66
|
+
return [{ type: "text", text: `File written to ${written} (${data.length} bytes).` }];
|
|
67
|
+
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import fs from "fs";
|
|
2
|
-
import path from "path";
|
|
3
1
|
import { screenshotDesign } from "../core/screenshot.js";
|
|
2
|
+
import { deliverFile } from "./relay.js";
|
|
4
3
|
import { requireString } from "./types.js";
|
|
5
4
|
/** Marge par défaut autour d'un noeud capturé : de l'air, sans noyer l'élément. */
|
|
6
5
|
const NODE_PADDING = 16;
|
|
@@ -20,12 +19,19 @@ export function screenshotDesignTool(store, args) {
|
|
|
20
19
|
const subject = node ? `${node.type} "${node.name}" (${node.id})` : `design "${design.name}"`;
|
|
21
20
|
const geometry = `${Math.round(shot.sourceWidth)}x${Math.round(shot.sourceHeight)} design px ` +
|
|
22
21
|
`rendered at ${shot.width}x${shot.height} px (scale ${Math.round(shot.scale * 100) / 100})`;
|
|
23
|
-
const
|
|
24
|
-
if (
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
22
|
+
const destination = typeof args?.destination_path === "string" ? args.destination_path.trim() : "";
|
|
23
|
+
if (destination) {
|
|
24
|
+
// Livrer le PNG et le montrer à la fois n'aurait pas de sens : le relais retire le
|
|
25
|
+
// contenu du résultat, et une image de plus dans le contexte coûte cher pour rien.
|
|
26
|
+
const blocks = deliverFile(shot.png, "image/png", {
|
|
27
|
+
destination_path: destination,
|
|
28
|
+
filename: requireString(args, "filename"),
|
|
29
|
+
destination_overwrite: args?.destination_overwrite === true,
|
|
30
|
+
destination_relay: args?.destination_relay === true,
|
|
31
|
+
});
|
|
32
|
+
return {
|
|
33
|
+
content: [{ type: "text", text: `Screenshot of ${subject} — ${geometry}.` }, ...blocks],
|
|
34
|
+
};
|
|
29
35
|
}
|
|
30
36
|
return {
|
|
31
37
|
content: [
|
|
@@ -20,6 +20,25 @@ export interface ImageToolResult {
|
|
|
20
20
|
type: "image";
|
|
21
21
|
data: string;
|
|
22
22
|
mimeType: string;
|
|
23
|
-
}>;
|
|
23
|
+
} | ResourceBlock>;
|
|
24
|
+
isError?: boolean;
|
|
25
|
+
}
|
|
26
|
+
/** Bloc qu'un outil renvoie pour un fichier que le relais du gateway écrira chez l'agent. */
|
|
27
|
+
export interface ResourceBlock {
|
|
28
|
+
type: "resource";
|
|
29
|
+
resource: {
|
|
30
|
+
uri: string;
|
|
31
|
+
name: string;
|
|
32
|
+
mimeType: string;
|
|
33
|
+
blob: string;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/** Variante de ToolResult pour les outils qui peuvent livrer un fichier. */
|
|
37
|
+
export interface FileToolResult {
|
|
38
|
+
[key: string]: unknown;
|
|
39
|
+
content: Array<{
|
|
40
|
+
type: "text";
|
|
41
|
+
text: string;
|
|
42
|
+
} | ResourceBlock>;
|
|
24
43
|
isError?: boolean;
|
|
25
44
|
}
|
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.8";
|
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.8';
|