extraktor 1.0.0
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/LICENSE +21 -0
- package/README.md +174 -0
- package/dist/chunk-5IH5TLAQ.js +91 -0
- package/dist/chunk-PHMSK7VD.js +6411 -0
- package/dist/chunk-VLLFGYUN.js +2773 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +3261 -0
- package/dist/design-md-generator-YMQOE2IW.js +502 -0
- package/dist/index.d.ts +2774 -0
- package/dist/index.js +58 -0
- package/dist/server-DR7RCM5S.js +328 -0
- package/dist/style-applier-BMHP6V57.js +1032 -0
- package/dist/theme-package-generator-E55BBBZN.js +412 -0
- package/package.json +99 -0
- package/skills/analyze-design.md +20 -0
- package/skills/apply-style.md +24 -0
- package/skills/clone-site.md +29 -0
- package/skills/design-md.md +29 -0
- package/skills/devtools-extract.md +30 -0
- package/skills/extract-tokens.md +21 -0
- package/skills/genome.md +31 -0
- package/skills/regen.md +32 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AnimationExtractor,
|
|
3
|
+
BaseExtractor,
|
|
4
|
+
CSSVarsTransformer,
|
|
5
|
+
ColorExtractor,
|
|
6
|
+
EffectsExtractor,
|
|
7
|
+
ExtractorRegistry,
|
|
8
|
+
Orchestrator,
|
|
9
|
+
PageAnalyzer,
|
|
10
|
+
PlaywrightDriver,
|
|
11
|
+
SpacingExtractor,
|
|
12
|
+
TailwindTransformer,
|
|
13
|
+
TokenTransformer,
|
|
14
|
+
TypographyExtractor,
|
|
15
|
+
areColorsSimilar,
|
|
16
|
+
createDefaultConfig,
|
|
17
|
+
extractGradientColors,
|
|
18
|
+
generateColorName,
|
|
19
|
+
generateColorScale,
|
|
20
|
+
getContrastRatio,
|
|
21
|
+
getDefaultConfig,
|
|
22
|
+
groupSimilarColors,
|
|
23
|
+
loadConfig,
|
|
24
|
+
mergeConfig,
|
|
25
|
+
parseColor,
|
|
26
|
+
toColorFormat
|
|
27
|
+
} from "./chunk-PHMSK7VD.js";
|
|
28
|
+
import {
|
|
29
|
+
createLogger
|
|
30
|
+
} from "./chunk-5IH5TLAQ.js";
|
|
31
|
+
export {
|
|
32
|
+
AnimationExtractor,
|
|
33
|
+
BaseExtractor,
|
|
34
|
+
CSSVarsTransformer,
|
|
35
|
+
ColorExtractor,
|
|
36
|
+
EffectsExtractor,
|
|
37
|
+
ExtractorRegistry,
|
|
38
|
+
Orchestrator,
|
|
39
|
+
PageAnalyzer,
|
|
40
|
+
PlaywrightDriver,
|
|
41
|
+
SpacingExtractor,
|
|
42
|
+
TailwindTransformer,
|
|
43
|
+
TokenTransformer,
|
|
44
|
+
TypographyExtractor,
|
|
45
|
+
areColorsSimilar,
|
|
46
|
+
createDefaultConfig,
|
|
47
|
+
createLogger,
|
|
48
|
+
extractGradientColors,
|
|
49
|
+
generateColorName,
|
|
50
|
+
generateColorScale,
|
|
51
|
+
getContrastRatio,
|
|
52
|
+
getDefaultConfig,
|
|
53
|
+
groupSimilarColors,
|
|
54
|
+
loadConfig,
|
|
55
|
+
mergeConfig,
|
|
56
|
+
parseColor,
|
|
57
|
+
toColorFormat
|
|
58
|
+
};
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
GenomeEngine,
|
|
4
|
+
SiteCloner
|
|
5
|
+
} from "./chunk-VLLFGYUN.js";
|
|
6
|
+
import {
|
|
7
|
+
Orchestrator,
|
|
8
|
+
PlaywrightDriver,
|
|
9
|
+
getDefaultConfig
|
|
10
|
+
} from "./chunk-PHMSK7VD.js";
|
|
11
|
+
import {
|
|
12
|
+
createLogger
|
|
13
|
+
} from "./chunk-5IH5TLAQ.js";
|
|
14
|
+
|
|
15
|
+
// src/mcp/server.ts
|
|
16
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
17
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
18
|
+
import { z } from "zod";
|
|
19
|
+
import path from "path";
|
|
20
|
+
import fs from "fs-extra";
|
|
21
|
+
var logger = createLogger({ level: "error" });
|
|
22
|
+
async function startMcpServer() {
|
|
23
|
+
const server = new McpServer({
|
|
24
|
+
name: "extraktor",
|
|
25
|
+
version: "1.0.0"
|
|
26
|
+
});
|
|
27
|
+
server.tool(
|
|
28
|
+
"extract_design_tokens",
|
|
29
|
+
"Extract design tokens (colors, typography, spacing, animations, effects) from any URL. Returns Tailwind config, CSS variables, and DTCG tokens. No API key required.",
|
|
30
|
+
{
|
|
31
|
+
url: z.string().describe("URL to extract from"),
|
|
32
|
+
format: z.enum(["tailwind", "css-variables", "design-tokens", "all"]).default("all").describe("Output format")
|
|
33
|
+
},
|
|
34
|
+
async ({ url, format }) => {
|
|
35
|
+
const outputDir = path.join("/tmp", `extraktor-mcp-${Date.now()}`);
|
|
36
|
+
await fs.ensureDir(outputDir);
|
|
37
|
+
try {
|
|
38
|
+
const config = getDefaultConfig();
|
|
39
|
+
config.browser.headless = true;
|
|
40
|
+
config.browser.timeout = 3e4;
|
|
41
|
+
config.output.formats = format === "all" ? ["tailwind", "css-variables", "design-tokens"] : [format === "css-variables" ? "css-variables" : format === "design-tokens" ? "design-tokens" : "tailwind"];
|
|
42
|
+
const orchestrator = new Orchestrator({ config, logger });
|
|
43
|
+
const result = await orchestrator.extract(url, outputDir);
|
|
44
|
+
const output = {};
|
|
45
|
+
const tailwindPath = path.join(outputDir, "tailwind.config.ts");
|
|
46
|
+
if (await fs.pathExists(tailwindPath)) {
|
|
47
|
+
output.tailwindConfig = await fs.readFile(tailwindPath, "utf-8");
|
|
48
|
+
}
|
|
49
|
+
const cssPath = path.join(outputDir, "variables.css");
|
|
50
|
+
if (await fs.pathExists(cssPath)) {
|
|
51
|
+
output.cssVariables = await fs.readFile(cssPath, "utf-8");
|
|
52
|
+
}
|
|
53
|
+
const tokensPath = path.join(outputDir, "tokens.json");
|
|
54
|
+
if (await fs.pathExists(tokensPath)) {
|
|
55
|
+
output.designTokens = await fs.readFile(tokensPath, "utf-8");
|
|
56
|
+
}
|
|
57
|
+
const summary = {
|
|
58
|
+
url: result.metadata.url,
|
|
59
|
+
title: result.metadata.title,
|
|
60
|
+
colors: result.colors.palette.length,
|
|
61
|
+
fonts: result.typography.fontFamilies.length,
|
|
62
|
+
fontSizes: result.typography.fontSizes.length,
|
|
63
|
+
spacingValues: result.spacing.scale.length,
|
|
64
|
+
shadows: result.effects.shadows.length,
|
|
65
|
+
animations: result.animations.keyframes.length,
|
|
66
|
+
...output
|
|
67
|
+
};
|
|
68
|
+
return { content: [{ type: "text", text: JSON.stringify(summary, null, 2) }] };
|
|
69
|
+
} finally {
|
|
70
|
+
await fs.remove(outputDir);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
);
|
|
74
|
+
server.tool(
|
|
75
|
+
"analyze_site",
|
|
76
|
+
"Quick analysis of a website - colors, fonts, spacing, layout patterns, SPA framework detection. No files generated, no API key needed.",
|
|
77
|
+
{
|
|
78
|
+
url: z.string().describe("URL to analyze")
|
|
79
|
+
},
|
|
80
|
+
async ({ url }) => {
|
|
81
|
+
const config = getDefaultConfig();
|
|
82
|
+
config.browser.headless = true;
|
|
83
|
+
config.browser.timeout = 3e4;
|
|
84
|
+
const orchestrator = new Orchestrator({ config, logger });
|
|
85
|
+
const result = await orchestrator.analyze(url);
|
|
86
|
+
const analysis = {
|
|
87
|
+
url: result.metadata.url,
|
|
88
|
+
title: result.metadata.title,
|
|
89
|
+
spaFramework: result.metadata.spaFramework,
|
|
90
|
+
elementsAnalyzed: result.metadata.elementsAnalyzed,
|
|
91
|
+
colors: {
|
|
92
|
+
count: result.colors.palette.length,
|
|
93
|
+
top5: result.colors.palette.slice(0, 5).map((c) => ({ name: c.name, value: c.value })),
|
|
94
|
+
gradients: result.colors.gradients.length
|
|
95
|
+
},
|
|
96
|
+
typography: {
|
|
97
|
+
fonts: result.typography.fontFamilies.map((f) => f.name),
|
|
98
|
+
sizeCount: result.typography.fontSizes.length,
|
|
99
|
+
sizes: result.typography.fontSizes.slice(0, 8).map((s) => s.value)
|
|
100
|
+
},
|
|
101
|
+
spacing: {
|
|
102
|
+
scaleCount: result.spacing.scale.length,
|
|
103
|
+
values: result.spacing.scale.slice(0, 10).map((s) => s.value)
|
|
104
|
+
},
|
|
105
|
+
effects: {
|
|
106
|
+
shadows: result.effects.shadows.length,
|
|
107
|
+
borderRadii: result.effects.borderRadii.length
|
|
108
|
+
},
|
|
109
|
+
animations: {
|
|
110
|
+
keyframes: result.animations.keyframes.length,
|
|
111
|
+
transitions: result.animations.transitions.length
|
|
112
|
+
},
|
|
113
|
+
layout: {
|
|
114
|
+
containers: result.layout.containers.length,
|
|
115
|
+
gridPatterns: result.layout.gridPatterns.length,
|
|
116
|
+
flexPatterns: result.layout.flexPatterns.length,
|
|
117
|
+
breakpoints: result.layout.breakpoints.length
|
|
118
|
+
},
|
|
119
|
+
cssVariables: Object.keys(result.cssVariables).length
|
|
120
|
+
};
|
|
121
|
+
return { content: [{ type: "text", text: JSON.stringify(analysis, null, 2) }] };
|
|
122
|
+
}
|
|
123
|
+
);
|
|
124
|
+
server.tool(
|
|
125
|
+
"clone_site",
|
|
126
|
+
"Clone a website into a working Next.js project with all assets (images, fonts, videos). Downloads and localizes everything. No API key needed.",
|
|
127
|
+
{
|
|
128
|
+
url: z.string().describe("URL to clone"),
|
|
129
|
+
outputDir: z.string().describe("Directory to save the project"),
|
|
130
|
+
projectName: z.string().optional().describe("Project name (defaults to domain)")
|
|
131
|
+
},
|
|
132
|
+
async ({ url, outputDir, projectName }) => {
|
|
133
|
+
const resolvedOutput = path.resolve(outputDir);
|
|
134
|
+
await fs.ensureDir(resolvedOutput);
|
|
135
|
+
const config = getDefaultConfig();
|
|
136
|
+
config.browser.timeout = 6e4;
|
|
137
|
+
const driver = new PlaywrightDriver({
|
|
138
|
+
browserConfig: config.browser,
|
|
139
|
+
spaConfig: config.spa,
|
|
140
|
+
logger
|
|
141
|
+
});
|
|
142
|
+
await driver.initialize();
|
|
143
|
+
const navResult = await driver.navigate(url);
|
|
144
|
+
if (!navResult.success) {
|
|
145
|
+
await driver.close();
|
|
146
|
+
return { content: [{ type: "text", text: `Navigation failed: ${navResult.error}` }] };
|
|
147
|
+
}
|
|
148
|
+
const page = driver.getPage();
|
|
149
|
+
if (!page) {
|
|
150
|
+
await driver.close();
|
|
151
|
+
return { content: [{ type: "text", text: "Failed to get page" }] };
|
|
152
|
+
}
|
|
153
|
+
const name = projectName || new URL(url).hostname.replace(/\./g, "-");
|
|
154
|
+
const cloner = new SiteCloner();
|
|
155
|
+
const result = await cloner.clone(page, {
|
|
156
|
+
url,
|
|
157
|
+
outputDir: resolvedOutput,
|
|
158
|
+
projectName: name,
|
|
159
|
+
downloadAssets: true,
|
|
160
|
+
inlineStyles: true
|
|
161
|
+
});
|
|
162
|
+
await driver.close();
|
|
163
|
+
return {
|
|
164
|
+
content: [{
|
|
165
|
+
type: "text",
|
|
166
|
+
text: JSON.stringify({
|
|
167
|
+
projectDir: result.projectDir,
|
|
168
|
+
platform: result.platform,
|
|
169
|
+
assets: result.assets,
|
|
170
|
+
pagesCloned: result.pagesCloned,
|
|
171
|
+
nextSteps: `cd ${result.projectDir} && npm install && npm run dev`
|
|
172
|
+
}, null, 2)
|
|
173
|
+
}]
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
);
|
|
177
|
+
server.tool(
|
|
178
|
+
"extract_genome",
|
|
179
|
+
"Vision-AI design system extraction. Identifies components visually using Claude Vision, generates clean React/Tailwind components, Storybook, and a portable genome.json. Uses your ANTHROPIC_API_KEY.",
|
|
180
|
+
{
|
|
181
|
+
url: z.string().describe("URL to extract genome from"),
|
|
182
|
+
outputDir: z.string().describe("Directory to save the genome"),
|
|
183
|
+
maxComponents: z.number().default(20).describe("Max components to synthesize"),
|
|
184
|
+
skipStorybook: z.boolean().default(false).describe("Skip Storybook generation")
|
|
185
|
+
},
|
|
186
|
+
async ({ url, outputDir, maxComponents, skipStorybook }) => {
|
|
187
|
+
const apiKey = process.env.ANTHROPIC_API_KEY;
|
|
188
|
+
if (!apiKey) {
|
|
189
|
+
return {
|
|
190
|
+
content: [{
|
|
191
|
+
type: "text",
|
|
192
|
+
text: "ANTHROPIC_API_KEY not found in environment. genome requires Claude Vision API access. Set the environment variable and restart."
|
|
193
|
+
}]
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
const resolvedOutput = path.resolve(outputDir);
|
|
197
|
+
const engine = new GenomeEngine({ logger });
|
|
198
|
+
const genome = await engine.genome({
|
|
199
|
+
url,
|
|
200
|
+
outputDir: resolvedOutput,
|
|
201
|
+
apiKey,
|
|
202
|
+
maxComponents,
|
|
203
|
+
skipStorybook,
|
|
204
|
+
headless: true
|
|
205
|
+
});
|
|
206
|
+
return {
|
|
207
|
+
content: [{
|
|
208
|
+
type: "text",
|
|
209
|
+
text: JSON.stringify({
|
|
210
|
+
sourceUrl: genome.meta.sourceUrl,
|
|
211
|
+
componentCount: genome.meta.componentCount,
|
|
212
|
+
components: genome.components.map((c) => ({ name: c.name, type: c.type, description: c.description })),
|
|
213
|
+
sectionOrder: genome.layout.sectionOrder,
|
|
214
|
+
outputDir: resolvedOutput,
|
|
215
|
+
colorCount: Object.keys(genome.theme.colors).length,
|
|
216
|
+
styleNarrative: genome.styleNarrative.slice(0, 300) + "..."
|
|
217
|
+
}, null, 2)
|
|
218
|
+
}]
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
);
|
|
222
|
+
server.tool(
|
|
223
|
+
"regen_page",
|
|
224
|
+
"Generate a new page from a previously extracted genome. Creates a Next.js page using the genome's components and design language with original content. Uses your ANTHROPIC_API_KEY.",
|
|
225
|
+
{
|
|
226
|
+
genomeDir: z.string().describe("Path to genome directory (containing genome.json)"),
|
|
227
|
+
prompt: z.string().describe("Description of the page to generate"),
|
|
228
|
+
outputDir: z.string().describe("Directory to save the generated page")
|
|
229
|
+
},
|
|
230
|
+
async ({ genomeDir, prompt, outputDir }) => {
|
|
231
|
+
const apiKey = process.env.ANTHROPIC_API_KEY;
|
|
232
|
+
if (!apiKey) {
|
|
233
|
+
return {
|
|
234
|
+
content: [{
|
|
235
|
+
type: "text",
|
|
236
|
+
text: "ANTHROPIC_API_KEY not found in environment. regen requires Claude API access."
|
|
237
|
+
}]
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
const engine = new GenomeEngine({ logger });
|
|
241
|
+
const result = await engine.regen({
|
|
242
|
+
genomeDir: path.resolve(genomeDir),
|
|
243
|
+
prompt,
|
|
244
|
+
outputDir: path.resolve(outputDir),
|
|
245
|
+
apiKey
|
|
246
|
+
});
|
|
247
|
+
return {
|
|
248
|
+
content: [{
|
|
249
|
+
type: "text",
|
|
250
|
+
text: JSON.stringify({
|
|
251
|
+
outputDir: result,
|
|
252
|
+
prompt,
|
|
253
|
+
nextSteps: `cd ${result} && npm install && npm run dev`
|
|
254
|
+
}, null, 2)
|
|
255
|
+
}]
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
);
|
|
259
|
+
server.tool(
|
|
260
|
+
"extract_colors",
|
|
261
|
+
"Extract just the color palette from a URL. Returns colors with names, hex values, and usage categories. No API key needed.",
|
|
262
|
+
{
|
|
263
|
+
url: z.string().describe("URL to extract colors from")
|
|
264
|
+
},
|
|
265
|
+
async ({ url }) => {
|
|
266
|
+
const config = getDefaultConfig();
|
|
267
|
+
config.browser.headless = true;
|
|
268
|
+
config.browser.timeout = 2e4;
|
|
269
|
+
config.output.formats = [];
|
|
270
|
+
const orchestrator = new Orchestrator({ config, logger });
|
|
271
|
+
const result = await orchestrator.analyze(url);
|
|
272
|
+
const colors = {
|
|
273
|
+
palette: result.colors.palette.map((c) => ({
|
|
274
|
+
name: c.name,
|
|
275
|
+
value: c.value,
|
|
276
|
+
usage: c.usage,
|
|
277
|
+
frequency: c.frequency
|
|
278
|
+
})),
|
|
279
|
+
gradients: result.colors.gradients.map((g) => ({
|
|
280
|
+
value: g.value,
|
|
281
|
+
type: g.type
|
|
282
|
+
}))
|
|
283
|
+
};
|
|
284
|
+
return { content: [{ type: "text", text: JSON.stringify(colors, null, 2) }] };
|
|
285
|
+
}
|
|
286
|
+
);
|
|
287
|
+
server.tool(
|
|
288
|
+
"extract_typography",
|
|
289
|
+
"Extract typography system from a URL. Returns font families, sizes, weights, line heights. No API key needed.",
|
|
290
|
+
{
|
|
291
|
+
url: z.string().describe("URL to extract typography from")
|
|
292
|
+
},
|
|
293
|
+
async ({ url }) => {
|
|
294
|
+
const config = getDefaultConfig();
|
|
295
|
+
config.browser.headless = true;
|
|
296
|
+
config.browser.timeout = 2e4;
|
|
297
|
+
config.output.formats = [];
|
|
298
|
+
const orchestrator = new Orchestrator({ config, logger });
|
|
299
|
+
const result = await orchestrator.analyze(url);
|
|
300
|
+
const typography = {
|
|
301
|
+
families: result.typography.fontFamilies.map((f) => ({
|
|
302
|
+
name: f.name,
|
|
303
|
+
category: f.category,
|
|
304
|
+
source: f.source
|
|
305
|
+
})),
|
|
306
|
+
sizes: result.typography.fontSizes.map((s) => ({
|
|
307
|
+
name: s.name,
|
|
308
|
+
value: s.value,
|
|
309
|
+
pxValue: s.pxValue
|
|
310
|
+
})),
|
|
311
|
+
weights: result.typography.fontWeights.map((w) => ({
|
|
312
|
+
name: w.name,
|
|
313
|
+
value: w.value
|
|
314
|
+
})),
|
|
315
|
+
lineHeights: result.typography.lineHeights.map((l) => ({
|
|
316
|
+
name: l.name,
|
|
317
|
+
value: l.value
|
|
318
|
+
}))
|
|
319
|
+
};
|
|
320
|
+
return { content: [{ type: "text", text: JSON.stringify(typography, null, 2) }] };
|
|
321
|
+
}
|
|
322
|
+
);
|
|
323
|
+
const transport = new StdioServerTransport();
|
|
324
|
+
await server.connect(transport);
|
|
325
|
+
}
|
|
326
|
+
export {
|
|
327
|
+
startMcpServer
|
|
328
|
+
};
|