@feimacode/open-design-agent-kit-mcp 0.1.4 → 0.1.7
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 +10 -3
- package/out/index.js +481 -81
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
# @feimacode/open-design-agent-kit-mcp
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**Pro-grade design for any MCP-capable coding agent. One command, nothing else to run.**
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
This is [Open Design](https://github.com/nexu-io/open-design)'s design library and workflow, packaged as a plain stdio MCP server for Claude Code, Codex, Cursor, and anything else that speaks MCP. It includes 163 skills, 114 design templates, 152 brand design systems, and 167 remixable examples. Ask your agent for a deck, a landing page, a dashboard, or a prototype, and it builds one that looks designed rather than generated, as real files in your project.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
- **No desktop app, no daemon, no account.** Your agent launches the server on demand over stdio, and it exits when the agent does.
|
|
8
|
+
- **No API keys or model settings.** The server never calls a model. Your agent's own selected model does the generation with its normal file-editing tools. The server supplies the library and handles the bookkeeping (manifests, active design system).
|
|
9
|
+
- **Zero config.** It works in the current project out of the box. The active design system is remembered in `.open-design/config.json`.
|
|
10
|
+
- **Plain files.** Everything it produces is ordinary HTML plus small JSON sidecars in your repo, so you can diff, review, and commit it.
|
|
11
|
+
|
|
12
|
+
It has the same core tools and the same content as the [VS Code extension](https://marketplace.visualstudio.com/items?itemName=feima.open-design-agent-kit), over MCP instead of native `languageModelTools`.
|
|
13
|
+
|
|
14
|
+
## See what it builds
|
|
8
15
|
|
|
9
16
|
Four of the ~280 vendored skills and templates, rendered exactly as-is:
|
|
10
17
|
|
package/out/index.js
CHANGED
|
@@ -3341,11 +3341,11 @@ var require_excerpt = __commonJS({
|
|
|
3341
3341
|
if (typeof opts.excerpt === "function") {
|
|
3342
3342
|
return opts.excerpt(file, opts);
|
|
3343
3343
|
}
|
|
3344
|
-
const
|
|
3345
|
-
if (
|
|
3344
|
+
const sep3 = file.data.excerpt_separator || opts.excerpt_separator;
|
|
3345
|
+
if (sep3 == null && (opts.excerpt === false || opts.excerpt == null)) {
|
|
3346
3346
|
return file;
|
|
3347
3347
|
}
|
|
3348
|
-
const delimiter = typeof opts.excerpt === "string" ? opts.excerpt :
|
|
3348
|
+
const delimiter = typeof opts.excerpt === "string" ? opts.excerpt : sep3 || opts.delimiters[0];
|
|
3349
3349
|
const idx = file.content.indexOf(delimiter);
|
|
3350
3350
|
if (idx !== -1) {
|
|
3351
3351
|
file.excerpt = file.content.slice(0, idx);
|
|
@@ -3410,7 +3410,7 @@ var require_parse = __commonJS({
|
|
|
3410
3410
|
var require_gray_matter = __commonJS({
|
|
3411
3411
|
"../../node_modules/gray-matter/index.js"(exports2, module2) {
|
|
3412
3412
|
"use strict";
|
|
3413
|
-
var
|
|
3413
|
+
var fs9 = require("fs");
|
|
3414
3414
|
var sections = require_section_matter();
|
|
3415
3415
|
var defaults = require_defaults();
|
|
3416
3416
|
var stringify = require_stringify();
|
|
@@ -3494,7 +3494,7 @@ var require_gray_matter = __commonJS({
|
|
|
3494
3494
|
return stringify(file, data, options2);
|
|
3495
3495
|
};
|
|
3496
3496
|
matter2.read = function(filepath, options2) {
|
|
3497
|
-
const str2 =
|
|
3497
|
+
const str2 = fs9.readFileSync(filepath, "utf8");
|
|
3498
3498
|
const file = matter2(str2, options2);
|
|
3499
3499
|
file.path = filepath;
|
|
3500
3500
|
return file;
|
|
@@ -3531,7 +3531,7 @@ var import_types = require("@modelcontextprotocol/sdk/types.js");
|
|
|
3531
3531
|
var import_node_fs = require("node:fs");
|
|
3532
3532
|
var path = __toESM(require("node:path"));
|
|
3533
3533
|
var import_gray_matter = __toESM(require_gray_matter());
|
|
3534
|
-
var SKILL_SOURCES = ["skill", "design-template", "example"];
|
|
3534
|
+
var SKILL_SOURCES = ["skill", "design-template", "example", "community"];
|
|
3535
3535
|
var SKILL_MODES = [
|
|
3536
3536
|
"prototype",
|
|
3537
3537
|
"deck",
|
|
@@ -3611,7 +3611,7 @@ async function loadSkillLikeDir(assetsRoot, subdir, source) {
|
|
|
3611
3611
|
}
|
|
3612
3612
|
return result;
|
|
3613
3613
|
}
|
|
3614
|
-
async function loadExamples(assetsRoot) {
|
|
3614
|
+
async function loadExamples(assetsRoot, source = "example") {
|
|
3615
3615
|
const dir = path.join(assetsRoot, "examples");
|
|
3616
3616
|
const result = /* @__PURE__ */ new Map();
|
|
3617
3617
|
if (!await pathExists(dir)) return result;
|
|
@@ -3642,7 +3642,7 @@ async function loadExamples(assetsRoot) {
|
|
|
3642
3642
|
triggers: [...normalizeStringArray(data.triggers), ...normalizeStringArray(data.tags)],
|
|
3643
3643
|
category: typeof od?.category === "string" ? od.category : typeof data.category === "string" ? data.category : void 0,
|
|
3644
3644
|
mode: normalizeMode(od?.mode),
|
|
3645
|
-
source
|
|
3645
|
+
source,
|
|
3646
3646
|
examplePrompt,
|
|
3647
3647
|
featured: false,
|
|
3648
3648
|
exampleArtifactPath: path.posix.join("examples", entry.name, "example.html"),
|
|
@@ -3771,9 +3771,10 @@ function mergeSkillPools(...pools) {
|
|
|
3771
3771
|
return result;
|
|
3772
3772
|
}
|
|
3773
3773
|
var ContentIndex = class {
|
|
3774
|
-
constructor(assetsRoot, getUserDesignSystemsDir) {
|
|
3774
|
+
constructor(assetsRoot, getUserDesignSystemsDir, getCommunityContentDir) {
|
|
3775
3775
|
this.assetsRoot = assetsRoot;
|
|
3776
3776
|
this.getUserDesignSystemsDir = getUserDesignSystemsDir;
|
|
3777
|
+
this.getCommunityContentDir = getCommunityContentDir;
|
|
3777
3778
|
}
|
|
3778
3779
|
ensureLoaded() {
|
|
3779
3780
|
if (!this.loaded) {
|
|
@@ -3791,8 +3792,24 @@ var ContentIndex = class {
|
|
|
3791
3792
|
}
|
|
3792
3793
|
return this.loaded;
|
|
3793
3794
|
}
|
|
3794
|
-
|
|
3795
|
+
// Merges the cached built-in skill/template/example pool with a
|
|
3796
|
+
// freshly-rescanned community pool on every call — same reasoning as
|
|
3797
|
+
// loadedDesignSystems()'s user-pool rescan: a sync command can replace the
|
|
3798
|
+
// on-disk community cache at any moment in the session, with no reload.
|
|
3799
|
+
// mergeSkillPools regroups by each entry's own `.id` (dirId) regardless of
|
|
3800
|
+
// whether its input map is raw or already merged, so re-running it here
|
|
3801
|
+
// against the cached (already-merged) pool plus the fresh community pool
|
|
3802
|
+
// correctly re-applies collision-priority ordering (community always
|
|
3803
|
+
// loses) without re-deriving anything from the cached pool.
|
|
3804
|
+
async loadedSkills() {
|
|
3795
3805
|
const { skills } = await this.ensureLoaded();
|
|
3806
|
+
const communityDir = this.getCommunityContentDir?.();
|
|
3807
|
+
const communitySkills = communityDir ? await loadExamples(communityDir, "community") : /* @__PURE__ */ new Map();
|
|
3808
|
+
if (communitySkills.size === 0) return skills;
|
|
3809
|
+
return mergeSkillPools(skills, communitySkills);
|
|
3810
|
+
}
|
|
3811
|
+
async listSkills(query, mode, source, remixableOnly) {
|
|
3812
|
+
const skills = await this.loadedSkills();
|
|
3796
3813
|
const wantedMode = mode?.trim().toLowerCase();
|
|
3797
3814
|
const wantedSource = source?.trim().toLowerCase();
|
|
3798
3815
|
const results = [];
|
|
@@ -3818,7 +3835,7 @@ var ContentIndex = class {
|
|
|
3818
3835
|
return results;
|
|
3819
3836
|
}
|
|
3820
3837
|
async getSkill(id) {
|
|
3821
|
-
const
|
|
3838
|
+
const skills = await this.loadedSkills();
|
|
3822
3839
|
const trimmed = id.trim();
|
|
3823
3840
|
const exact = skills.get(trimmed);
|
|
3824
3841
|
if (exact) return exact;
|
|
@@ -3829,7 +3846,7 @@ var ContentIndex = class {
|
|
|
3829
3846
|
return candidates[0];
|
|
3830
3847
|
}
|
|
3831
3848
|
async listSkillModes() {
|
|
3832
|
-
const
|
|
3849
|
+
const skills = await this.loadedSkills();
|
|
3833
3850
|
const modes = /* @__PURE__ */ new Set();
|
|
3834
3851
|
for (const skill of skills.values()) modes.add(skill.mode);
|
|
3835
3852
|
return [...modes].sort((a, b) => a.localeCompare(b));
|
|
@@ -3882,11 +3899,11 @@ function selectCraftSections(all, suggestedIds) {
|
|
|
3882
3899
|
function composeInstructions(input) {
|
|
3883
3900
|
const parts = [];
|
|
3884
3901
|
parts.push(
|
|
3885
|
-
`#
|
|
3902
|
+
`# Open Design generation brief
|
|
3886
3903
|
|
|
3887
|
-
You are authoring a new design artifact using the
|
|
3904
|
+
You are authoring a new design artifact using the Open Design skill "${input.skillName}". Follow the skill's workflow below, treat the active design system's tokens (if any) as authoritative, and author the files yourself with your own file-editing tools \u2014 do not look for any tool that writes design content on your behalf.
|
|
3888
3905
|
|
|
3889
|
-
**Everything below is delivered to you as plain text; none of it is a file path you can open.** The skill text, craft rules, and design system notes below are copied verbatim from
|
|
3906
|
+
**Everything below is delivered to you as plain text; none of it is a file path you can open.** The skill text, craft rules, and design system notes below are copied verbatim from Open Design's own upstream catalog and may mention file names or source paths (e.g. "example.html", "apps/daemon/src/...", a "Resource map" of sibling files) that describe Open Design's own separate codebase or runtime \u2014 none of those exist in this workspace or are readable by you. Never search for, open, or navigate to any file path mentioned anywhere below; if you want reference examples, call remix_open_design_example or list_open_design_skills instead of searching the filesystem. Treat every path-looking string below as descriptive prose only.`
|
|
3890
3907
|
);
|
|
3891
3908
|
parts.push(`
|
|
3892
3909
|
|
|
@@ -3933,6 +3950,21 @@ ${s.body.trim()}`).join("\n\n")}`
|
|
|
3933
3950
|
Detected: ${input.existingAppFrameworks.join(", ")}. Before generating, consider looking at a few of its real existing components/pages for actual conventions (styling approach, layout patterns, component structure) with your own file-reading tools, so the artifact you produce is closer to how this app already looks. This does NOT change where or how you write the artifact \u2014 it's still a standalone prototype at the output path below, not a real app file; it's purely a nudge toward visual consistency, not a requirement to integrate with the app's code.`
|
|
3934
3951
|
);
|
|
3935
3952
|
}
|
|
3953
|
+
if (input.collectionContext) {
|
|
3954
|
+
const { collectionName, index, total, role, siblingScreens } = input.collectionContext;
|
|
3955
|
+
const siblingsText = siblingScreens.length > 0 ? siblingScreens.map((s) => `- **${s.role}** \u2014 "${s.title}"`).join("\n") : "(none yet \u2014 this is the first screen)";
|
|
3956
|
+
parts.push(
|
|
3957
|
+
`
|
|
3958
|
+
|
|
3959
|
+
## Part of a design collection
|
|
3960
|
+
|
|
3961
|
+
This artifact is screen ${index} of ${total} in the design collection "${collectionName}" \u2014 this screen's role: **${role}**. The other screens in this collection so far:
|
|
3962
|
+
|
|
3963
|
+
${siblingsText}
|
|
3964
|
+
|
|
3965
|
+
Keep this screen visually and stylistically consistent with the others (same design system, same header/nav treatment, same component style) without re-reading their HTML \u2014 you already have their role and title above as context. Do not duplicate content that belongs on a different screen.`
|
|
3966
|
+
);
|
|
3967
|
+
}
|
|
3936
3968
|
parts.push(
|
|
3937
3969
|
`
|
|
3938
3970
|
|
|
@@ -3945,7 +3977,7 @@ Choose a short semantic filename derived from the brief, product, or artifact ty
|
|
|
3945
3977
|
|
|
3946
3978
|
## Output
|
|
3947
3979
|
|
|
3948
|
-
Author the artifact's entry file at \`${input.suggestedEntryPath}\` (adjust the filename to follow the semantic-naming guidance above, or the skill's own fixed-name convention, but keep it under the same directory). Write any supporting files (stylesheets, scripts, imported assets) as siblings under that directory. After writing all files, call register_open_design_artifact with the entry path, a kind, a title, and the supporting file paths (relative to the entry file's own directory) so it is recognized as an
|
|
3980
|
+
Author the artifact's entry file at \`${input.suggestedEntryPath}\` (adjust the filename to follow the semantic-naming guidance above, or the skill's own fixed-name convention, but keep it under the same directory). Write any supporting files (stylesheets, scripts, imported assets) as siblings under that directory. After writing all files, call register_open_design_artifact with the entry path, a kind, a title, and the supporting file paths (relative to the entry file's own directory) so it is recognized as an Open Design artifact.`
|
|
3949
3981
|
);
|
|
3950
3982
|
return parts.join("");
|
|
3951
3983
|
}
|
|
@@ -4043,11 +4075,17 @@ async function extractBrandEvidence(sourceUrl) {
|
|
|
4043
4075
|
return synthesizeBrandEvidence(base, html, stylesheetTexts, warnings);
|
|
4044
4076
|
}
|
|
4045
4077
|
|
|
4078
|
+
// ../core/src/generation/collectionEntryPath.ts
|
|
4079
|
+
var path2 = __toESM(require("node:path"));
|
|
4080
|
+
function suggestCollectionScreenEntryPath(outputDir, collectionId, screenSlug) {
|
|
4081
|
+
return path2.posix.join(outputDir, collectionId, `${screenSlug}.html`);
|
|
4082
|
+
}
|
|
4083
|
+
|
|
4046
4084
|
// ../core/src/generation/customDesignSystemInstructions.ts
|
|
4047
4085
|
function composeCustomDesignSystemInstructions(input) {
|
|
4048
4086
|
const parts = [];
|
|
4049
4087
|
parts.push(
|
|
4050
|
-
`# Create a custom
|
|
4088
|
+
`# Create a custom Open Design design system
|
|
4051
4089
|
|
|
4052
4090
|
You are authoring a new, custom design system as a single \`DESIGN.md\` file. Once written, it becomes a first-class, selectable design system \u2014 the exact same mechanism as the ~150 bundled ones, just workspace-local instead of built into the extension.`
|
|
4053
4091
|
);
|
|
@@ -4115,9 +4153,155 @@ Call \`set_active_design_system\` with designSystemId "${input.id}" so it's used
|
|
|
4115
4153
|
return parts.join("");
|
|
4116
4154
|
}
|
|
4117
4155
|
|
|
4156
|
+
// ../core/src/generation/figmaPull.ts
|
|
4157
|
+
var FIGMA_URL_RE = /^https?:\/\/(?:www\.)?figma\.com\/(?:file|design)\/([A-Za-z0-9]+)/i;
|
|
4158
|
+
function parseFigmaUrl(url) {
|
|
4159
|
+
const match = FIGMA_URL_RE.exec(url.trim());
|
|
4160
|
+
if (!match) return void 0;
|
|
4161
|
+
const fileKey = match[1];
|
|
4162
|
+
let nodeId;
|
|
4163
|
+
try {
|
|
4164
|
+
const parsed = new URL(url);
|
|
4165
|
+
const raw = parsed.searchParams.get("node-id");
|
|
4166
|
+
if (raw) nodeId = raw.replace("-", ":");
|
|
4167
|
+
} catch {
|
|
4168
|
+
}
|
|
4169
|
+
return { fileKey, nodeId };
|
|
4170
|
+
}
|
|
4171
|
+
var FigmaApiError = class extends Error {
|
|
4172
|
+
};
|
|
4173
|
+
async function fetchFigmaNode(token, ref, fetchImpl = fetch) {
|
|
4174
|
+
if (!ref.nodeId) {
|
|
4175
|
+
throw new FigmaApiError(
|
|
4176
|
+
`This Figma URL has no node id. Use Figma's "Copy link to selection" on the specific frame (not just the file link) and try again.`
|
|
4177
|
+
);
|
|
4178
|
+
}
|
|
4179
|
+
const url = `https://api.figma.com/v1/files/${ref.fileKey}/nodes?ids=${encodeURIComponent(ref.nodeId)}`;
|
|
4180
|
+
const response = await fetchImpl(url, { headers: { "X-Figma-Token": token } });
|
|
4181
|
+
if (!response.ok) {
|
|
4182
|
+
throw new FigmaApiError(`Figma API responded with HTTP ${response.status} while fetching the frame.`);
|
|
4183
|
+
}
|
|
4184
|
+
const body = await response.json();
|
|
4185
|
+
const document = body.nodes?.[ref.nodeId]?.document;
|
|
4186
|
+
if (!document) {
|
|
4187
|
+
throw new FigmaApiError(`Figma API response did not include a document for node "${ref.nodeId}" \u2014 it may not exist, or the token lacks access.`);
|
|
4188
|
+
}
|
|
4189
|
+
return document;
|
|
4190
|
+
}
|
|
4191
|
+
async function fetchFigmaFrameImage(token, fileKey, nodeId, fetchImpl = fetch) {
|
|
4192
|
+
try {
|
|
4193
|
+
const url = `https://api.figma.com/v1/images/${fileKey}?ids=${encodeURIComponent(nodeId)}&format=png&scale=2`;
|
|
4194
|
+
const response = await fetchImpl(url, { headers: { "X-Figma-Token": token } });
|
|
4195
|
+
if (!response.ok) return void 0;
|
|
4196
|
+
const body = await response.json();
|
|
4197
|
+
if (body.err) return void 0;
|
|
4198
|
+
return body.images?.[nodeId] ?? void 0;
|
|
4199
|
+
} catch {
|
|
4200
|
+
return void 0;
|
|
4201
|
+
}
|
|
4202
|
+
}
|
|
4203
|
+
function toHexByte(value) {
|
|
4204
|
+
return Math.round(Math.max(0, Math.min(1, value)) * 255).toString(16).padStart(2, "0");
|
|
4205
|
+
}
|
|
4206
|
+
function toHexColor(color) {
|
|
4207
|
+
const hex = `#${toHexByte(color.r)}${toHexByte(color.g)}${toHexByte(color.b)}`;
|
|
4208
|
+
return color.a !== void 0 && color.a < 1 ? `${hex}${toHexByte(color.a)}` : hex;
|
|
4209
|
+
}
|
|
4210
|
+
function firstSolidColor(paints) {
|
|
4211
|
+
const solid = paints?.find((p) => p.type === "SOLID" && p.visible !== false && p.color);
|
|
4212
|
+
if (!solid?.color) return void 0;
|
|
4213
|
+
return toHexColor({ ...solid.color, a: solid.opacity ?? solid.color.a });
|
|
4214
|
+
}
|
|
4215
|
+
var DEFAULT_MAX_NODES = 500;
|
|
4216
|
+
function summarizeFigmaNode(node, opts = {}) {
|
|
4217
|
+
const maxNodes = opts.maxNodes ?? DEFAULT_MAX_NODES;
|
|
4218
|
+
let count = 0;
|
|
4219
|
+
let truncated = false;
|
|
4220
|
+
function walk(n) {
|
|
4221
|
+
if (count >= maxNodes) {
|
|
4222
|
+
truncated = true;
|
|
4223
|
+
return void 0;
|
|
4224
|
+
}
|
|
4225
|
+
count++;
|
|
4226
|
+
const summary = { id: n.id, name: n.name, type: n.type };
|
|
4227
|
+
if (n.absoluteBoundingBox) summary.box = { ...n.absoluteBoundingBox };
|
|
4228
|
+
const fillColor = firstSolidColor(n.fills);
|
|
4229
|
+
if (fillColor) summary.fillColor = fillColor;
|
|
4230
|
+
const strokeColor = firstSolidColor(n.strokes);
|
|
4231
|
+
if (strokeColor) summary.strokeColor = strokeColor;
|
|
4232
|
+
if (typeof n.cornerRadius === "number") summary.cornerRadius = n.cornerRadius;
|
|
4233
|
+
if (typeof n.opacity === "number" && n.opacity < 1) summary.opacity = n.opacity;
|
|
4234
|
+
if (n.type === "TEXT") {
|
|
4235
|
+
summary.characters = n.characters;
|
|
4236
|
+
summary.fontFamily = n.style?.fontFamily;
|
|
4237
|
+
summary.fontSize = n.style?.fontSize;
|
|
4238
|
+
summary.fontWeight = n.style?.fontWeight;
|
|
4239
|
+
summary.textAlign = n.style?.textAlignHorizontal;
|
|
4240
|
+
}
|
|
4241
|
+
if (n.children && n.children.length > 0) {
|
|
4242
|
+
const children = n.children.map(walk).filter((c) => !!c);
|
|
4243
|
+
if (children.length > 0) summary.children = children;
|
|
4244
|
+
}
|
|
4245
|
+
return summary;
|
|
4246
|
+
}
|
|
4247
|
+
const root = walk(node) ?? { id: node.id, name: node.name, type: node.type };
|
|
4248
|
+
return { root, nodeCount: count, truncated };
|
|
4249
|
+
}
|
|
4250
|
+
function composePullFigmaInstructions(input) {
|
|
4251
|
+
const { frameSummary, frameName, imageUrl, designSystemId, suggestedEntryPath } = input;
|
|
4252
|
+
const parts = [];
|
|
4253
|
+
parts.push(
|
|
4254
|
+
`# Translate a Figma frame into a code artifact
|
|
4255
|
+
|
|
4256
|
+
You are translating the Figma frame "${frameName}" into a real, production-quality HTML/CSS (or React, if more appropriate) artifact with 1:1 visual fidelity \u2014 not a loose interpretation.`
|
|
4257
|
+
);
|
|
4258
|
+
parts.push(
|
|
4259
|
+
`
|
|
4260
|
+
|
|
4261
|
+
## Ground truth
|
|
4262
|
+
|
|
4263
|
+
The JSON below is a deterministic, exact structural summary of the frame \u2014 positions, sizes, fill/stroke colors (hex), corner radii, and text runs (with their exact characters, font, size). Treat it as authoritative: match exact colors, relative positions/sizes, and text content precisely. Do not invent content, copy, or elements that aren't present in this data.${frameSummary.truncated ? ` NOTE: this summary was truncated at ${frameSummary.nodeCount} nodes \u2014 the frame has more content than shown; ask the user if the missing portion matters before proceeding.` : ""}
|
|
4264
|
+
|
|
4265
|
+
\`\`\`json
|
|
4266
|
+
${JSON.stringify(frameSummary.root, null, 2)}
|
|
4267
|
+
\`\`\``
|
|
4268
|
+
);
|
|
4269
|
+
parts.push(
|
|
4270
|
+
imageUrl ? `
|
|
4271
|
+
|
|
4272
|
+
## Visual reference
|
|
4273
|
+
|
|
4274
|
+
A rendered PNG export of this exact frame is available at: ${imageUrl}
|
|
4275
|
+
If your environment can view images from a URL, fetch and look at it to cross-check visual fidelity (layering, gradients, and other details the structural JSON above simplifies away). If it cannot, rely on the structural JSON alone.` : `
|
|
4276
|
+
|
|
4277
|
+
## Visual reference
|
|
4278
|
+
|
|
4279
|
+
No rendered image export was available \u2014 rely on the structural JSON alone.`
|
|
4280
|
+
);
|
|
4281
|
+
parts.push(
|
|
4282
|
+
designSystemId ? `
|
|
4283
|
+
|
|
4284
|
+
## Design system
|
|
4285
|
+
|
|
4286
|
+
This frame is associated with design system "${designSystemId}" \u2014 reuse its component conventions where the frame's structure clearly matches them, but the frame's own exact colors/type from the JSON above still take precedence over the design system's generic tokens.` : `
|
|
4287
|
+
|
|
4288
|
+
## Design system
|
|
4289
|
+
|
|
4290
|
+
No design system was specified \u2014 implement the frame's own exact styling from the JSON above directly (inline styles or plain CSS), without inventing a design-system abstraction that wasn't requested.`
|
|
4291
|
+
);
|
|
4292
|
+
parts.push(
|
|
4293
|
+
`
|
|
4294
|
+
|
|
4295
|
+
## Output
|
|
4296
|
+
|
|
4297
|
+
Write the artifact's entry file at \`${suggestedEntryPath}\` (adjust the filename if there's a clearly better one, e.g. if it collides with something already there) using your own file-editing tools, then call register_open_design_artifact.`
|
|
4298
|
+
);
|
|
4299
|
+
return parts.join("");
|
|
4300
|
+
}
|
|
4301
|
+
|
|
4118
4302
|
// ../core/src/generation/portToAppInstructions.ts
|
|
4119
4303
|
var import_node_fs2 = require("node:fs");
|
|
4120
|
-
var
|
|
4304
|
+
var path3 = __toESM(require("node:path"));
|
|
4121
4305
|
var CANDIDATE_COMPONENT_DIRS = ["src/components", "app/components", "components"];
|
|
4122
4306
|
var KNOWN_COMPONENT_EXTENSIONS = [".tsx", ".jsx", ".vue", ".svelte", ".ts", ".js"];
|
|
4123
4307
|
async function pathExists2(p) {
|
|
@@ -4134,7 +4318,7 @@ function toPascalCase(name) {
|
|
|
4134
4318
|
}
|
|
4135
4319
|
async function suggestTargetComponentPath(workspaceRoot, artifactName) {
|
|
4136
4320
|
for (const candidateDir of CANDIDATE_COMPONENT_DIRS) {
|
|
4137
|
-
const absDir =
|
|
4321
|
+
const absDir = path3.join(workspaceRoot, candidateDir);
|
|
4138
4322
|
if (!await pathExists2(absDir)) continue;
|
|
4139
4323
|
let entries = [];
|
|
4140
4324
|
try {
|
|
@@ -4148,7 +4332,7 @@ async function suggestTargetComponentPath(workspaceRoot, artifactName) {
|
|
|
4148
4332
|
if (ext) extensionCounts.set(ext, (extensionCounts.get(ext) ?? 0) + 1);
|
|
4149
4333
|
}
|
|
4150
4334
|
const guessedExt = [...extensionCounts.entries()].sort((a, b) => b[1] - a[1])[0]?.[0] ?? ".tsx";
|
|
4151
|
-
return
|
|
4335
|
+
return path3.posix.join(candidateDir, `${toPascalCase(artifactName)}${guessedExt}`);
|
|
4152
4336
|
}
|
|
4153
4337
|
return void 0;
|
|
4154
4338
|
}
|
|
@@ -4156,9 +4340,9 @@ function composePortToAppInstructions(input) {
|
|
|
4156
4340
|
const { artifactEntryPath, artifactContent, targetComponentPath, referenceComponentPath } = input;
|
|
4157
4341
|
const parts = [];
|
|
4158
4342
|
parts.push(
|
|
4159
|
-
`# Promote an
|
|
4343
|
+
`# Promote an Open Design prototype into this app's real code
|
|
4160
4344
|
|
|
4161
|
-
You are porting the finished
|
|
4345
|
+
You are porting the finished Open Design artifact below into this workspace's actual, existing application \u2014 reproducing its visual design as idiomatic, real production code, not copying it into the app verbatim.`
|
|
4162
4346
|
);
|
|
4163
4347
|
parts.push(`
|
|
4164
4348
|
|
|
@@ -4209,7 +4393,7 @@ Do NOT wire the new component into routing, navigation, or any other part of the
|
|
|
4209
4393
|
}
|
|
4210
4394
|
|
|
4211
4395
|
// ../core/src/vendored/artifactManifest.ts
|
|
4212
|
-
var
|
|
4396
|
+
var path4 = __toESM(require("node:path"));
|
|
4213
4397
|
var MANIFEST_VERSION = 1;
|
|
4214
4398
|
var MAX_TITLE_LENGTH = 200;
|
|
4215
4399
|
var MAX_SOURCE_SKILL_ID_LENGTH = 128;
|
|
@@ -4217,6 +4401,10 @@ var MAX_DESIGN_SYSTEM_ID_LENGTH = 128;
|
|
|
4217
4401
|
var MAX_SUPPORTING_FILE_LENGTH = 260;
|
|
4218
4402
|
var MAX_SUPPORTING_FILES = 128;
|
|
4219
4403
|
var MAX_METADATA_BYTES = 16 * 1024;
|
|
4404
|
+
var MAX_COLLECTION_ID_LENGTH = 100;
|
|
4405
|
+
var MAX_COLLECTION_NAME_LENGTH = 200;
|
|
4406
|
+
var MAX_SCREEN_ROLE_LENGTH = 100;
|
|
4407
|
+
var MAX_SCREEN_INDEX = 999;
|
|
4220
4408
|
var ALLOWED_KINDS = /* @__PURE__ */ new Set([
|
|
4221
4409
|
"html",
|
|
4222
4410
|
"deck",
|
|
@@ -4337,6 +4525,29 @@ function validateArtifactManifestInput(manifest, entry, options2 = {}) {
|
|
|
4337
4525
|
);
|
|
4338
4526
|
if (dsErr) return { ok: false, error: dsErr };
|
|
4339
4527
|
}
|
|
4528
|
+
if (manifest.collectionId !== void 0 && manifest.collectionId !== null) {
|
|
4529
|
+
const collectionIdErr = validateBoundedString(manifest.collectionId, "artifactManifest.collectionId", MAX_COLLECTION_ID_LENGTH, {
|
|
4530
|
+
allowEmpty: true
|
|
4531
|
+
});
|
|
4532
|
+
if (collectionIdErr) return { ok: false, error: collectionIdErr };
|
|
4533
|
+
}
|
|
4534
|
+
if (manifest.collectionName !== void 0 && manifest.collectionName !== null) {
|
|
4535
|
+
const collectionNameErr = validateBoundedString(manifest.collectionName, "artifactManifest.collectionName", MAX_COLLECTION_NAME_LENGTH, {
|
|
4536
|
+
allowEmpty: true
|
|
4537
|
+
});
|
|
4538
|
+
if (collectionNameErr) return { ok: false, error: collectionNameErr };
|
|
4539
|
+
}
|
|
4540
|
+
if (manifest.screenRole !== void 0 && manifest.screenRole !== null) {
|
|
4541
|
+
const screenRoleErr = validateBoundedString(manifest.screenRole, "artifactManifest.screenRole", MAX_SCREEN_ROLE_LENGTH, {
|
|
4542
|
+
allowEmpty: true
|
|
4543
|
+
});
|
|
4544
|
+
if (screenRoleErr) return { ok: false, error: screenRoleErr };
|
|
4545
|
+
}
|
|
4546
|
+
if (manifest.screenIndex !== void 0 && manifest.screenIndex !== null) {
|
|
4547
|
+
if (typeof manifest.screenIndex !== "number" || !Number.isInteger(manifest.screenIndex) || manifest.screenIndex < 0 || manifest.screenIndex > MAX_SCREEN_INDEX) {
|
|
4548
|
+
return { ok: false, error: `artifactManifest.screenIndex must be an integer between 0 and ${MAX_SCREEN_INDEX}` };
|
|
4549
|
+
}
|
|
4550
|
+
}
|
|
4340
4551
|
if (manifest.metadata !== void 0) {
|
|
4341
4552
|
if (!isPlainObject(manifest.metadata)) {
|
|
4342
4553
|
return { ok: false, error: "artifactManifest.metadata must be a plain object" };
|
|
@@ -4368,12 +4579,27 @@ function sanitizeManifest(manifest, entry, options2 = {}) {
|
|
|
4368
4579
|
updatedAt: options2.preserveUpdatedAt && typeof manifest.updatedAt === "string" ? manifest.updatedAt : now,
|
|
4369
4580
|
sourceSkillId: manifest.sourceSkillId,
|
|
4370
4581
|
designSystemId: manifest.designSystemId ?? void 0,
|
|
4582
|
+
collectionId: manifest.collectionId ?? void 0,
|
|
4583
|
+
collectionName: manifest.collectionName ?? void 0,
|
|
4584
|
+
screenRole: manifest.screenRole ?? void 0,
|
|
4585
|
+
screenIndex: typeof manifest.screenIndex === "number" ? manifest.screenIndex : void 0,
|
|
4371
4586
|
metadata: manifest.metadata
|
|
4372
4587
|
};
|
|
4373
4588
|
}
|
|
4589
|
+
function parsePersistedManifest(raw, fallbackEntry) {
|
|
4590
|
+
try {
|
|
4591
|
+
const parsed = JSON.parse(raw);
|
|
4592
|
+
if (!parsed || parsed.version !== MANIFEST_VERSION) return null;
|
|
4593
|
+
const entry = typeof parsed.entry === "string" && parsed.entry ? parsed.entry : fallbackEntry;
|
|
4594
|
+
const result = validateArtifactManifestInput(parsed, entry, { preserveUpdatedAt: true });
|
|
4595
|
+
return result.ok ? result.value : null;
|
|
4596
|
+
} catch {
|
|
4597
|
+
return null;
|
|
4598
|
+
}
|
|
4599
|
+
}
|
|
4374
4600
|
function inferLegacyManifest(entry) {
|
|
4375
4601
|
const lower = entry.toLowerCase();
|
|
4376
|
-
const ext =
|
|
4602
|
+
const ext = path4.extname(lower);
|
|
4377
4603
|
const isDeck = ext === ".html" && (lower.includes("deck") || lower.includes("slides") || lower.includes("pitch"));
|
|
4378
4604
|
if (ext === ".html" || ext === ".htm") {
|
|
4379
4605
|
return {
|
|
@@ -4416,7 +4642,7 @@ function inferLegacyManifest(entry) {
|
|
|
4416
4642
|
|
|
4417
4643
|
// ../core/src/vendored/artifactCreate.ts
|
|
4418
4644
|
var import_node_fs3 = require("node:fs");
|
|
4419
|
-
var
|
|
4645
|
+
var path5 = __toESM(require("node:path"));
|
|
4420
4646
|
var ArtifactManifestRequiredError = class extends Error {
|
|
4421
4647
|
constructor(entry) {
|
|
4422
4648
|
super(`artifactManifest is required for ${entry}; no safe default manifest can be inferred`);
|
|
@@ -4450,9 +4676,9 @@ function artifactManifestSidecarPath(entryPath) {
|
|
|
4450
4676
|
return `${entryPath}.artifact.json`;
|
|
4451
4677
|
}
|
|
4452
4678
|
function assertWorkspaceRelative(workspaceRoot, entryPath) {
|
|
4453
|
-
const abs =
|
|
4454
|
-
const rel =
|
|
4455
|
-
if (rel.startsWith("..") ||
|
|
4679
|
+
const abs = path5.isAbsolute(entryPath) ? entryPath : path5.join(workspaceRoot, entryPath);
|
|
4680
|
+
const rel = path5.relative(workspaceRoot, abs);
|
|
4681
|
+
if (rel.startsWith("..") || path5.isAbsolute(rel)) {
|
|
4456
4682
|
throw new Error(`entryPath escapes the workspace: ${entryPath}`);
|
|
4457
4683
|
}
|
|
4458
4684
|
return abs;
|
|
@@ -4518,7 +4744,7 @@ async function setActiveDesignSystem(designSystemId, store, getDesignSystem) {
|
|
|
4518
4744
|
|
|
4519
4745
|
// ../core/src/workspace/appDetection.ts
|
|
4520
4746
|
var import_node_fs4 = require("node:fs");
|
|
4521
|
-
var
|
|
4747
|
+
var path6 = __toESM(require("node:path"));
|
|
4522
4748
|
var FRAMEWORK_PACKAGES = {
|
|
4523
4749
|
react: "React",
|
|
4524
4750
|
next: "Next.js",
|
|
@@ -4533,7 +4759,7 @@ async function detectExistingApp(workspaceRoot) {
|
|
|
4533
4759
|
if (!workspaceRoot) return [];
|
|
4534
4760
|
let raw;
|
|
4535
4761
|
try {
|
|
4536
|
-
raw = await import_node_fs4.promises.readFile(
|
|
4762
|
+
raw = await import_node_fs4.promises.readFile(path6.join(workspaceRoot, "package.json"), "utf8");
|
|
4537
4763
|
} catch {
|
|
4538
4764
|
return [];
|
|
4539
4765
|
}
|
|
@@ -4553,14 +4779,14 @@ async function detectExistingApp(workspaceRoot) {
|
|
|
4553
4779
|
|
|
4554
4780
|
// ../core/src/workspace/artifactComments.ts
|
|
4555
4781
|
var import_node_fs5 = require("node:fs");
|
|
4556
|
-
var
|
|
4782
|
+
var path7 = __toESM(require("node:path"));
|
|
4557
4783
|
function commentsSidecarPath(entryPath) {
|
|
4558
4784
|
return `${entryPath}.comments.json`;
|
|
4559
4785
|
}
|
|
4560
4786
|
function assertWorkspaceRelative2(workspaceRoot, entryPath) {
|
|
4561
|
-
const abs =
|
|
4562
|
-
const rel =
|
|
4563
|
-
if (rel.startsWith("..") ||
|
|
4787
|
+
const abs = path7.isAbsolute(entryPath) ? entryPath : path7.join(workspaceRoot, entryPath);
|
|
4788
|
+
const rel = path7.relative(workspaceRoot, abs);
|
|
4789
|
+
if (rel.startsWith("..") || path7.isAbsolute(rel)) {
|
|
4564
4790
|
throw new Error(`entryPath escapes the workspace: ${entryPath}`);
|
|
4565
4791
|
}
|
|
4566
4792
|
return abs;
|
|
@@ -4576,39 +4802,95 @@ async function readArtifactComments(workspaceRoot, entryPath) {
|
|
|
4576
4802
|
}
|
|
4577
4803
|
}
|
|
4578
4804
|
|
|
4579
|
-
// ../core/src/workspace/
|
|
4805
|
+
// ../core/src/workspace/collectionScan.ts
|
|
4580
4806
|
var import_node_fs6 = require("node:fs");
|
|
4581
|
-
var
|
|
4807
|
+
var path8 = __toESM(require("node:path"));
|
|
4808
|
+
var ARTIFACT_SIDECAR_SUFFIX = ".artifact.json";
|
|
4809
|
+
async function pathExists3(p) {
|
|
4810
|
+
try {
|
|
4811
|
+
await import_node_fs6.promises.access(p);
|
|
4812
|
+
return true;
|
|
4813
|
+
} catch {
|
|
4814
|
+
return false;
|
|
4815
|
+
}
|
|
4816
|
+
}
|
|
4817
|
+
async function walkArtifactSidecars(absDir, workspaceRoot, out) {
|
|
4818
|
+
let entries;
|
|
4819
|
+
try {
|
|
4820
|
+
entries = await import_node_fs6.promises.readdir(absDir, { withFileTypes: true });
|
|
4821
|
+
} catch {
|
|
4822
|
+
return;
|
|
4823
|
+
}
|
|
4824
|
+
for (const entry of entries) {
|
|
4825
|
+
const absChild = path8.join(absDir, entry.name);
|
|
4826
|
+
if (entry.isDirectory()) {
|
|
4827
|
+
await walkArtifactSidecars(absChild, workspaceRoot, out);
|
|
4828
|
+
continue;
|
|
4829
|
+
}
|
|
4830
|
+
if (!entry.isFile() || !entry.name.endsWith(ARTIFACT_SIDECAR_SUFFIX)) continue;
|
|
4831
|
+
const entryPath = path8.relative(workspaceRoot, absChild).slice(0, -ARTIFACT_SIDECAR_SUFFIX.length).split(path8.sep).join("/");
|
|
4832
|
+
try {
|
|
4833
|
+
const raw = await import_node_fs6.promises.readFile(absChild, "utf8");
|
|
4834
|
+
const manifest = parsePersistedManifest(raw, entryPath);
|
|
4835
|
+
if (manifest) out.push({ entryPath, manifest });
|
|
4836
|
+
} catch {
|
|
4837
|
+
}
|
|
4838
|
+
}
|
|
4839
|
+
}
|
|
4840
|
+
function toScreen(entryPath, manifest) {
|
|
4841
|
+
return {
|
|
4842
|
+
entryPath,
|
|
4843
|
+
title: typeof manifest.title === "string" && manifest.title ? manifest.title : entryPath,
|
|
4844
|
+
screenIndex: typeof manifest.screenIndex === "number" ? manifest.screenIndex : void 0,
|
|
4845
|
+
screenRole: typeof manifest.screenRole === "string" ? manifest.screenRole : void 0
|
|
4846
|
+
};
|
|
4847
|
+
}
|
|
4848
|
+
function sortScreens(screens) {
|
|
4849
|
+
return [...screens].sort((a, b) => (a.screenIndex ?? Number.MAX_SAFE_INTEGER) - (b.screenIndex ?? Number.MAX_SAFE_INTEGER));
|
|
4850
|
+
}
|
|
4851
|
+
async function findCollectionArtifacts(workspaceRoot, outputDir, collectionId) {
|
|
4852
|
+
const absOutputDir = path8.join(workspaceRoot, outputDir);
|
|
4853
|
+
if (!await pathExists3(absOutputDir)) return [];
|
|
4854
|
+
const found = [];
|
|
4855
|
+
await walkArtifactSidecars(absOutputDir, workspaceRoot, found);
|
|
4856
|
+
return sortScreens(
|
|
4857
|
+
found.filter(({ manifest }) => manifest.collectionId === collectionId).map(({ entryPath, manifest }) => toScreen(entryPath, manifest))
|
|
4858
|
+
);
|
|
4859
|
+
}
|
|
4860
|
+
|
|
4861
|
+
// ../core/src/workspace/remixExample.ts
|
|
4862
|
+
var import_node_fs7 = require("node:fs");
|
|
4863
|
+
var path9 = __toESM(require("node:path"));
|
|
4582
4864
|
async function listFilesRecursive(dir, baseDir) {
|
|
4583
4865
|
const out = [];
|
|
4584
|
-
const entries = await
|
|
4866
|
+
const entries = await import_node_fs7.promises.readdir(dir, { withFileTypes: true });
|
|
4585
4867
|
for (const entry of entries) {
|
|
4586
|
-
const full =
|
|
4868
|
+
const full = path9.join(dir, entry.name);
|
|
4587
4869
|
if (entry.isDirectory()) {
|
|
4588
4870
|
out.push(...await listFilesRecursive(full, baseDir));
|
|
4589
4871
|
} else if (entry.isFile()) {
|
|
4590
|
-
out.push(
|
|
4872
|
+
out.push(path9.relative(baseDir, full).split(path9.sep).join("/"));
|
|
4591
4873
|
}
|
|
4592
4874
|
}
|
|
4593
4875
|
return out;
|
|
4594
4876
|
}
|
|
4595
4877
|
async function copyExampleArtifact(options2) {
|
|
4596
|
-
const sourceHtmlAbs =
|
|
4597
|
-
const sourceDir =
|
|
4598
|
-
const destHtmlAbs =
|
|
4599
|
-
const destRel =
|
|
4600
|
-
if (destRel.startsWith("..") ||
|
|
4878
|
+
const sourceHtmlAbs = path9.join(options2.assetsRoot, options2.exampleArtifactPath);
|
|
4879
|
+
const sourceDir = path9.dirname(sourceHtmlAbs);
|
|
4880
|
+
const destHtmlAbs = path9.isAbsolute(options2.entryPath) ? options2.entryPath : path9.join(options2.workspaceRoot, options2.entryPath);
|
|
4881
|
+
const destRel = path9.relative(options2.workspaceRoot, destHtmlAbs);
|
|
4882
|
+
if (destRel.startsWith("..") || path9.isAbsolute(destRel)) {
|
|
4601
4883
|
throw new Error(`entryPath escapes the workspace: ${options2.entryPath}`);
|
|
4602
4884
|
}
|
|
4603
|
-
const destDir =
|
|
4604
|
-
await
|
|
4605
|
-
await
|
|
4606
|
-
const sourceAssetsDir =
|
|
4885
|
+
const destDir = path9.dirname(destHtmlAbs);
|
|
4886
|
+
await import_node_fs7.promises.mkdir(destDir, { recursive: true });
|
|
4887
|
+
await import_node_fs7.promises.copyFile(sourceHtmlAbs, destHtmlAbs);
|
|
4888
|
+
const sourceAssetsDir = path9.join(sourceDir, "assets");
|
|
4607
4889
|
let supportingFiles = [];
|
|
4608
4890
|
try {
|
|
4609
|
-
await
|
|
4610
|
-
const destAssetsDir =
|
|
4611
|
-
await
|
|
4891
|
+
await import_node_fs7.promises.access(sourceAssetsDir);
|
|
4892
|
+
const destAssetsDir = path9.join(destDir, "assets");
|
|
4893
|
+
await import_node_fs7.promises.cp(sourceAssetsDir, destAssetsDir, { recursive: true });
|
|
4612
4894
|
supportingFiles = await listFilesRecursive(destAssetsDir, destDir);
|
|
4613
4895
|
} catch {
|
|
4614
4896
|
}
|
|
@@ -4616,7 +4898,7 @@ async function copyExampleArtifact(options2) {
|
|
|
4616
4898
|
}
|
|
4617
4899
|
|
|
4618
4900
|
// src/env.ts
|
|
4619
|
-
var
|
|
4901
|
+
var path10 = __toESM(require("node:path"));
|
|
4620
4902
|
function getWorkspaceRoot() {
|
|
4621
4903
|
return process.env.OPEN_DESIGN_WORKSPACE_ROOT?.trim() || process.cwd();
|
|
4622
4904
|
}
|
|
@@ -4624,19 +4906,22 @@ var DEFAULT_OUTPUT_DIR = ".open-design";
|
|
|
4624
4906
|
function getOutputDirectory() {
|
|
4625
4907
|
return process.env.OPEN_DESIGN_OUTPUT_DIR?.trim() || DEFAULT_OUTPUT_DIR;
|
|
4626
4908
|
}
|
|
4909
|
+
function getFigmaToken() {
|
|
4910
|
+
return process.env.OPEN_DESIGN_FIGMA_TOKEN?.trim() || void 0;
|
|
4911
|
+
}
|
|
4627
4912
|
function getAssetsRoot() {
|
|
4628
4913
|
const contentPackageJson = require.resolve("@feimacode/open-design-agent-kit-content/package.json");
|
|
4629
|
-
return
|
|
4914
|
+
return path10.join(path10.dirname(contentPackageJson), "assets", "open-design");
|
|
4630
4915
|
}
|
|
4631
4916
|
|
|
4632
4917
|
// src/store.ts
|
|
4633
|
-
var
|
|
4634
|
-
var
|
|
4918
|
+
var import_node_fs8 = require("node:fs");
|
|
4919
|
+
var path11 = __toESM(require("node:path"));
|
|
4635
4920
|
function createFileActiveDesignSystemStore(workspaceRoot) {
|
|
4636
|
-
const configPath =
|
|
4921
|
+
const configPath = path11.join(workspaceRoot, ".open-design", "config.json");
|
|
4637
4922
|
async function readConfig() {
|
|
4638
4923
|
try {
|
|
4639
|
-
const raw = await
|
|
4924
|
+
const raw = await import_node_fs8.promises.readFile(configPath, "utf8");
|
|
4640
4925
|
const parsed = JSON.parse(raw);
|
|
4641
4926
|
return typeof parsed === "object" && parsed !== null ? parsed : {};
|
|
4642
4927
|
} catch {
|
|
@@ -4656,14 +4941,14 @@ function createFileActiveDesignSystemStore(workspaceRoot) {
|
|
|
4656
4941
|
} else {
|
|
4657
4942
|
delete config.activeDesignSystemId;
|
|
4658
4943
|
}
|
|
4659
|
-
await
|
|
4660
|
-
await
|
|
4944
|
+
await import_node_fs8.promises.mkdir(path11.dirname(configPath), { recursive: true });
|
|
4945
|
+
await import_node_fs8.promises.writeFile(configPath, JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
4661
4946
|
}
|
|
4662
4947
|
};
|
|
4663
4948
|
}
|
|
4664
4949
|
|
|
4665
4950
|
// src/tools.ts
|
|
4666
|
-
var
|
|
4951
|
+
var path12 = __toESM(require("node:path"));
|
|
4667
4952
|
function slugify(input) {
|
|
4668
4953
|
const slug = input.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60);
|
|
4669
4954
|
return slug || "artifact";
|
|
@@ -4671,7 +4956,7 @@ function slugify(input) {
|
|
|
4671
4956
|
function suggestEntryPath(ctx, brief, skillName) {
|
|
4672
4957
|
const slugSource = brief.trim().length > 0 ? brief : skillName;
|
|
4673
4958
|
const slug = slugify(slugSource);
|
|
4674
|
-
return
|
|
4959
|
+
return path12.posix.join(ctx.outputDir, slug, `${slug}.html`);
|
|
4675
4960
|
}
|
|
4676
4961
|
var KIND_TO_RENDERER = {
|
|
4677
4962
|
html: "html",
|
|
@@ -4709,6 +4994,19 @@ async function listSkills(ctx, input) {
|
|
|
4709
4994
|
exampleArtifactPath: s.exampleArtifactPath
|
|
4710
4995
|
}));
|
|
4711
4996
|
}
|
|
4997
|
+
async function listRemixablePrompts(ctx) {
|
|
4998
|
+
const skills = await ctx.contentIndex.listSkills(void 0, void 0, "example", true);
|
|
4999
|
+
return skills.map((s) => ({
|
|
5000
|
+
name: s.id.replace(/:/g, "-"),
|
|
5001
|
+
publicId: s.id,
|
|
5002
|
+
displayName: s.name,
|
|
5003
|
+
examplePrompt: s.examplePrompt
|
|
5004
|
+
}));
|
|
5005
|
+
}
|
|
5006
|
+
function buildRemixPromptMessage(prompt) {
|
|
5007
|
+
const brief = prompt.examplePrompt ? ` ${prompt.examplePrompt}` : "";
|
|
5008
|
+
return `Remix the Open Design example "${prompt.displayName}" \u2014 call the open-design MCP server's remix_open_design_example tool with skillId "${prompt.publicId}".${brief}`;
|
|
5009
|
+
}
|
|
4712
5010
|
async function listDesignSystems(ctx, input) {
|
|
4713
5011
|
const designSystems = await ctx.contentIndex.listDesignSystems(input.query, input.category);
|
|
4714
5012
|
const activeId = await ctx.store.get();
|
|
@@ -4735,8 +5033,25 @@ async function prepareBrief(ctx, input) {
|
|
|
4735
5033
|
const { designSystemId, designSystem } = resolution;
|
|
4736
5034
|
const allCraftSections = await ctx.contentIndex.craftSections();
|
|
4737
5035
|
const craftSections = selectCraftSections(allCraftSections, designSystem?.craftSuggested);
|
|
4738
|
-
const suggestedEntryPath = suggestEntryPath(ctx, input.brief, skill.name);
|
|
4739
5036
|
const existingAppFrameworks = await detectExistingApp(ctx.workspaceRoot);
|
|
5037
|
+
let suggestedEntryPath;
|
|
5038
|
+
let collectionContext;
|
|
5039
|
+
if (input.collectionId) {
|
|
5040
|
+
if (!input.screenRole) {
|
|
5041
|
+
return `screenRole is required when collectionId is given \u2014 a short label for this screen's role in the flow, e.g. "splash", "value-prop", "checkout".`;
|
|
5042
|
+
}
|
|
5043
|
+
const siblings = await findCollectionArtifacts(ctx.workspaceRoot, ctx.outputDir, input.collectionId);
|
|
5044
|
+
suggestedEntryPath = suggestCollectionScreenEntryPath(ctx.outputDir, input.collectionId, slugify(input.screenRole));
|
|
5045
|
+
collectionContext = {
|
|
5046
|
+
collectionName: input.collectionName ?? input.collectionId,
|
|
5047
|
+
index: siblings.length + 1,
|
|
5048
|
+
total: input.screenTotal ?? siblings.length + 1,
|
|
5049
|
+
role: input.screenRole,
|
|
5050
|
+
siblingScreens: siblings.map((s) => ({ role: s.screenRole ?? "?", title: s.title }))
|
|
5051
|
+
};
|
|
5052
|
+
} else {
|
|
5053
|
+
suggestedEntryPath = suggestEntryPath(ctx, input.brief, skill.name);
|
|
5054
|
+
}
|
|
4740
5055
|
const instructions = composeInstructions({
|
|
4741
5056
|
skillName: skill.name,
|
|
4742
5057
|
skillBody: skill.body,
|
|
@@ -4745,7 +5060,8 @@ async function prepareBrief(ctx, input) {
|
|
|
4745
5060
|
craftSections,
|
|
4746
5061
|
brief: input.brief,
|
|
4747
5062
|
suggestedEntryPath,
|
|
4748
|
-
existingAppFrameworks
|
|
5063
|
+
existingAppFrameworks,
|
|
5064
|
+
collectionContext
|
|
4749
5065
|
});
|
|
4750
5066
|
const payload = {
|
|
4751
5067
|
instructions,
|
|
@@ -4773,14 +5089,18 @@ async function registerArtifact(ctx, input) {
|
|
|
4773
5089
|
title: input.title,
|
|
4774
5090
|
supportingFiles: input.supportingFiles,
|
|
4775
5091
|
sourceSkillId: input.sourceSkillId,
|
|
4776
|
-
designSystemId: input.designSystemId
|
|
5092
|
+
designSystemId: input.designSystemId,
|
|
5093
|
+
collectionId: input.collectionId,
|
|
5094
|
+
collectionName: input.collectionName,
|
|
5095
|
+
screenIndex: input.screenIndex,
|
|
5096
|
+
screenRole: input.screenRole
|
|
4777
5097
|
}
|
|
4778
5098
|
});
|
|
4779
5099
|
return `Artifact registered at ${input.entryPath}.artifact.json
|
|
4780
5100
|
|
|
4781
5101
|
${JSON.stringify(manifest, null, 2)}
|
|
4782
5102
|
|
|
4783
|
-
Written to ${
|
|
5103
|
+
Written to ${path12.join(ctx.workspaceRoot, input.entryPath)}. This host has no live preview editor \u2014 there is nothing further to open.`;
|
|
4784
5104
|
} catch (err) {
|
|
4785
5105
|
const message = err instanceof Error ? err.message : String(err);
|
|
4786
5106
|
return `Failed to register artifact: ${message}`;
|
|
@@ -4803,17 +5123,17 @@ async function getArtifact(ctx, input) {
|
|
|
4803
5123
|
}
|
|
4804
5124
|
async function setActiveDesignSystemTool(ctx, input) {
|
|
4805
5125
|
const result = await setActiveDesignSystem(input.designSystemId, ctx.store, (id) => ctx.contentIndex.getDesignSystem(id));
|
|
4806
|
-
if (result.outcome === "cleared") return "Cleared the active
|
|
5126
|
+
if (result.outcome === "cleared") return "Cleared the active Open Design design system.";
|
|
4807
5127
|
if (result.outcome === "unknown") {
|
|
4808
5128
|
const available = (await ctx.contentIndex.listDesignSystems()).map((d) => d.id).slice(0, 20);
|
|
4809
5129
|
return `Unknown designSystemId "${result.unknownId}". Call list_open_design_design_systems to see available ids. A few available ids: ${available.join(", ")}`;
|
|
4810
5130
|
}
|
|
4811
|
-
return `Active
|
|
5131
|
+
return `Active Open Design design system set to "${input.designSystemId}" (${result.designSystem.name}).`;
|
|
4812
5132
|
}
|
|
4813
5133
|
async function createCustomDesignSystem(ctx, input) {
|
|
4814
5134
|
const slug = slugify(input.name);
|
|
4815
5135
|
const id = `user:${slug}`;
|
|
4816
|
-
const suggestedEntryPath =
|
|
5136
|
+
const suggestedEntryPath = path12.posix.join(ctx.outputDir, "design-systems", slug, "DESIGN.md");
|
|
4817
5137
|
const evidence = input.sourceUrl ? await extractBrandEvidence(input.sourceUrl) : void 0;
|
|
4818
5138
|
const instructions = composeCustomDesignSystemInstructions({ name: input.name, brief: input.brief, suggestedEntryPath, id, evidence });
|
|
4819
5139
|
return JSON.stringify({ instructions, suggestedEntryPath, id }, null, 2);
|
|
@@ -4823,7 +5143,7 @@ async function portToAppCode(ctx, input) {
|
|
|
4823
5143
|
if (!artifact) {
|
|
4824
5144
|
return `No artifact found at ${input.entryPath}. It may not have been written yet.`;
|
|
4825
5145
|
}
|
|
4826
|
-
const artifactName = typeof artifact.manifest?.title === "string" ? artifact.manifest.title :
|
|
5146
|
+
const artifactName = typeof artifact.manifest?.title === "string" ? artifact.manifest.title : path12.basename(input.entryPath, path12.extname(input.entryPath));
|
|
4827
5147
|
const targetComponentPath = input.targetComponentPath ?? await suggestTargetComponentPath(ctx.workspaceRoot, artifactName);
|
|
4828
5148
|
const instructions = composePortToAppInstructions({
|
|
4829
5149
|
artifactEntryPath: input.entryPath,
|
|
@@ -4833,6 +5153,33 @@ async function portToAppCode(ctx, input) {
|
|
|
4833
5153
|
});
|
|
4834
5154
|
return JSON.stringify({ instructions, suggestedTargetComponentPath: targetComponentPath }, null, 2);
|
|
4835
5155
|
}
|
|
5156
|
+
async function pullFigmaFrame(ctx, input) {
|
|
5157
|
+
if (!ctx.figmaToken) {
|
|
5158
|
+
return "No Figma access token configured. Set the OPEN_DESIGN_FIGMA_TOKEN environment variable for this MCP server to a Figma personal access token (Figma \u2192 Settings \u2192 Personal access tokens) and try again.";
|
|
5159
|
+
}
|
|
5160
|
+
const ref = parseFigmaUrl(input.figmaUrl);
|
|
5161
|
+
if (!ref) {
|
|
5162
|
+
return `"${input.figmaUrl}" does not look like a Figma file/design URL.`;
|
|
5163
|
+
}
|
|
5164
|
+
let node;
|
|
5165
|
+
try {
|
|
5166
|
+
node = await fetchFigmaNode(ctx.figmaToken, ref);
|
|
5167
|
+
} catch (err) {
|
|
5168
|
+
return err instanceof FigmaApiError ? err.message : `Failed to fetch the Figma frame: ${err instanceof Error ? err.message : String(err)}`;
|
|
5169
|
+
}
|
|
5170
|
+
const frameSummary = summarizeFigmaNode(node);
|
|
5171
|
+
const imageUrl = await fetchFigmaFrameImage(ctx.figmaToken, ref.fileKey, ref.nodeId);
|
|
5172
|
+
const slug = slugify(node.name);
|
|
5173
|
+
const suggestedEntryPath = path12.posix.join(ctx.outputDir, "figma", `${slug}.html`);
|
|
5174
|
+
const instructions = composePullFigmaInstructions({
|
|
5175
|
+
frameSummary,
|
|
5176
|
+
frameName: node.name,
|
|
5177
|
+
imageUrl,
|
|
5178
|
+
designSystemId: input.designSystemId,
|
|
5179
|
+
suggestedEntryPath
|
|
5180
|
+
});
|
|
5181
|
+
return JSON.stringify({ instructions, suggestedEntryPath }, null, 2);
|
|
5182
|
+
}
|
|
4836
5183
|
async function remixExample(ctx, input) {
|
|
4837
5184
|
const skill = await ctx.contentIndex.getSkill(input.skillId);
|
|
4838
5185
|
if (!skill) {
|
|
@@ -4877,7 +5224,7 @@ var TOOL_DEFS = [
|
|
|
4877
5224
|
{
|
|
4878
5225
|
tool: {
|
|
4879
5226
|
name: "list_open_design_skills",
|
|
4880
|
-
description: "Lists available
|
|
5227
|
+
description: "Lists available Open Design skills, design templates, and remixable examples \u2014 reusable design-task recipes, rendering styles, and (for 'example' entries) actual starting artifacts bundled with this server. Each result's id is namespaced as 'od:<mode>:<name>' (e.g. 'od:deck:guizang-ppt') \u2014 pass this full id as skillId to prepare_open_design_brief or remix_open_design_example. Each result's 'source' field is 'skill', 'design-template', or 'example'. Some results include an 'examplePrompt' \u2014 prefer it (or lightly adapt it) over inventing your own brief when it closely fits. A result with a non-empty 'exampleArtifactPath' has an actual rendered starting artifact \u2014 prefer remix_open_design_example over prepare_open_design_brief for those. Optionally filter by a free-text query, an exact mode, an exact source, and/or remixableOnly to see only entries with a rendered starting artifact. If a query returns few or no results, call this tool again with a broader query or no arguments at all to browse the full catalog \u2014 do NOT search the filesystem, grep, or read any file (this catalog is not stored anywhere as a single readable file such as a content.json, index, or schema file; it exists only inside this server's own runtime and is reachable exclusively through this tool).",
|
|
4881
5228
|
inputSchema: {
|
|
4882
5229
|
type: "object",
|
|
4883
5230
|
properties: {
|
|
@@ -4904,7 +5251,7 @@ var TOOL_DEFS = [
|
|
|
4904
5251
|
{
|
|
4905
5252
|
tool: {
|
|
4906
5253
|
name: "list_open_design_design_systems",
|
|
4907
|
-
description: "Lists available
|
|
5254
|
+
description: "Lists available Open Design design systems \u2014 brand-inspired visual token sets (palette, typography, spacing, component rules), each with a 'category' and an 'active' flag marking the current active design system, if any. Optionally filter by a free-text query and/or an exact category. If a query returns few or no results, call this tool again with a broader query or no arguments at all to browse the full catalog \u2014 do NOT search the filesystem, grep, or read any file (this catalog is not stored anywhere as a single readable file such as a content.json, index, or schema file; it exists only inside this server's own runtime and is reachable exclusively through this tool).",
|
|
4908
5255
|
inputSchema: {
|
|
4909
5256
|
type: "object",
|
|
4910
5257
|
properties: {
|
|
@@ -4918,7 +5265,7 @@ var TOOL_DEFS = [
|
|
|
4918
5265
|
{
|
|
4919
5266
|
tool: {
|
|
4920
5267
|
name: "prepare_open_design_brief",
|
|
4921
|
-
description: "Composes generation instructions for an
|
|
5268
|
+
description: "Composes generation instructions for an Open Design artifact by combining the chosen skill's workflow, an optional design system's visual tokens, universal craft rules, and the user's brief. Returns an 'instructions' string you must follow, plus a 'suggestedEntryPath'. Does NOT write any files \u2014 after calling it, author the entry file (and any supporting files) yourself with your normal file-editing tools, then call register_open_design_artifact.",
|
|
4922
5269
|
inputSchema: {
|
|
4923
5270
|
type: "object",
|
|
4924
5271
|
properties: {
|
|
@@ -4927,12 +5274,31 @@ var TOOL_DEFS = [
|
|
|
4927
5274
|
type: "string",
|
|
4928
5275
|
description: "Optional design system id from list_open_design_design_systems. If omitted, falls back to the active design system. If given explicitly, it also becomes the new active one."
|
|
4929
5276
|
},
|
|
4930
|
-
brief: { type: "string", description: "The user's design brief / request, in their own words." }
|
|
5277
|
+
brief: { type: "string", description: "The user's design brief / request, in their own words." },
|
|
5278
|
+
collectionId: {
|
|
5279
|
+
type: "string",
|
|
5280
|
+
description: 'Set this (a slug you derive once from the collection name, reused verbatim on every call) when this artifact is one screen of a multi-screen design collection \u2014 see the "design collections" guidance for the full workflow.'
|
|
5281
|
+
},
|
|
5282
|
+
collectionName: {
|
|
5283
|
+
type: "string",
|
|
5284
|
+
description: 'A short display name for the collection, e.g. "Fintech Onboarding Flow". Required together with collectionId.'
|
|
5285
|
+
},
|
|
5286
|
+
screenRole: {
|
|
5287
|
+
type: "string",
|
|
5288
|
+
description: `Required together with collectionId \u2014 this screen's short role label, e.g. "splash", "value-prop", "checkout".`
|
|
5289
|
+
},
|
|
5290
|
+
screenTotal: {
|
|
5291
|
+
type: "number",
|
|
5292
|
+
description: `Optional: the total number of screens you've planned for this collection, for an accurate "screen N of M" framing.`
|
|
5293
|
+
}
|
|
4931
5294
|
},
|
|
4932
5295
|
required: ["skillId", "brief"]
|
|
4933
5296
|
}
|
|
4934
5297
|
},
|
|
4935
|
-
handler: (ctx, args) => prepareBrief(
|
|
5298
|
+
handler: (ctx, args) => prepareBrief(
|
|
5299
|
+
ctx,
|
|
5300
|
+
args
|
|
5301
|
+
)
|
|
4936
5302
|
},
|
|
4937
5303
|
{
|
|
4938
5304
|
tool: {
|
|
@@ -4954,7 +5320,11 @@ var TOOL_DEFS = [
|
|
|
4954
5320
|
description: "Workspace-relative paths to sibling files the entry depends on, relative to the entry file's directory."
|
|
4955
5321
|
},
|
|
4956
5322
|
sourceSkillId: { type: "string", description: "The skillId used to generate this artifact, if any." },
|
|
4957
|
-
designSystemId: { type: "string", description: "The designSystemId used to generate this artifact, if any." }
|
|
5323
|
+
designSystemId: { type: "string", description: "The designSystemId used to generate this artifact, if any." },
|
|
5324
|
+
collectionId: { type: "string", description: "The same collectionId passed to prepare_open_design_brief, if this screen is part of a design collection." },
|
|
5325
|
+
collectionName: { type: "string", description: "The same collectionName passed to prepare_open_design_brief, if this screen is part of a design collection." },
|
|
5326
|
+
screenIndex: { type: "number", description: "0-based position of this screen within its collection (0 for the first screen, 1 for the second, ...)." },
|
|
5327
|
+
screenRole: { type: "string", description: "The same screenRole passed to prepare_open_design_brief, if this screen is part of a design collection." }
|
|
4958
5328
|
},
|
|
4959
5329
|
required: ["entryPath", "kind", "title"]
|
|
4960
5330
|
}
|
|
@@ -4967,7 +5337,7 @@ var TOOL_DEFS = [
|
|
|
4967
5337
|
{
|
|
4968
5338
|
tool: {
|
|
4969
5339
|
name: "get_open_design_artifact",
|
|
4970
|
-
description: "Reads back a registered
|
|
5340
|
+
description: "Reads back a registered Open Design artifact: its manifest, entry file content, supporting file list, and any open comments. This host has no comment-authoring UI, so 'openComments' will normally be empty.",
|
|
4971
5341
|
inputSchema: {
|
|
4972
5342
|
type: "object",
|
|
4973
5343
|
properties: { entryPath: { type: "string", description: "Workspace-relative path to the artifact's entry file." } },
|
|
@@ -4979,7 +5349,7 @@ var TOOL_DEFS = [
|
|
|
4979
5349
|
{
|
|
4980
5350
|
tool: {
|
|
4981
5351
|
name: "set_active_design_system",
|
|
4982
|
-
description: "Sets (or clears, if designSystemId is omitted/empty) the active
|
|
5352
|
+
description: "Sets (or clears, if designSystemId is omitted/empty) the active Open Design design system, persisted for this workspace. Used automatically by prepare_open_design_brief whenever called without an explicit designSystemId.",
|
|
4983
5353
|
inputSchema: {
|
|
4984
5354
|
type: "object",
|
|
4985
5355
|
properties: {
|
|
@@ -5011,7 +5381,7 @@ var TOOL_DEFS = [
|
|
|
5011
5381
|
{
|
|
5012
5382
|
tool: {
|
|
5013
5383
|
name: "port_open_design_artifact_to_app",
|
|
5014
|
-
description: "Composes instructions for porting a finished
|
|
5384
|
+
description: "Composes instructions for porting a finished Open Design artifact's design into this workspace's actual, existing application as real, idiomatic production code. Does NOT write any files. Directs the model to ground the port in a real existing component and explicitly excludes routing/navigation wiring.",
|
|
5015
5385
|
inputSchema: {
|
|
5016
5386
|
type: "object",
|
|
5017
5387
|
properties: {
|
|
@@ -5024,10 +5394,25 @@ var TOOL_DEFS = [
|
|
|
5024
5394
|
},
|
|
5025
5395
|
handler: (ctx, args) => portToAppCode(ctx, args)
|
|
5026
5396
|
},
|
|
5397
|
+
{
|
|
5398
|
+
tool: {
|
|
5399
|
+
name: "pull_open_design_figma_frame",
|
|
5400
|
+
description: `Composes instructions for translating a Figma frame into a real code artifact with 1:1 visual fidelity. Fetches the frame's structure via the Figma REST API (requires the OPEN_DESIGN_FIGMA_TOKEN environment variable \u2014 a Figma personal access token) and a best-effort rendered image export, then returns 'instructions' embedding a deterministic structural summary as ground truth. Does NOT write any files \u2014 after calling it, author the entry file yourself with your normal file-editing tools, then call register_open_design_artifact. figmaUrl must be a frame-scoped link (Figma's "Copy link to selection"), not a bare file link.`,
|
|
5401
|
+
inputSchema: {
|
|
5402
|
+
type: "object",
|
|
5403
|
+
properties: {
|
|
5404
|
+
figmaUrl: { type: "string", description: 'A Figma frame URL, e.g. from "Copy link to selection" \u2014 must include a node-id.' },
|
|
5405
|
+
designSystemId: { type: "string", description: "Optional design system id from list_open_design_design_systems to align the translated code with." }
|
|
5406
|
+
},
|
|
5407
|
+
required: ["figmaUrl"]
|
|
5408
|
+
}
|
|
5409
|
+
},
|
|
5410
|
+
handler: (ctx, args) => pullFigmaFrame(ctx, args)
|
|
5411
|
+
},
|
|
5027
5412
|
{
|
|
5028
5413
|
tool: {
|
|
5029
5414
|
name: "remix_open_design_example",
|
|
5030
|
-
description: "Copies a curated
|
|
5415
|
+
description: "Copies a curated Open Design example artifact (identified by its skillId, which must have a non-empty 'exampleArtifactPath') into the workspace as a starting point, registers it, and returns instructions to MODIFY the copied file rather than regenerate it from scratch.",
|
|
5031
5416
|
inputSchema: {
|
|
5032
5417
|
type: "object",
|
|
5033
5418
|
properties: { skillId: { type: "string", description: "A skillId whose 'exampleArtifactPath' is non-empty." } },
|
|
@@ -5043,11 +5428,11 @@ function buildContext() {
|
|
|
5043
5428
|
const assetsRoot = getAssetsRoot();
|
|
5044
5429
|
const contentIndex = new ContentIndex(assetsRoot, () => `${workspaceRoot}/${outputDir}/design-systems`);
|
|
5045
5430
|
const store = createFileActiveDesignSystemStore(workspaceRoot);
|
|
5046
|
-
return { contentIndex, store, workspaceRoot, outputDir, assetsRoot };
|
|
5431
|
+
return { contentIndex, store, workspaceRoot, outputDir, assetsRoot, figmaToken: getFigmaToken() };
|
|
5047
5432
|
}
|
|
5048
5433
|
async function main() {
|
|
5049
5434
|
const ctx = buildContext();
|
|
5050
|
-
const server = new import_server.Server({ name: "open-design", version: "0.1.0" }, { capabilities: { tools: {} } });
|
|
5435
|
+
const server = new import_server.Server({ name: "open-design", version: "0.1.0" }, { capabilities: { tools: {}, prompts: {} } });
|
|
5051
5436
|
server.setRequestHandler(import_types.ListToolsRequestSchema, async () => ({ tools: TOOL_DEFS.map((d) => d.tool) }));
|
|
5052
5437
|
server.setRequestHandler(import_types.CallToolRequestSchema, async (request) => {
|
|
5053
5438
|
const def = TOOL_DEFS.find((d) => d.tool.name === request.params.name);
|
|
@@ -5062,6 +5447,21 @@ async function main() {
|
|
|
5062
5447
|
return { content: [{ type: "text", text: `Tool "${request.params.name}" failed: ${message}` }], isError: true };
|
|
5063
5448
|
}
|
|
5064
5449
|
});
|
|
5450
|
+
server.setRequestHandler(import_types.ListPromptsRequestSchema, async () => {
|
|
5451
|
+
const prompts = await listRemixablePrompts(ctx);
|
|
5452
|
+
return { prompts: prompts.map((p) => ({ name: p.name, description: `${p.displayName} (Open Design remixable example)` })) };
|
|
5453
|
+
});
|
|
5454
|
+
server.setRequestHandler(import_types.GetPromptRequestSchema, async (request) => {
|
|
5455
|
+
const prompts = await listRemixablePrompts(ctx);
|
|
5456
|
+
const prompt = prompts.find((p) => p.name === request.params.name);
|
|
5457
|
+
if (!prompt) {
|
|
5458
|
+
throw new Error(`Unknown prompt: ${request.params.name}`);
|
|
5459
|
+
}
|
|
5460
|
+
return {
|
|
5461
|
+
description: `${prompt.displayName} (Open Design remixable example)`,
|
|
5462
|
+
messages: [{ role: "user", content: { type: "text", text: buildRemixPromptMessage(prompt) } }]
|
|
5463
|
+
};
|
|
5464
|
+
});
|
|
5065
5465
|
const transport = new import_stdio.StdioServerTransport();
|
|
5066
5466
|
await server.connect(transport);
|
|
5067
5467
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@feimacode/open-design-agent-kit-mcp",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.1.7",
|
|
4
|
+
"description": "Open Design as an MCP server — the same skill/design-system/artifact tools the VS Code extension exposes, usable from any MCP-capable coding agent (Claude Code, Codex, and others).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": ">=18"
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"start": "node ./out/index.js"
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
|
-
"@feimacode/open-design-agent-kit-content": "^0.1.
|
|
45
|
+
"@feimacode/open-design-agent-kit-content": "^0.1.7",
|
|
46
46
|
"@modelcontextprotocol/sdk": "^1.30.0"
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|