fui-studio-mcp 0.1.2 → 0.1.3
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 +4 -1
- package/dist/{chunk-S3AUIOGZ.js → chunk-NYLRWVKF.js} +62 -0
- package/dist/{chunk-U3VXQSCB.js → chunk-ZCNEK4RK.js} +1 -1
- package/dist/{dist-LUAYBAOC.js → dist-3XKU7BN6.js} +1 -1
- package/dist/index.js +95 -4
- package/dist/{node-XJJEAAN7.js → node-VANCEYKL.js} +2 -2
- package/dist/runtime.global.js +908 -908
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -117,12 +117,15 @@ add it, use these args instead:
|
|
|
117
117
|
|
|
118
118
|
## What the assistant can do
|
|
119
119
|
|
|
120
|
-
These are the server's
|
|
120
|
+
These are the server's 26 tools:
|
|
121
121
|
|
|
122
122
|
- **Projects:** `new_project`, `open_project`, `save_project`,
|
|
123
123
|
`list_projects`, `get_project`.
|
|
124
124
|
- **Presets:** `list_presets`, `new_from_preset` — the studio's starter
|
|
125
125
|
documents (radar, dial, boot sequence, depth stack…) as a place to begin.
|
|
126
|
+
- **Elements:** `list_elements`, `insert_element` — layers the user saved
|
|
127
|
+
for reuse in the studio (its `elements/` folder), put into the project
|
|
128
|
+
as a new layer.
|
|
126
129
|
- **Building:** `list_node_types`, `describe_node` (every param explained,
|
|
127
130
|
with units and ranges), `add_node`, `add_image`, `remove_node`,
|
|
128
131
|
`set_param`, `connect`, `disconnect`, `set_keyframe`, `list_nodes`.
|
|
@@ -1105,6 +1105,67 @@ function orderKeys(value) {
|
|
|
1105
1105
|
function serializeProject(project) {
|
|
1106
1106
|
return JSON.stringify(orderKeys(project), null, 2) + "\n";
|
|
1107
1107
|
}
|
|
1108
|
+
function insertComposition(project, doc, mint, known = () => true) {
|
|
1109
|
+
const elementLayer = doc.graph.nodes.find((n) => n.type === "layer");
|
|
1110
|
+
if (!elementLayer) return void 0;
|
|
1111
|
+
const canvas = project.graph.nodes.find((n) => n.type === "canvas");
|
|
1112
|
+
const tokens = project.graph.nodes.find((n) => n.type === "style-tokens");
|
|
1113
|
+
const elementTokens = doc.graph.nodes.find((n) => n.type === "style-tokens");
|
|
1114
|
+
const taken = new Set(project.graph.nodes.filter((n) => n.type === "layer").map((n) => n.name?.trim()));
|
|
1115
|
+
const base2 = doc.name.trim() || elementLayer.name?.trim() || "Element";
|
|
1116
|
+
let name = base2;
|
|
1117
|
+
for (let i = 2; taken.has(name); i++) name = `${base2} ${i}`;
|
|
1118
|
+
let top = 0;
|
|
1119
|
+
for (const n of project.graph.nodes) if (n.type === "layer" && typeof n.params.order === "number") top = Math.max(top, n.params.order);
|
|
1120
|
+
const remap = /* @__PURE__ */ new Map();
|
|
1121
|
+
const nodes = doc.graph.nodes.filter((n) => n.type !== "canvas" && n.type !== "ograf" && !(n.type === "style-tokens" && tokens) && known(n)).map((n) => {
|
|
1122
|
+
const id = mint(n.type.replace(/[^a-z]/g, "") || "node");
|
|
1123
|
+
remap.set(n.id, id);
|
|
1124
|
+
const copy = { ...structuredClone(n), id };
|
|
1125
|
+
if (n.id === elementLayer.id) {
|
|
1126
|
+
copy.name = name;
|
|
1127
|
+
copy.params = { ...copy.params, order: top + 1 };
|
|
1128
|
+
}
|
|
1129
|
+
return copy;
|
|
1130
|
+
});
|
|
1131
|
+
for (const n of nodes) {
|
|
1132
|
+
if (ROOT_TYPES.has(n.type)) delete n.layer;
|
|
1133
|
+
else if (n.layer !== void 0) {
|
|
1134
|
+
const home = remap.get(n.layer);
|
|
1135
|
+
if (home) n.layer = home;
|
|
1136
|
+
else delete n.layer;
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
const edges = [];
|
|
1140
|
+
for (const e of doc.graph.edges) {
|
|
1141
|
+
const from = remap.get(e.from);
|
|
1142
|
+
const to = remap.get(e.to);
|
|
1143
|
+
if (from && to) edges.push({ ...e, id: mint("edge"), from, to });
|
|
1144
|
+
else if (to && tokens && elementTokens && e.from === elementTokens.id) edges.push({ ...e, id: mint("edge"), from: tokens.id, to });
|
|
1145
|
+
}
|
|
1146
|
+
const layerId = remap.get(elementLayer.id);
|
|
1147
|
+
if (canvas) edges.push({ id: mint("edge"), from: layerId, fromPort: "out", to: canvas.id, toPort: "in" });
|
|
1148
|
+
const groups = doc.groups.flatMap((g) => {
|
|
1149
|
+
const nodeIds = g.nodeIds.flatMap((id) => remap.has(id) ? [remap.get(id)] : []);
|
|
1150
|
+
if (nodeIds.length === 0) return [];
|
|
1151
|
+
return [
|
|
1152
|
+
{
|
|
1153
|
+
id: mint("group"),
|
|
1154
|
+
name: g.name,
|
|
1155
|
+
nodeIds,
|
|
1156
|
+
collapsed: g.collapsed,
|
|
1157
|
+
exposed: g.exposed.flatMap((entry) => {
|
|
1158
|
+
const dot = entry.indexOf(".");
|
|
1159
|
+
const copy = remap.get(dot < 0 ? entry : entry.slice(0, dot));
|
|
1160
|
+
return copy ? [`${copy}${dot < 0 ? "" : entry.slice(dot)}`] : [];
|
|
1161
|
+
})
|
|
1162
|
+
}
|
|
1163
|
+
];
|
|
1164
|
+
});
|
|
1165
|
+
const assets = { ...project.assets };
|
|
1166
|
+
for (const [key, record] of Object.entries(doc.assets)) if (!(key in assets)) assets[key] = structuredClone(record);
|
|
1167
|
+
return { nodes, edges, groups, assets, layerId, name };
|
|
1168
|
+
}
|
|
1108
1169
|
|
|
1109
1170
|
// ../../packages/engine/dist/index.js
|
|
1110
1171
|
var DEFAULT_STYLE = {
|
|
@@ -17968,6 +18029,7 @@ export {
|
|
|
17968
18029
|
adoptions,
|
|
17969
18030
|
loadProject,
|
|
17970
18031
|
serializeProject,
|
|
18032
|
+
insertComposition,
|
|
17971
18033
|
isExposable,
|
|
17972
18034
|
layerDuration,
|
|
17973
18035
|
layerLoop,
|
package/dist/index.js
CHANGED
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
allTtfBuffers,
|
|
5
5
|
fontDefs,
|
|
6
6
|
nodeOutliner
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-ZCNEK4RK.js";
|
|
8
8
|
import {
|
|
9
9
|
DEFAULT_FONT_FAMILY,
|
|
10
10
|
Keyframe,
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
emptyProject,
|
|
18
18
|
envName,
|
|
19
19
|
evaluate,
|
|
20
|
+
insertComposition,
|
|
20
21
|
isExposable,
|
|
21
22
|
layerDuration,
|
|
22
23
|
layerLoop,
|
|
@@ -39,7 +40,7 @@ import {
|
|
|
39
40
|
wouldCycle,
|
|
40
41
|
yuvFrameBytes,
|
|
41
42
|
yuvToRgb
|
|
42
|
-
} from "./chunk-
|
|
43
|
+
} from "./chunk-NYLRWVKF.js";
|
|
43
44
|
|
|
44
45
|
// src/index.ts
|
|
45
46
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
@@ -834,6 +835,75 @@ var Session = class {
|
|
|
834
835
|
this.stamp = this.mtimeOf(target);
|
|
835
836
|
return target;
|
|
836
837
|
}
|
|
838
|
+
/**
|
|
839
|
+
* The elements in the projects folder: `elements/<name>.tui`, each a
|
|
840
|
+
* layer saved for reuse from the studio (its layers pane) — the shelf
|
|
841
|
+
* the studio's elements button shows. Newest first.
|
|
842
|
+
*/
|
|
843
|
+
listElements() {
|
|
844
|
+
if (!this.root) fail("The server was started without a projects folder (--projects <dir> or FUI_PROJECTS), so there is no elements/ folder to list.");
|
|
845
|
+
const dir = join3(this.root, "elements");
|
|
846
|
+
let entries;
|
|
847
|
+
try {
|
|
848
|
+
entries = readdirSync(dir);
|
|
849
|
+
} catch {
|
|
850
|
+
return [];
|
|
851
|
+
}
|
|
852
|
+
const out = [];
|
|
853
|
+
for (const file of entries) {
|
|
854
|
+
if (!file.endsWith(".tui")) continue;
|
|
855
|
+
const abs = join3(dir, file);
|
|
856
|
+
const at = this.mtimeOf(abs) ?? 0;
|
|
857
|
+
let description = "";
|
|
858
|
+
try {
|
|
859
|
+
const { project } = loadProject(JSON.parse(readFileSync3(abs, "utf8")));
|
|
860
|
+
const layer = project.graph.nodes.find((n) => n.type === "layer");
|
|
861
|
+
const members = project.graph.nodes.filter((n) => layer && n.layer === layer.id);
|
|
862
|
+
const kinds = [...new Set(members.map((n) => n.type))];
|
|
863
|
+
description = `${members.length} node${members.length === 1 ? "" : "s"}${kinds.length > 0 ? ` (${kinds.slice(0, 6).join(", ")}${kinds.length > 6 ? ", \u2026" : ""})` : ""}`;
|
|
864
|
+
} catch {
|
|
865
|
+
description = "unreadable";
|
|
866
|
+
}
|
|
867
|
+
out.push({ name: file.slice(0, -4), description, saved: new Date(at).toISOString(), at });
|
|
868
|
+
}
|
|
869
|
+
out.sort((a, b) => b.at - a.at);
|
|
870
|
+
return out.map(({ at: _at, ...rest }) => rest);
|
|
871
|
+
}
|
|
872
|
+
/**
|
|
873
|
+
* An element into the open project as a new Layer on top (the shared
|
|
874
|
+
* surgery in @fui/schema): its name counted up when taken, wired into the
|
|
875
|
+
* Canvas, its Tokens wires re-bound to this document's Tokens. Its
|
|
876
|
+
* pictures' bytes come along from `elements/<name>.assets/` and are
|
|
877
|
+
* written beside the project on save. The Layer and its drivers have no
|
|
878
|
+
* place here yet: the save lays them out, as it lays out any node added
|
|
879
|
+
* without a position.
|
|
880
|
+
*/
|
|
881
|
+
insertElement(name) {
|
|
882
|
+
if (!this.root) fail("The server was started without a projects folder (--projects <dir> or FUI_PROJECTS), so there is no elements/ folder to insert from.");
|
|
883
|
+
const abs = join3(this.root, "elements", `${name.replace(/\.tui$/i, "")}.tui`);
|
|
884
|
+
if (!existsSync2(abs)) {
|
|
885
|
+
const have = this.listElements().map((e) => e.name);
|
|
886
|
+
fail(`No element "${name}".${have.length > 0 ? ` The elements: ${have.join(", ")}.` : " The elements/ folder is empty \u2014 save a layer as an element in the studio first."}`);
|
|
887
|
+
}
|
|
888
|
+
const { project: doc } = loadProject(JSON.parse(readFileSync3(abs, "utf8")));
|
|
889
|
+
const insertion = insertComposition(this.project, { ...doc, name: name.replace(/\.tui$/i, "") }, makeId, (n) => registry.forNode(n) !== void 0);
|
|
890
|
+
if (!insertion) fail(`${abs} is not an element: it has no Layer in it.`);
|
|
891
|
+
const dir = assetDirFor(abs);
|
|
892
|
+
for (const [key, record] of Object.entries(doc.assets)) {
|
|
893
|
+
if (record.video || this.held.has(key)) continue;
|
|
894
|
+
const found = assetFileIn(dir, key, record);
|
|
895
|
+
if (!found) continue;
|
|
896
|
+
const bytes = new Uint8Array(readFileSync3(found.path));
|
|
897
|
+
this.held.set(key, { href: `data:${record.mime};base64,${Buffer.from(bytes).toString("base64")}`, bytes, mime: record.mime });
|
|
898
|
+
}
|
|
899
|
+
this.project.graph.nodes.push(...insertion.nodes);
|
|
900
|
+
this.project.graph.edges.push(...insertion.edges);
|
|
901
|
+
this.project.groups.push(...insertion.groups);
|
|
902
|
+
this.project.assets = insertion.assets;
|
|
903
|
+
for (const n of insertion.nodes) if (n.layer === void 0) this.placed.add(n.id);
|
|
904
|
+
const layer = insertion.nodes.find((n) => n.id === insertion.layerId);
|
|
905
|
+
return { layer, nodes: insertion.nodes };
|
|
906
|
+
}
|
|
837
907
|
/**
|
|
838
908
|
* The projects folder's documents, loose and one folder deep — the shape
|
|
839
909
|
* the studio's Open dialog shows. Newest first.
|
|
@@ -1348,6 +1418,8 @@ Pixels: shapes are vectors until a rasterize node draws them; post nodes (blur,
|
|
|
1348
1418
|
|
|
1349
1419
|
Keep the graph readable, because the user opens it in the studio. A node whose output is wired to nothing draws straight onto the page. That works, but a page of loose nodes looks like a graph with its wires missing. Gather the parts of one element (a radar's rings, ticks, labels and crosshair) into a Merge node: add_node merge, then connect each part to its "in" port. Merge draws in connection order, so the first wire is at the bottom. Give the Merge a name ("radar", "readout") so the studio shows what it is. Leave position out of add_node: on save, nodes without one are laid out in columns that follow the wires.
|
|
1350
1420
|
|
|
1421
|
+
Elements: list_elements shows layers the user saved for reuse in the studio; insert_element puts one into the project as a new Layer, ready to change. When the user mentions something they have made before (their radar, their readout), look there first.
|
|
1422
|
+
|
|
1351
1423
|
Layers: a Layer node is a self-contained composition with its own page and its own clock. add_node with layer: "<layer id>" puts a node inside it. Arrange Layers in time with add_clip; a document with no clips plays every Layer all the time. list_layers explains the rules.
|
|
1352
1424
|
|
|
1353
1425
|
The user is probably watching. When the user has the same projects folder open in FUI Studio (Choose folder in the browser) with this project loaded, every save_project appears there within a second. So save after each meaningful step, and say what changed. The user can edit in the studio too: if save_project refuses because the file changed, open_project it again, which picks up their edits, redo your change and save. Do not reach for overwrite unless the user asks for it.
|
|
@@ -1505,6 +1577,25 @@ server.registerTool(
|
|
|
1505
1577
|
return text(await session.summary());
|
|
1506
1578
|
})
|
|
1507
1579
|
);
|
|
1580
|
+
server.registerTool(
|
|
1581
|
+
"list_elements",
|
|
1582
|
+
{
|
|
1583
|
+
description: "The elements in the projects folder: layers the user saved for reuse from the studio (a radar, a readout, a panel), each a working composition with a picture. insert_element puts one into the open project as a new layer; prefer that to rebuilding what the user already has.",
|
|
1584
|
+
inputSchema: {}
|
|
1585
|
+
},
|
|
1586
|
+
guard(() => text(session.listElements()))
|
|
1587
|
+
);
|
|
1588
|
+
server.registerTool(
|
|
1589
|
+
"insert_element",
|
|
1590
|
+
{
|
|
1591
|
+
description: "Insert an element (list_elements) into the open project as a new Layer on top, wired into the Canvas, named after the element (counted up when the name is taken), taking the colours of this project\u2019s Style Tokens. Its nodes are ordinary nodes afterwards: list_nodes shows them, set_param changes them. Returns the new Layer and its nodes.",
|
|
1592
|
+
inputSchema: { name: z2.string().describe("The element\u2019s name as list_elements gives it") }
|
|
1593
|
+
},
|
|
1594
|
+
guard(({ name }) => {
|
|
1595
|
+
const { layer, nodes } = session.insertElement(name);
|
|
1596
|
+
return text({ layer: { id: layer.id, name: layer.name, order: layer.params.order }, nodes: nodes.map((n) => ({ id: n.id, type: n.type, ...n.layer ? { layer: n.layer } : {} })) });
|
|
1597
|
+
})
|
|
1598
|
+
);
|
|
1508
1599
|
server.registerTool(
|
|
1509
1600
|
"list_nodes",
|
|
1510
1601
|
{ description: "Every node in the graph, with params and incoming wires.", inputSchema: {} },
|
|
@@ -1699,8 +1790,8 @@ server.registerTool(
|
|
|
1699
1790
|
const abs = session.resolvePath(path);
|
|
1700
1791
|
mkdirSync2(dirname3(abs), { recursive: true });
|
|
1701
1792
|
if (/\.(ograf|drfx)$/i.test(abs)) {
|
|
1702
|
-
const { exportTemplate } = await import("./node-
|
|
1703
|
-
const { packDrfx, packOgraf } = await import("./dist-
|
|
1793
|
+
const { exportTemplate } = await import("./node-VANCEYKL.js");
|
|
1794
|
+
const { packDrfx, packOgraf } = await import("./dist-3XKU7BN6.js");
|
|
1704
1795
|
const template = exportTemplate(session.project, void 0, session.assetResolver());
|
|
1705
1796
|
const errors = template.lint.filter((i) => i.level === "error");
|
|
1706
1797
|
if (errors.length > 0) throw new SessionError(`The manifest has errors: ${errors.map((i) => `${i.where}: ${i.message}`).join("; ")}`);
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
import {
|
|
3
3
|
loadFaces,
|
|
4
4
|
nodeOutliner
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-ZCNEK4RK.js";
|
|
6
6
|
import {
|
|
7
7
|
buildTemplate
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-NYLRWVKF.js";
|
|
9
9
|
|
|
10
10
|
// ../../packages/ograf/dist/node.js
|
|
11
11
|
import { existsSync, readFileSync } from "fs";
|