@imenam/mcp-design 1.0.1
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 +231 -0
- package/dist/shared/node-defaults.d.ts +30 -0
- package/dist/shared/node-defaults.js +67 -0
- package/dist/shared/node-fields.d.ts +33 -0
- package/dist/shared/node-fields.js +40 -0
- package/dist/shared/render.d.ts +105 -0
- package/dist/shared/render.js +215 -0
- package/dist/shared/schemas/design.schema.d.ts +231 -0
- package/dist/shared/schemas/design.schema.js +98 -0
- package/dist/shared/tree.d.ts +93 -0
- package/dist/shared/tree.js +223 -0
- package/dist/src/cli/setup-mcp.d.ts +5 -0
- package/dist/src/cli/setup-mcp.js +87 -0
- package/dist/src/core/design-store.d.ts +61 -0
- package/dist/src/core/design-store.js +294 -0
- package/dist/src/core/diff.d.ts +22 -0
- package/dist/src/core/diff.js +97 -0
- package/dist/src/core/export.d.ts +6 -0
- package/dist/src/core/export.js +251 -0
- package/dist/src/core/hierarchy.d.ts +18 -0
- package/dist/src/core/hierarchy.js +68 -0
- package/dist/src/core/ids.d.ts +4 -0
- package/dist/src/core/ids.js +21 -0
- package/dist/src/core/log-dir.d.ts +6 -0
- package/dist/src/core/log-dir.js +12 -0
- package/dist/src/core/migrate.d.ts +18 -0
- package/dist/src/core/migrate.js +54 -0
- package/dist/src/core/node-factory.d.ts +19 -0
- package/dist/src/core/node-factory.js +103 -0
- package/dist/src/core/screenshot.d.ts +31 -0
- package/dist/src/core/screenshot.js +53 -0
- package/dist/src/gui-worker.d.ts +1 -0
- package/dist/src/gui-worker.js +334 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.js +673 -0
- package/dist/src/tools/add-nodes.d.ts +7 -0
- package/dist/src/tools/add-nodes.js +26 -0
- package/dist/src/tools/create-design.d.ts +7 -0
- package/dist/src/tools/create-design.js +15 -0
- package/dist/src/tools/delete-design.d.ts +3 -0
- package/dist/src/tools/delete-design.js +7 -0
- package/dist/src/tools/delete-nodes.d.ts +3 -0
- package/dist/src/tools/delete-nodes.js +29 -0
- package/dist/src/tools/export-design.d.ts +3 -0
- package/dist/src/tools/export-design.js +21 -0
- package/dist/src/tools/get-changes.d.ts +11 -0
- package/dist/src/tools/get-changes.js +27 -0
- package/dist/src/tools/get-design.d.ts +7 -0
- package/dist/src/tools/get-design.js +29 -0
- package/dist/src/tools/group-nodes.d.ts +10 -0
- package/dist/src/tools/group-nodes.js +83 -0
- package/dist/src/tools/list-designs.d.ts +3 -0
- package/dist/src/tools/list-designs.js +13 -0
- package/dist/src/tools/mark-synced.d.ts +7 -0
- package/dist/src/tools/mark-synced.js +19 -0
- package/dist/src/tools/reorder-nodes.d.ts +7 -0
- package/dist/src/tools/reorder-nodes.js +60 -0
- package/dist/src/tools/screenshot-design.d.ts +3 -0
- package/dist/src/tools/screenshot-design.js +36 -0
- package/dist/src/tools/types.d.ts +25 -0
- package/dist/src/tools/types.js +13 -0
- package/dist/src/tools/ungroup-nodes.d.ts +7 -0
- package/dist/src/tools/ungroup-nodes.js +64 -0
- package/dist/src/tools/update-design.d.ts +4 -0
- package/dist/src/tools/update-design.js +33 -0
- package/dist/src/tools/update-nodes.d.ts +3 -0
- package/dist/src/tools/update-nodes.js +35 -0
- package/dist/src/version.d.ts +1 -0
- package/dist/src/version.js +2 -0
- package/package.json +58 -0
- package/scripts/generate-version.js +20 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { buildNode } from "../core/node-factory.js";
|
|
2
|
+
import { requireString, textResult } from "./types.js";
|
|
3
|
+
export function addNodes(store, args) {
|
|
4
|
+
const id = requireString(args, "design_id");
|
|
5
|
+
const input = args?.nodes;
|
|
6
|
+
if (!Array.isArray(input) || input.length === 0) {
|
|
7
|
+
throw new Error(`"nodes" is required and must be a non-empty array.`);
|
|
8
|
+
}
|
|
9
|
+
const current = store.get(id);
|
|
10
|
+
const existingIds = new Set(current.nodes.map((n) => n.id));
|
|
11
|
+
const created = [];
|
|
12
|
+
// Tout est construit avant d'écrire : une entrée invalide n'ajoute rien du tout.
|
|
13
|
+
input.forEach((raw, index) => {
|
|
14
|
+
const node = buildNode(raw, existingIds, current.nodes.length + index + 1);
|
|
15
|
+
existingIds.add(node.id);
|
|
16
|
+
created.push(node);
|
|
17
|
+
});
|
|
18
|
+
const design = store.mutate(id, "agent", (draft) => {
|
|
19
|
+
draft.nodes.push(...created);
|
|
20
|
+
});
|
|
21
|
+
const lines = created.map((n) => `- ${n.id} — "${n.name}" (${n.type}) at ${Math.round(n.x)},${Math.round(n.y)} size ${Math.round(n.width)}x${Math.round(n.height)}`);
|
|
22
|
+
return {
|
|
23
|
+
...textResult([`Added ${created.length} node(s) to "${design.name}":`, ...lines].join("\n")),
|
|
24
|
+
design,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { DesignStore } from "../core/design-store.js";
|
|
2
|
+
import type { Design } from "../../shared/schemas/design.schema.js";
|
|
3
|
+
import { type ToolResult } from "./types.js";
|
|
4
|
+
export interface CreateDesignResult extends ToolResult {
|
|
5
|
+
design?: Design;
|
|
6
|
+
}
|
|
7
|
+
export declare function createDesign(store: DesignStore, args: Record<string, unknown> | undefined): CreateDesignResult;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { textResult, requireString } from "./types.js";
|
|
2
|
+
export function createDesign(store, args) {
|
|
3
|
+
const name = requireString(args, "name");
|
|
4
|
+
const design = store.create({
|
|
5
|
+
name,
|
|
6
|
+
width: args?.width === undefined ? undefined : Number(args.width),
|
|
7
|
+
height: args?.height === undefined ? undefined : Number(args.height),
|
|
8
|
+
background: typeof args?.background === "string" ? args.background : undefined,
|
|
9
|
+
});
|
|
10
|
+
return {
|
|
11
|
+
...textResult(`Design "${design.name}" created with id "${design.id}" (${design.width}x${design.height}, background ${design.background}).\n` +
|
|
12
|
+
`Add content with add_nodes, then open the GUI to let the user refine it.`),
|
|
13
|
+
design,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { requireString, textResult } from "./types.js";
|
|
2
|
+
export function deleteDesign(store, args) {
|
|
3
|
+
const id = requireString(args, "design_id");
|
|
4
|
+
const design = store.get(id);
|
|
5
|
+
store.delete(id);
|
|
6
|
+
return textResult(`Design "${design.name}" (${id}) deleted, along with its ${design.nodes.length} node(s).`);
|
|
7
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { DesignError } from "../core/node-factory.js";
|
|
2
|
+
import { withDescendants } from "../core/hierarchy.js";
|
|
3
|
+
import { requireString, textResult } from "./types.js";
|
|
4
|
+
export function deleteNodes(store, args) {
|
|
5
|
+
const id = requireString(args, "design_id");
|
|
6
|
+
const nodeIds = args?.node_ids;
|
|
7
|
+
if (!Array.isArray(nodeIds) || nodeIds.length === 0) {
|
|
8
|
+
throw new Error(`"node_ids" is required and must be a non-empty array of node ids.`);
|
|
9
|
+
}
|
|
10
|
+
const current = store.get(id);
|
|
11
|
+
const known = new Set(current.nodes.map((n) => n.id));
|
|
12
|
+
const missing = nodeIds.filter((nodeId) => !known.has(String(nodeId)));
|
|
13
|
+
if (missing.length > 0) {
|
|
14
|
+
throw new DesignError(`Node(s) not found in design "${id}": ${missing.join(", ")}.`);
|
|
15
|
+
}
|
|
16
|
+
// Supprimer un groupe emporte son contenu : le laisser derrière lui produirait
|
|
17
|
+
// des noeuds orphelins pointant vers un parent disparu.
|
|
18
|
+
const toRemove = withDescendants(current.nodes, nodeIds.map((nodeId) => String(nodeId)));
|
|
19
|
+
const nested = toRemove.size - nodeIds.length;
|
|
20
|
+
const design = store.mutate(id, "agent", (draft) => {
|
|
21
|
+
draft.nodes = draft.nodes.filter((n) => !toRemove.has(n.id));
|
|
22
|
+
});
|
|
23
|
+
return {
|
|
24
|
+
...textResult(`Deleted ${toRemove.size} node(s) from "${design.name}"` +
|
|
25
|
+
`${nested > 0 ? ` (${nested} of them nested inside a deleted group)` : ""}. ` +
|
|
26
|
+
`${design.nodes.length} node(s) remaining.`),
|
|
27
|
+
design,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { EXPORT_FORMATS, exportDesign as render } from "../core/export.js";
|
|
4
|
+
import { requireString, textResult } from "./types.js";
|
|
5
|
+
export function exportDesignTool(store, args) {
|
|
6
|
+
const id = requireString(args, "design_id");
|
|
7
|
+
const format = (args?.format ?? "html");
|
|
8
|
+
if (!EXPORT_FORMATS.includes(format)) {
|
|
9
|
+
throw new Error(`Unknown format "${String(format)}". Expected one of: ${EXPORT_FORMATS.join(", ")}.`);
|
|
10
|
+
}
|
|
11
|
+
const design = store.get(id);
|
|
12
|
+
const content = render(design, format);
|
|
13
|
+
const outputPath = typeof args?.output_path === "string" ? args.output_path.trim() : "";
|
|
14
|
+
if (outputPath) {
|
|
15
|
+
const target = path.resolve(outputPath);
|
|
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
|
+
}
|
|
20
|
+
return textResult(content);
|
|
21
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { DesignStore } from "../core/design-store.js";
|
|
2
|
+
import { type ToolResult } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Cœur de la boucle agent <-> utilisateur : ce que la GUI (ou l'agent) a changé
|
|
5
|
+
* depuis le dernier `mark_synced`.
|
|
6
|
+
*
|
|
7
|
+
* Lire les changements ne les acquitte jamais : seul `mark_synced` déplace la
|
|
8
|
+
* référence, pour qu'un appel de lecture ne puisse pas faire disparaître un
|
|
9
|
+
* diff que l'agent n'a pas encore intégré.
|
|
10
|
+
*/
|
|
11
|
+
export declare function getChanges(store: DesignStore, args: Record<string, unknown> | undefined): ToolResult;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { diffNodes, formatDiff } from "../core/diff.js";
|
|
2
|
+
import { requireString, textResult } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Cœur de la boucle agent <-> utilisateur : ce que la GUI (ou l'agent) a changé
|
|
5
|
+
* depuis le dernier `mark_synced`.
|
|
6
|
+
*
|
|
7
|
+
* Lire les changements ne les acquitte jamais : seul `mark_synced` déplace la
|
|
8
|
+
* référence, pour qu'un appel de lecture ne puisse pas faire disparaître un
|
|
9
|
+
* diff que l'agent n'a pas encore intégré.
|
|
10
|
+
*/
|
|
11
|
+
export function getChanges(store, args) {
|
|
12
|
+
const id = requireString(args, "design_id");
|
|
13
|
+
const design = store.get(id);
|
|
14
|
+
const diff = diffNodes(design.baseline.nodes, design.nodes);
|
|
15
|
+
const total = diff.added.length + diff.updated.length + diff.removed.length;
|
|
16
|
+
if (total === 0 && !diff.reordered) {
|
|
17
|
+
return textResult(`No change in "${design.name}" since the baseline taken at ${design.baseline.at} (revision ${design.baseline.revision}).`);
|
|
18
|
+
}
|
|
19
|
+
const header = [
|
|
20
|
+
`Changes in "${design.name}" (${id}) since revision ${design.baseline.revision} (${design.baseline.at}).`,
|
|
21
|
+
`Current revision ${design.revision}, last edited by ${design.lastEditor}.`,
|
|
22
|
+
"",
|
|
23
|
+
].join("\n");
|
|
24
|
+
const footer = "\n\nOnce these changes are integrated into the codebase, call mark_synced to move the baseline forward. " +
|
|
25
|
+
"Use export_design (format html or jsx) to get ready-to-adapt markup.";
|
|
26
|
+
return textResult(header + formatDiff(diff) + footer);
|
|
27
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { DesignStore } from "../core/design-store.js";
|
|
2
|
+
import { type ToolResult } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Deux niveaux de détail : `summary` (défaut) pour se repérer sans saturer le
|
|
5
|
+
* contexte, `json` quand l'agent a besoin des valeurs exactes pour patcher.
|
|
6
|
+
*/
|
|
7
|
+
export declare function getDesign(store: DesignStore, args: Record<string, unknown> | undefined): ToolResult;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { toSummary } from "../core/export.js";
|
|
2
|
+
import { countChanges } from "../core/diff.js";
|
|
3
|
+
import { requireString, textResult } from "./types.js";
|
|
4
|
+
/**
|
|
5
|
+
* Deux niveaux de détail : `summary` (défaut) pour se repérer sans saturer le
|
|
6
|
+
* contexte, `json` quand l'agent a besoin des valeurs exactes pour patcher.
|
|
7
|
+
*/
|
|
8
|
+
export function getDesign(store, args) {
|
|
9
|
+
const id = requireString(args, "design_id");
|
|
10
|
+
const format = args?.format === "json" ? "json" : "summary";
|
|
11
|
+
const design = store.get(id);
|
|
12
|
+
const pending = countChanges(design.baseline.nodes, design.nodes);
|
|
13
|
+
const notice = pending > 0
|
|
14
|
+
? `\n\nNOTE: ${pending} node(s) differ from the last synced baseline. Call get_changes to see exactly what the user edited.`
|
|
15
|
+
: "";
|
|
16
|
+
if (format === "json") {
|
|
17
|
+
return textResult(JSON.stringify({
|
|
18
|
+
id: design.id,
|
|
19
|
+
name: design.name,
|
|
20
|
+
width: design.width,
|
|
21
|
+
height: design.height,
|
|
22
|
+
background: design.background,
|
|
23
|
+
revision: design.revision,
|
|
24
|
+
lastEditor: design.lastEditor,
|
|
25
|
+
nodes: design.nodes,
|
|
26
|
+
}, null, 2) + notice);
|
|
27
|
+
}
|
|
28
|
+
return textResult(toSummary(design) + notice);
|
|
29
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { DesignStore } from "../core/design-store.js";
|
|
2
|
+
import type { MutationResult } from "./add-nodes.js";
|
|
3
|
+
/**
|
|
4
|
+
* Regroupe des éléments sous un nouveau groupe.
|
|
5
|
+
*
|
|
6
|
+
* Les éléments doivent partager le même parent : regrouper des éléments situés
|
|
7
|
+
* dans des groupes différents changerait leur ordre d'empilement de façon
|
|
8
|
+
* imprévisible, mieux vaut le refuser que le deviner.
|
|
9
|
+
*/
|
|
10
|
+
export declare function groupNodes(store: DesignStore, args: Record<string, unknown> | undefined): MutationResult;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { boundsOf, computeLayout, descendantIds, spaceOriginOf } from "../../shared/tree.js";
|
|
2
|
+
import { DesignError } from "../core/node-factory.js";
|
|
3
|
+
import { shortId } from "../core/ids.js";
|
|
4
|
+
import { requireString, textResult } from "./types.js";
|
|
5
|
+
/**
|
|
6
|
+
* Regroupe des éléments sous un nouveau groupe.
|
|
7
|
+
*
|
|
8
|
+
* Les éléments doivent partager le même parent : regrouper des éléments situés
|
|
9
|
+
* dans des groupes différents changerait leur ordre d'empilement de façon
|
|
10
|
+
* imprévisible, mieux vaut le refuser que le deviner.
|
|
11
|
+
*/
|
|
12
|
+
export function groupNodes(store, args) {
|
|
13
|
+
const id = requireString(args, "design_id");
|
|
14
|
+
const nodeIds = args?.node_ids;
|
|
15
|
+
if (!Array.isArray(nodeIds) || nodeIds.length === 0) {
|
|
16
|
+
throw new Error(`"node_ids" is required and must be a non-empty array of node ids.`);
|
|
17
|
+
}
|
|
18
|
+
const current = store.get(id);
|
|
19
|
+
const byId = new Map(current.nodes.map((node) => [node.id, node]));
|
|
20
|
+
const targets = nodeIds.map((raw) => {
|
|
21
|
+
const node = byId.get(String(raw));
|
|
22
|
+
if (!node)
|
|
23
|
+
throw new DesignError(`Node "${String(raw)}" not found in design "${id}".`);
|
|
24
|
+
return node;
|
|
25
|
+
});
|
|
26
|
+
const parents = new Set(targets.map((node) => node.parentId));
|
|
27
|
+
if (parents.size > 1) {
|
|
28
|
+
throw new DesignError(`Cannot group nodes that live in different groups. Move them under the same parent first (update_nodes with parentId).`);
|
|
29
|
+
}
|
|
30
|
+
const parentId = targets[0].parentId;
|
|
31
|
+
// Regrouper un groupe et l'un de ses propres descendants n'a pas de sens.
|
|
32
|
+
for (const node of targets) {
|
|
33
|
+
const inside = descendantIds(current.nodes, node.id);
|
|
34
|
+
const nested = targets.find((other) => other.id !== node.id && inside.has(other.id));
|
|
35
|
+
if (nested) {
|
|
36
|
+
throw new DesignError(`Cannot group "${nested.id}" together with its own ancestor "${node.id}".`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const layout = computeLayout(current.nodes);
|
|
40
|
+
const spaceOrigin = spaceOriginOf(layout, parentId);
|
|
41
|
+
const boxes = targets.map((node) => boundsOf(layout, node.id));
|
|
42
|
+
const groupX = Math.min(...boxes.map((box) => box.x)) - spaceOrigin.x;
|
|
43
|
+
const groupY = Math.min(...boxes.map((box) => box.y)) - spaceOrigin.y;
|
|
44
|
+
const groupId = typeof args?.group_id === "string" && args.group_id.trim() !== "" ? args.group_id.trim() : shortId("g");
|
|
45
|
+
if (byId.has(groupId))
|
|
46
|
+
throw new DesignError(`Node id "${groupId}" already exists in this design.`);
|
|
47
|
+
const group = {
|
|
48
|
+
id: groupId,
|
|
49
|
+
type: "group",
|
|
50
|
+
name: typeof args?.name === "string" && args.name.trim() !== "" ? args.name.trim() : "Groupe",
|
|
51
|
+
parentId,
|
|
52
|
+
x: groupX,
|
|
53
|
+
y: groupY,
|
|
54
|
+
width: 0,
|
|
55
|
+
height: 0,
|
|
56
|
+
rotation: 0,
|
|
57
|
+
opacity: 1,
|
|
58
|
+
};
|
|
59
|
+
const moved = new Set(targets.map((node) => node.id));
|
|
60
|
+
const design = store.mutate(id, "agent", (draft) => {
|
|
61
|
+
// Les coordonnées deviennent relatives au groupe : le contenu ne bouge pas à l'écran.
|
|
62
|
+
const rebased = draft.nodes.map((node) => moved.has(node.id) ? { ...node, parentId: groupId, x: node.x - groupX, y: node.y - groupY } : node);
|
|
63
|
+
// Le groupe prend la place du plus haut de ses membres dans l'ordre
|
|
64
|
+
// d'empilement, et ses membres le suivent dans leur ordre relatif d'origine.
|
|
65
|
+
const members = rebased.filter((node) => moved.has(node.id));
|
|
66
|
+
const lastMemberId = members[members.length - 1].id;
|
|
67
|
+
const reordered = [];
|
|
68
|
+
for (const node of rebased) {
|
|
69
|
+
if (!moved.has(node.id)) {
|
|
70
|
+
reordered.push(node);
|
|
71
|
+
}
|
|
72
|
+
else if (node.id === lastMemberId) {
|
|
73
|
+
reordered.push(group, ...members);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
draft.nodes = reordered;
|
|
77
|
+
});
|
|
78
|
+
return {
|
|
79
|
+
...textResult(`Grouped ${targets.length} node(s) into "${group.name}" (${groupId})` +
|
|
80
|
+
`${parentId ? ` inside group ${parentId}` : ""}. The content did not move on screen.`),
|
|
81
|
+
design,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { textResult } from "./types.js";
|
|
2
|
+
export function listDesigns(store) {
|
|
3
|
+
const designs = store.list();
|
|
4
|
+
if (designs.length === 0) {
|
|
5
|
+
return textResult("No design yet. Use create_design to start one.");
|
|
6
|
+
}
|
|
7
|
+
const lines = designs.map((d) => {
|
|
8
|
+
const pending = d.pendingChanges > 0 ? ` — ${d.pendingChanges} unsynced change(s)` : "";
|
|
9
|
+
return (`- ${d.id} — "${d.name}" ${d.width}x${d.height}, ${d.nodeCount} node(s), ` +
|
|
10
|
+
`rev ${d.revision} last edited by ${d.lastEditor} at ${d.updatedAt}${pending}`);
|
|
11
|
+
});
|
|
12
|
+
return textResult([`${designs.length} design(s):`, ...lines].join("\n"));
|
|
13
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { DesignStore } from "../core/design-store.js";
|
|
2
|
+
import type { MutationResult } from "./add-nodes.js";
|
|
3
|
+
/**
|
|
4
|
+
* Seul point d'acquittement des changements. Appelé une fois le design réellement
|
|
5
|
+
* reporté dans le code, jamais "au cas où".
|
|
6
|
+
*/
|
|
7
|
+
export declare function markSynced(store: DesignStore, args: Record<string, unknown> | undefined): MutationResult;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { countChanges } from "../core/diff.js";
|
|
2
|
+
import { requireString, textResult } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Seul point d'acquittement des changements. Appelé une fois le design réellement
|
|
5
|
+
* reporté dans le code, jamais "au cas où".
|
|
6
|
+
*/
|
|
7
|
+
export function markSynced(store, args) {
|
|
8
|
+
const id = requireString(args, "design_id");
|
|
9
|
+
const before = store.get(id);
|
|
10
|
+
const pending = countChanges(before.baseline.nodes, before.nodes);
|
|
11
|
+
const design = store.markSynced(id);
|
|
12
|
+
return {
|
|
13
|
+
...textResult(pending === 0
|
|
14
|
+
? `Baseline of "${design.name}" was already up to date (revision ${design.revision}).`
|
|
15
|
+
: `Baseline of "${design.name}" moved to revision ${design.revision}. ` +
|
|
16
|
+
`${pending} change(s) acknowledged — get_changes now reports nothing until the next edit.`),
|
|
17
|
+
design,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { DesignStore } from "../core/design-store.js";
|
|
2
|
+
import type { MutationResult } from "./add-nodes.js";
|
|
3
|
+
/**
|
|
4
|
+
* Change l'ordre d'empilement d'éléments — l'équivalent agent du panneau de
|
|
5
|
+
* calques de la GUI, dont il partage la logique (`reorderNodes` dans shared/tree).
|
|
6
|
+
*/
|
|
7
|
+
export declare function reorderNodesTool(store: DesignStore, args: Record<string, unknown> | undefined): MutationResult;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { REORDER_DIRECTIONS, childrenByParent, reorderNodes as reorder, } from "../../shared/tree.js";
|
|
2
|
+
import { DesignError } from "../core/node-factory.js";
|
|
3
|
+
import { requireString, textResult } from "./types.js";
|
|
4
|
+
/**
|
|
5
|
+
* Change l'ordre d'empilement d'éléments — l'équivalent agent du panneau de
|
|
6
|
+
* calques de la GUI, dont il partage la logique (`reorderNodes` dans shared/tree).
|
|
7
|
+
*/
|
|
8
|
+
export function reorderNodesTool(store, args) {
|
|
9
|
+
const id = requireString(args, "design_id");
|
|
10
|
+
const nodeIds = args?.node_ids;
|
|
11
|
+
if (!Array.isArray(nodeIds) || nodeIds.length === 0) {
|
|
12
|
+
throw new Error(`"node_ids" is required and must be a non-empty array of node ids.`);
|
|
13
|
+
}
|
|
14
|
+
const direction = args?.position;
|
|
15
|
+
if (!REORDER_DIRECTIONS.includes(direction)) {
|
|
16
|
+
throw new Error(`"position" is required and must be one of: ${REORDER_DIRECTIONS.join(", ")} ` +
|
|
17
|
+
`(received: ${JSON.stringify(args?.position)}).`);
|
|
18
|
+
}
|
|
19
|
+
const current = store.get(id);
|
|
20
|
+
const byId = new Map(current.nodes.map((node) => [node.id, node]));
|
|
21
|
+
const targets = nodeIds.map((raw) => {
|
|
22
|
+
const node = byId.get(String(raw));
|
|
23
|
+
if (!node)
|
|
24
|
+
throw new DesignError(`Node "${String(raw)}" not found in design "${id}".`);
|
|
25
|
+
return node;
|
|
26
|
+
});
|
|
27
|
+
const moved = new Set(targets.map((node) => node.id));
|
|
28
|
+
const before = current.nodes.map((node) => node.id).join(" ");
|
|
29
|
+
const design = store.mutate(id, "agent", (draft) => {
|
|
30
|
+
draft.nodes = reorder(draft.nodes, [...moved], direction);
|
|
31
|
+
});
|
|
32
|
+
if (design.nodes.map((node) => node.id).join(" ") === before) {
|
|
33
|
+
// Le cas est fréquent, et silencieux autrement : un élément déjà au sommet
|
|
34
|
+
// de sa fratrie ne bougera jamais, quel que soit le nombre d'appels.
|
|
35
|
+
const edge = direction === "back" || direction === "backward" ? "bottom" : "top";
|
|
36
|
+
const subject = targets.length === 1 ? `"${targets[0].name}" (${targets[0].id}) is` : "those nodes are";
|
|
37
|
+
return {
|
|
38
|
+
...textResult(`Nothing moved: ${subject} already at the ${edge} of their sibling order. ` +
|
|
39
|
+
`Stacking is relative to siblings only — to move an element past the content of another group, ` +
|
|
40
|
+
`reorder that group itself, or reparent the element with update_nodes.`),
|
|
41
|
+
design,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
// L'ordre final, fratrie par fratrie : évite à l'agent un get_design de contrôle.
|
|
45
|
+
const siblings = childrenByParent(design.nodes);
|
|
46
|
+
const parents = new Set(design.nodes.filter((node) => moved.has(node.id)).map((node) => node.parentId));
|
|
47
|
+
const lines = [...parents].map((parentId) => {
|
|
48
|
+
const order = (siblings.get(parentId) ?? [])
|
|
49
|
+
.map((node) => (moved.has(node.id) ? `[${node.name}]` : node.name))
|
|
50
|
+
.join(" then ");
|
|
51
|
+
return `- ${parentId === null ? "root level" : `inside group ${parentId}`}, bottom to top: ${order}`;
|
|
52
|
+
});
|
|
53
|
+
return {
|
|
54
|
+
...textResult([
|
|
55
|
+
`Moved ${targets.length} node(s) ${direction} in "${design.name}" (moved nodes in brackets):`,
|
|
56
|
+
...lines,
|
|
57
|
+
].join("\n")),
|
|
58
|
+
design,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { screenshotDesign } from "../core/screenshot.js";
|
|
4
|
+
import { requireString } from "./types.js";
|
|
5
|
+
/** Marge par défaut autour d'un noeud capturé : de l'air, sans noyer l'élément. */
|
|
6
|
+
const NODE_PADDING = 16;
|
|
7
|
+
export function screenshotDesignTool(store, args) {
|
|
8
|
+
const id = requireString(args, "design_id");
|
|
9
|
+
const design = store.get(id);
|
|
10
|
+
const nodeId = typeof args?.node_id === "string" && args.node_id.trim() !== "" ? args.node_id.trim() : undefined;
|
|
11
|
+
const node = nodeId ? design.nodes.find((candidate) => candidate.id === nodeId) : undefined;
|
|
12
|
+
if (nodeId && !node) {
|
|
13
|
+
throw new Error(`Node "${nodeId}" not found in design "${design.name}" (${design.id}).`);
|
|
14
|
+
}
|
|
15
|
+
const shot = screenshotDesign(design, {
|
|
16
|
+
nodeId,
|
|
17
|
+
scale: args?.scale === undefined ? undefined : Number(args.scale),
|
|
18
|
+
padding: args?.padding === undefined ? (nodeId ? NODE_PADDING : 0) : Number(args.padding),
|
|
19
|
+
});
|
|
20
|
+
const subject = node ? `${node.type} "${node.name}" (${node.id})` : `design "${design.name}"`;
|
|
21
|
+
const geometry = `${Math.round(shot.sourceWidth)}x${Math.round(shot.sourceHeight)} design px ` +
|
|
22
|
+
`rendered at ${shot.width}x${shot.height} px (scale ${Math.round(shot.scale * 100) / 100})`;
|
|
23
|
+
const outputPath = typeof args?.output_path === "string" ? args.output_path.trim() : "";
|
|
24
|
+
if (outputPath) {
|
|
25
|
+
const target = path.resolve(outputPath);
|
|
26
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
27
|
+
fs.writeFileSync(target, shot.png);
|
|
28
|
+
return { content: [{ type: "text", text: `Screenshot of ${subject} written to ${target} — ${geometry}.` }] };
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
content: [
|
|
32
|
+
{ type: "text", text: `Screenshot of ${subject} — ${geometry}, revision ${design.revision}.` },
|
|
33
|
+
{ type: "image", data: shot.png.toString("base64"), mimeType: "image/png" },
|
|
34
|
+
],
|
|
35
|
+
};
|
|
36
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** Forme de réponse attendue par le SDK MCP pour un appel d'outil. */
|
|
2
|
+
export interface ToolResult {
|
|
3
|
+
[key: string]: unknown;
|
|
4
|
+
content: Array<{
|
|
5
|
+
type: "text";
|
|
6
|
+
text: string;
|
|
7
|
+
}>;
|
|
8
|
+
isError?: boolean;
|
|
9
|
+
}
|
|
10
|
+
export declare function textResult(text: string): ToolResult;
|
|
11
|
+
export declare function errorResult(text: string): ToolResult;
|
|
12
|
+
export declare function requireString(args: Record<string, unknown> | undefined, field: string): string;
|
|
13
|
+
/** Variante de ToolResult pour les outils qui renvoient une image (capture d'écran). */
|
|
14
|
+
export interface ImageToolResult {
|
|
15
|
+
[key: string]: unknown;
|
|
16
|
+
content: Array<{
|
|
17
|
+
type: "text";
|
|
18
|
+
text: string;
|
|
19
|
+
} | {
|
|
20
|
+
type: "image";
|
|
21
|
+
data: string;
|
|
22
|
+
mimeType: string;
|
|
23
|
+
}>;
|
|
24
|
+
isError?: boolean;
|
|
25
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export function textResult(text) {
|
|
2
|
+
return { content: [{ type: "text", text }] };
|
|
3
|
+
}
|
|
4
|
+
export function errorResult(text) {
|
|
5
|
+
return { content: [{ type: "text", text: `Error: ${text}` }], isError: true };
|
|
6
|
+
}
|
|
7
|
+
export function requireString(args, field) {
|
|
8
|
+
const value = args?.[field];
|
|
9
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
10
|
+
throw new Error(`"${field}" is required and must be a non-empty string.`);
|
|
11
|
+
}
|
|
12
|
+
return value.trim();
|
|
13
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { DesignStore } from "../core/design-store.js";
|
|
2
|
+
import type { MutationResult } from "./add-nodes.js";
|
|
3
|
+
/**
|
|
4
|
+
* Dissout un ou plusieurs groupes : leur contenu remonte d'un niveau, à la place
|
|
5
|
+
* qu'occupait le groupe dans l'ordre d'empilement, sans bouger à l'écran.
|
|
6
|
+
*/
|
|
7
|
+
export declare function ungroupNodes(store: DesignStore, args: Record<string, unknown> | undefined): MutationResult;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { DesignError } from "../core/node-factory.js";
|
|
2
|
+
import { requireString, textResult } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Dissout un ou plusieurs groupes : leur contenu remonte d'un niveau, à la place
|
|
5
|
+
* qu'occupait le groupe dans l'ordre d'empilement, sans bouger à l'écran.
|
|
6
|
+
*/
|
|
7
|
+
export function ungroupNodes(store, args) {
|
|
8
|
+
const id = requireString(args, "design_id");
|
|
9
|
+
const groupIds = args?.group_ids;
|
|
10
|
+
if (!Array.isArray(groupIds) || groupIds.length === 0) {
|
|
11
|
+
throw new Error(`"group_ids" is required and must be a non-empty array of group ids.`);
|
|
12
|
+
}
|
|
13
|
+
const current = store.get(id);
|
|
14
|
+
const byId = new Map(current.nodes.map((node) => [node.id, node]));
|
|
15
|
+
const groups = groupIds.map((raw) => {
|
|
16
|
+
const node = byId.get(String(raw));
|
|
17
|
+
if (!node)
|
|
18
|
+
throw new DesignError(`Node "${String(raw)}" not found in design "${id}".`);
|
|
19
|
+
if (node.type !== "group") {
|
|
20
|
+
throw new DesignError(`Node "${node.id}" is a "${node.type}", not a group — nothing to ungroup.`);
|
|
21
|
+
}
|
|
22
|
+
return node;
|
|
23
|
+
});
|
|
24
|
+
const dissolved = new Set(groups.map((group) => group.id));
|
|
25
|
+
let promoted = 0;
|
|
26
|
+
const design = store.mutate(id, "agent", (draft) => {
|
|
27
|
+
const nodesById = new Map(draft.nodes.map((node) => [node.id, node]));
|
|
28
|
+
/**
|
|
29
|
+
* Remonte la chaîne d'ancêtres jusqu'au premier groupe conservé, en
|
|
30
|
+
* accumulant les offsets traversés. Gère du même coup le cas où plusieurs
|
|
31
|
+
* groupes imbriqués sont dissous en un seul appel.
|
|
32
|
+
*/
|
|
33
|
+
const surviving = (parentId) => {
|
|
34
|
+
let dx = 0;
|
|
35
|
+
let dy = 0;
|
|
36
|
+
let cursor = parentId;
|
|
37
|
+
while (cursor !== null && dissolved.has(cursor)) {
|
|
38
|
+
const group = nodesById.get(cursor);
|
|
39
|
+
dx += group.x;
|
|
40
|
+
dy += group.y;
|
|
41
|
+
cursor = group.parentId;
|
|
42
|
+
}
|
|
43
|
+
return { parentId: cursor, dx, dy };
|
|
44
|
+
};
|
|
45
|
+
const result = [];
|
|
46
|
+
for (const node of draft.nodes) {
|
|
47
|
+
if (dissolved.has(node.id))
|
|
48
|
+
continue;
|
|
49
|
+
const target = surviving(node.parentId);
|
|
50
|
+
if (target.parentId === node.parentId) {
|
|
51
|
+
result.push(node);
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
// Le contenu absorbe l'offset des groupes traversés : rien ne bouge à l'écran.
|
|
55
|
+
promoted++;
|
|
56
|
+
result.push({ ...node, parentId: target.parentId, x: node.x + target.dx, y: node.y + target.dy });
|
|
57
|
+
}
|
|
58
|
+
draft.nodes = result;
|
|
59
|
+
});
|
|
60
|
+
return {
|
|
61
|
+
...textResult(`Ungrouped ${groups.length} group(s); ${promoted} node(s) moved up one level. Nothing moved on screen.`),
|
|
62
|
+
design,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { DesignStore } from "../core/design-store.js";
|
|
2
|
+
import type { MutationResult } from "./add-nodes.js";
|
|
3
|
+
/** Propriétés de la planche elle-même (nom, dimensions, fond) — pas de ses noeuds. */
|
|
4
|
+
export declare function updateDesign(store: DesignStore, args: Record<string, unknown> | undefined): MutationResult;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { requireString, textResult } from "./types.js";
|
|
2
|
+
/** Propriétés de la planche elle-même (nom, dimensions, fond) — pas de ses noeuds. */
|
|
3
|
+
export function updateDesign(store, args) {
|
|
4
|
+
const id = requireString(args, "design_id");
|
|
5
|
+
const fields = ["name", "width", "height", "background"];
|
|
6
|
+
if (fields.every((field) => args?.[field] === undefined)) {
|
|
7
|
+
throw new Error(`Nothing to update. Provide at least one of: ${fields.join(", ")}.`);
|
|
8
|
+
}
|
|
9
|
+
const changed = [];
|
|
10
|
+
const design = store.mutate(id, "agent", (draft) => {
|
|
11
|
+
if (args?.name !== undefined) {
|
|
12
|
+
const name = String(args.name).trim();
|
|
13
|
+
if (name === "")
|
|
14
|
+
throw new Error(`"name" must be a non-empty string.`);
|
|
15
|
+
draft.name = name;
|
|
16
|
+
changed.push(`name -> "${name}"`);
|
|
17
|
+
}
|
|
18
|
+
for (const field of ["width", "height"]) {
|
|
19
|
+
if (args?.[field] === undefined)
|
|
20
|
+
continue;
|
|
21
|
+
const value = Number(args[field]);
|
|
22
|
+
if (!Number.isFinite(value) || value <= 0)
|
|
23
|
+
throw new Error(`"${field}" must be a positive number.`);
|
|
24
|
+
draft[field] = value;
|
|
25
|
+
changed.push(`${field} -> ${value}`);
|
|
26
|
+
}
|
|
27
|
+
if (args?.background !== undefined) {
|
|
28
|
+
draft.background = String(args.background);
|
|
29
|
+
changed.push(`background -> ${draft.background}`);
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
return { ...textResult(`Design "${design.id}" updated: ${changed.join(", ")}.`), design };
|
|
33
|
+
}
|