@hasna/skills 0.1.14 → 0.1.16
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/bin/index.js +1212 -220
- package/bin/mcp.js +516 -101
- package/dist/index.d.ts +2 -1
- package/dist/index.js +391 -105
- package/dist/lib/config.d.ts +8 -1
- package/dist/lib/installer.d.ts +11 -2
- package/dist/lib/registry.d.ts +13 -0
- package/dist/lib/scheduler.d.ts +47 -0
- package/package.json +2 -1
- package/skills/_common/index.ts +4 -0
- package/skills/_common/vision.ts +374 -0
- package/skills/skill-colorextract/SKILL.md +35 -0
- package/skills/skill-colorextract/bun.lock +102 -0
- package/skills/skill-colorextract/package.json +13 -0
- package/skills/skill-colorextract/src/index.ts +405 -0
- package/skills/skill-siteanalyze/SKILL.md +25 -0
- package/skills/skill-siteanalyze/package.json +13 -0
- package/skills/skill-siteanalyze/src/index.ts +592 -0
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* skill-colorextract — Extract color palettes from screenshots via AI Vision
|
|
4
|
+
* Supports multiple providers: anthropic, openai, xai, gemini
|
|
5
|
+
*/
|
|
6
|
+
import { readFileSync, writeFileSync, existsSync } from "fs";
|
|
7
|
+
import { extname } from "path";
|
|
8
|
+
import {
|
|
9
|
+
analyzeImage,
|
|
10
|
+
detectProvider,
|
|
11
|
+
listAvailableProviders,
|
|
12
|
+
parseJsonResponse,
|
|
13
|
+
type VisionProvider,
|
|
14
|
+
} from "../../_common/vision.js";
|
|
15
|
+
|
|
16
|
+
// ============================================================================
|
|
17
|
+
// Types
|
|
18
|
+
// ============================================================================
|
|
19
|
+
|
|
20
|
+
export interface ExtractedColor {
|
|
21
|
+
hex: string;
|
|
22
|
+
name: string; // human name like "slate blue" or "warm white"
|
|
23
|
+
usage: string; // "background", "primary text", "accent", "border", "button fill", etc.
|
|
24
|
+
frequency: "dominant" | "accent" | "minor";
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface ColorPalette {
|
|
28
|
+
primary: string | null;
|
|
29
|
+
secondary: string | null;
|
|
30
|
+
accent: string | null;
|
|
31
|
+
background: string | null;
|
|
32
|
+
text: string | null;
|
|
33
|
+
neutral: string[];
|
|
34
|
+
all: ExtractedColor[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface ColorExtractResult {
|
|
38
|
+
source: string;
|
|
39
|
+
colors: ExtractedColor[];
|
|
40
|
+
palette: ColorPalette;
|
|
41
|
+
provider: VisionProvider;
|
|
42
|
+
model: string;
|
|
43
|
+
openStylesProfile: {
|
|
44
|
+
name: string;
|
|
45
|
+
displayName: string;
|
|
46
|
+
category: string;
|
|
47
|
+
description: string;
|
|
48
|
+
colors: Record<string, string>;
|
|
49
|
+
principles: string[];
|
|
50
|
+
tags: string[];
|
|
51
|
+
};
|
|
52
|
+
rawAnalysis: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ============================================================================
|
|
56
|
+
// Media type detection
|
|
57
|
+
// ============================================================================
|
|
58
|
+
|
|
59
|
+
function getMediaType(imagePath: string): "image/png" | "image/jpeg" | "image/webp" | "image/gif" {
|
|
60
|
+
const ext = extname(imagePath).toLowerCase();
|
|
61
|
+
switch (ext) {
|
|
62
|
+
case ".jpg":
|
|
63
|
+
case ".jpeg":
|
|
64
|
+
return "image/jpeg";
|
|
65
|
+
case ".webp":
|
|
66
|
+
return "image/webp";
|
|
67
|
+
case ".gif":
|
|
68
|
+
return "image/gif";
|
|
69
|
+
case ".png":
|
|
70
|
+
default:
|
|
71
|
+
return "image/png";
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ============================================================================
|
|
76
|
+
// Core extraction function
|
|
77
|
+
// ============================================================================
|
|
78
|
+
|
|
79
|
+
export async function extractColors(
|
|
80
|
+
imagePath: string,
|
|
81
|
+
options?: { provider?: VisionProvider; model?: string }
|
|
82
|
+
): Promise<ColorExtractResult> {
|
|
83
|
+
const provider = options?.provider ?? detectProvider();
|
|
84
|
+
if (!provider) {
|
|
85
|
+
throw new Error(
|
|
86
|
+
"No AI provider API key found. Set one of: ANTHROPIC_API_KEY, OPENAI_API_KEY, XAI_API_KEY, GEMINI_API_KEY"
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const isUrl = imagePath.startsWith("http://") || imagePath.startsWith("https://");
|
|
91
|
+
|
|
92
|
+
let imageBase64: string;
|
|
93
|
+
let mediaType: "image/png" | "image/jpeg" | "image/webp" | "image/gif";
|
|
94
|
+
|
|
95
|
+
if (isUrl) {
|
|
96
|
+
// Fetch the image from the URL
|
|
97
|
+
const response = await fetch(imagePath);
|
|
98
|
+
if (!response.ok) {
|
|
99
|
+
throw new Error(`Failed to fetch image from URL: ${response.statusText}`);
|
|
100
|
+
}
|
|
101
|
+
const buffer = await response.arrayBuffer();
|
|
102
|
+
imageBase64 = Buffer.from(buffer).toString("base64");
|
|
103
|
+
|
|
104
|
+
// Try to detect media type from URL extension, fall back to content-type header
|
|
105
|
+
const urlMediaType = getMediaType(imagePath);
|
|
106
|
+
const contentType = response.headers.get("content-type");
|
|
107
|
+
if (urlMediaType === "image/png" && contentType && contentType.startsWith("image/")) {
|
|
108
|
+
mediaType = contentType.split(";")[0].trim() as typeof mediaType;
|
|
109
|
+
} else {
|
|
110
|
+
mediaType = urlMediaType;
|
|
111
|
+
}
|
|
112
|
+
} else {
|
|
113
|
+
// Read local file
|
|
114
|
+
if (!existsSync(imagePath)) {
|
|
115
|
+
throw new Error(`Image file not found: ${imagePath}`);
|
|
116
|
+
}
|
|
117
|
+
const fileBuffer = readFileSync(imagePath);
|
|
118
|
+
imageBase64 = fileBuffer.toString("base64");
|
|
119
|
+
mediaType = getMediaType(imagePath);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const prompt = `Analyze this screenshot/image and extract ALL colors used. For each color provide:
|
|
123
|
+
1. Exact hex value (#RRGGBB)
|
|
124
|
+
2. Human-readable name
|
|
125
|
+
3. Usage context (what is it used for in the UI)
|
|
126
|
+
4. Frequency (dominant/accent/minor)
|
|
127
|
+
|
|
128
|
+
Then categorize into a design palette:
|
|
129
|
+
- primary: the main brand/action color
|
|
130
|
+
- secondary: supporting color
|
|
131
|
+
- accent: highlight/CTA color
|
|
132
|
+
- background: main background
|
|
133
|
+
- text: primary text color
|
|
134
|
+
- neutral: array of neutral/gray tones (can be empty array)
|
|
135
|
+
|
|
136
|
+
Finally, suggest what design style this resembles (minimalist/brutalist/corporate/startup/glassmorphism/editorial/retro/material/neubrutalism/neumorphic).
|
|
137
|
+
|
|
138
|
+
Respond ONLY with valid JSON matching this schema:
|
|
139
|
+
{
|
|
140
|
+
"colors": [{ "hex": "#...", "name": "...", "usage": "...", "frequency": "dominant|accent|minor" }],
|
|
141
|
+
"palette": { "primary": "#...", "secondary": "#...", "accent": "#...", "background": "#...", "text": "#...", "neutral": ["#..."] },
|
|
142
|
+
"styleCategory": "minimalist",
|
|
143
|
+
"styleReasoning": "..."
|
|
144
|
+
}`;
|
|
145
|
+
|
|
146
|
+
const result = await analyzeImage(imageBase64, mediaType, prompt, {
|
|
147
|
+
provider,
|
|
148
|
+
model: options?.model,
|
|
149
|
+
systemPrompt: "You are a design systems expert and color analyst. Extract colors precisely.",
|
|
150
|
+
jsonMode: true,
|
|
151
|
+
maxTokens: 2048,
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
const rawAnalysis = result.text;
|
|
155
|
+
|
|
156
|
+
const parsed = parseJsonResponse(rawAnalysis) as {
|
|
157
|
+
colors: ExtractedColor[];
|
|
158
|
+
palette: {
|
|
159
|
+
primary: string | null;
|
|
160
|
+
secondary: string | null;
|
|
161
|
+
accent: string | null;
|
|
162
|
+
background: string | null;
|
|
163
|
+
text: string | null;
|
|
164
|
+
neutral: string[];
|
|
165
|
+
};
|
|
166
|
+
styleCategory: string;
|
|
167
|
+
styleReasoning: string;
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const { colors, palette, styleCategory, styleReasoning } = parsed;
|
|
171
|
+
|
|
172
|
+
// Build full palette with all colors attached
|
|
173
|
+
const fullPalette: ColorPalette = {
|
|
174
|
+
primary: palette.primary ?? null,
|
|
175
|
+
secondary: palette.secondary ?? null,
|
|
176
|
+
accent: palette.accent ?? null,
|
|
177
|
+
background: palette.background ?? null,
|
|
178
|
+
text: palette.text ?? null,
|
|
179
|
+
neutral: Array.isArray(palette.neutral) ? palette.neutral : [],
|
|
180
|
+
all: colors,
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
// Capitalize style category
|
|
184
|
+
const capitalizedCategory =
|
|
185
|
+
styleCategory.charAt(0).toUpperCase() + styleCategory.slice(1);
|
|
186
|
+
|
|
187
|
+
// Build open-styles compatible profile
|
|
188
|
+
const openStylesProfile = {
|
|
189
|
+
name: `extracted-${Date.now()}`,
|
|
190
|
+
displayName: "Extracted Style",
|
|
191
|
+
category: capitalizedCategory,
|
|
192
|
+
description: styleReasoning,
|
|
193
|
+
colors: {
|
|
194
|
+
...(palette.primary ? { primary: palette.primary } : {}),
|
|
195
|
+
...(palette.secondary ? { secondary: palette.secondary } : {}),
|
|
196
|
+
...(palette.accent ? { accent: palette.accent } : {}),
|
|
197
|
+
...(palette.background ? { background: palette.background } : {}),
|
|
198
|
+
...(palette.text ? { text: palette.text } : {}),
|
|
199
|
+
} as Record<string, string>,
|
|
200
|
+
principles: ["Derived from visual analysis"],
|
|
201
|
+
tags: ["extracted", "auto-generated"],
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
source: imagePath,
|
|
206
|
+
colors,
|
|
207
|
+
palette: fullPalette,
|
|
208
|
+
provider: result.provider,
|
|
209
|
+
model: result.model,
|
|
210
|
+
openStylesProfile,
|
|
211
|
+
rawAnalysis,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// ============================================================================
|
|
216
|
+
// CLI
|
|
217
|
+
// ============================================================================
|
|
218
|
+
|
|
219
|
+
type OutputFormat = "colors" | "profile" | "full";
|
|
220
|
+
|
|
221
|
+
interface CliOptions {
|
|
222
|
+
image: string | null;
|
|
223
|
+
format: OutputFormat;
|
|
224
|
+
output: string | null;
|
|
225
|
+
provider: VisionProvider | null;
|
|
226
|
+
model: string | null;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function parseArgs(argv: string[]): { command: string; options: CliOptions } {
|
|
230
|
+
const args = argv.slice(2); // strip node/bun + script path
|
|
231
|
+
|
|
232
|
+
let command = "help";
|
|
233
|
+
const options: CliOptions = {
|
|
234
|
+
image: null,
|
|
235
|
+
format: "full",
|
|
236
|
+
output: null,
|
|
237
|
+
provider: null,
|
|
238
|
+
model: null,
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
if (args.length === 0) {
|
|
242
|
+
return { command: "help", options };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// First positional arg is the command
|
|
246
|
+
const firstArg = args[0];
|
|
247
|
+
if (firstArg === "extract") {
|
|
248
|
+
command = "extract";
|
|
249
|
+
} else if (firstArg === "help" || firstArg === "--help" || firstArg === "-h") {
|
|
250
|
+
command = "help";
|
|
251
|
+
} else {
|
|
252
|
+
command = "help";
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Parse flags
|
|
256
|
+
for (let i = 1; i < args.length; i++) {
|
|
257
|
+
const arg = args[i];
|
|
258
|
+
if ((arg === "--image" || arg === "-i") && args[i + 1]) {
|
|
259
|
+
options.image = args[++i];
|
|
260
|
+
} else if ((arg === "--format" || arg === "-f") && args[i + 1]) {
|
|
261
|
+
const fmt = args[++i];
|
|
262
|
+
if (fmt === "colors" || fmt === "profile" || fmt === "full") {
|
|
263
|
+
options.format = fmt;
|
|
264
|
+
} else {
|
|
265
|
+
console.error(`Invalid format: ${fmt}. Use: colors, profile, full`);
|
|
266
|
+
process.exit(1);
|
|
267
|
+
}
|
|
268
|
+
} else if ((arg === "--output" || arg === "-o") && args[i + 1]) {
|
|
269
|
+
options.output = args[++i];
|
|
270
|
+
} else if (arg === "--provider" && args[i + 1]) {
|
|
271
|
+
const p = args[++i] as VisionProvider;
|
|
272
|
+
if (["anthropic", "openai", "xai", "gemini"].includes(p)) {
|
|
273
|
+
options.provider = p;
|
|
274
|
+
} else {
|
|
275
|
+
console.error(`Invalid provider: ${p}. Use: anthropic, openai, xai, gemini`);
|
|
276
|
+
process.exit(1);
|
|
277
|
+
}
|
|
278
|
+
} else if (arg === "--model" && args[i + 1]) {
|
|
279
|
+
options.model = args[++i];
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return { command, options };
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function printHelp(): void {
|
|
287
|
+
const available = listAvailableProviders();
|
|
288
|
+
console.log(`
|
|
289
|
+
skill-colorextract — Extract color palettes from screenshots and images via AI Vision
|
|
290
|
+
|
|
291
|
+
USAGE
|
|
292
|
+
skill-colorextract extract --image <path-or-url> [options]
|
|
293
|
+
skill-colorextract help
|
|
294
|
+
|
|
295
|
+
COMMANDS
|
|
296
|
+
extract Analyze an image and extract its color palette
|
|
297
|
+
help Show this help message
|
|
298
|
+
|
|
299
|
+
OPTIONS
|
|
300
|
+
--image, -i <path|url> Path to local image or HTTP/HTTPS URL (required)
|
|
301
|
+
--format, -f <format> Output format: colors | profile | full (default: full)
|
|
302
|
+
--output, -o <file> Write JSON result to file instead of stdout
|
|
303
|
+
--provider <name> AI provider: anthropic | openai | xai | gemini (auto-detected)
|
|
304
|
+
--model <name> Model override (uses provider default if not set)
|
|
305
|
+
|
|
306
|
+
FORMATS
|
|
307
|
+
colors Print only the extracted colors array
|
|
308
|
+
profile Print only the open-styles compatible profile object
|
|
309
|
+
full Print the complete extraction result (default)
|
|
310
|
+
|
|
311
|
+
EXAMPLES
|
|
312
|
+
skill-colorextract extract --image ./screenshot.png
|
|
313
|
+
skill-colorextract extract --image https://example.com/screenshot.png
|
|
314
|
+
skill-colorextract extract --image ./screenshot.png --format profile
|
|
315
|
+
skill-colorextract extract --image ./screenshot.png --output ./colors.json
|
|
316
|
+
skill-colorextract extract --image ./screenshot.png --provider openai
|
|
317
|
+
skill-colorextract extract --image ./screenshot.png --provider gemini --model gemini-2.0-flash
|
|
318
|
+
|
|
319
|
+
ENVIRONMENT
|
|
320
|
+
ANTHROPIC_API_KEY Claude API key (anthropic provider)
|
|
321
|
+
OPENAI_API_KEY OpenAI API key (openai provider)
|
|
322
|
+
XAI_API_KEY xAI API key (xai provider)
|
|
323
|
+
GEMINI_API_KEY Google Gemini API key (gemini provider)
|
|
324
|
+
|
|
325
|
+
AVAILABLE PROVIDERS
|
|
326
|
+
${available.length > 0 ? available.join(", ") : "(none — set an API key)"}
|
|
327
|
+
`);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
async function main(): Promise<void> {
|
|
331
|
+
const { command, options } = parseArgs(process.argv);
|
|
332
|
+
|
|
333
|
+
if (command === "help") {
|
|
334
|
+
printHelp();
|
|
335
|
+
process.exit(0);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (command === "extract") {
|
|
339
|
+
// Check for a provider (auto-detect or explicit)
|
|
340
|
+
const provider = options.provider ?? detectProvider();
|
|
341
|
+
if (!provider) {
|
|
342
|
+
console.error("Error: No AI provider API key found.");
|
|
343
|
+
console.error("Set one of: ANTHROPIC_API_KEY, OPENAI_API_KEY, XAI_API_KEY, GEMINI_API_KEY");
|
|
344
|
+
process.exit(1);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Validate image argument
|
|
348
|
+
if (!options.image) {
|
|
349
|
+
console.error("Error: --image <path-or-url> is required.");
|
|
350
|
+
console.error("Run `skill-colorextract help` for usage.");
|
|
351
|
+
process.exit(1);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Validate local file exists (URLs are validated during fetch)
|
|
355
|
+
const isUrl =
|
|
356
|
+
options.image.startsWith("http://") ||
|
|
357
|
+
options.image.startsWith("https://");
|
|
358
|
+
if (!isUrl && !existsSync(options.image)) {
|
|
359
|
+
console.error(`Error: Image file not found: ${options.image}`);
|
|
360
|
+
process.exit(1);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
try {
|
|
364
|
+
const result = await extractColors(options.image, {
|
|
365
|
+
provider: options.provider ?? undefined,
|
|
366
|
+
model: options.model ?? undefined,
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
// Determine output value based on format
|
|
370
|
+
let output: unknown;
|
|
371
|
+
if (options.format === "colors") {
|
|
372
|
+
output = result.colors;
|
|
373
|
+
} else if (options.format === "profile") {
|
|
374
|
+
output = result.openStylesProfile;
|
|
375
|
+
} else {
|
|
376
|
+
// full — omit rawAnalysis for cleaner output
|
|
377
|
+
output = {
|
|
378
|
+
source: result.source,
|
|
379
|
+
provider: result.provider,
|
|
380
|
+
model: result.model,
|
|
381
|
+
colors: result.colors,
|
|
382
|
+
palette: result.palette,
|
|
383
|
+
openStylesProfile: result.openStylesProfile,
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const json = JSON.stringify(output, null, 2);
|
|
388
|
+
|
|
389
|
+
if (options.output) {
|
|
390
|
+
writeFileSync(options.output, json, "utf-8");
|
|
391
|
+
console.log(`Result written to: ${options.output}`);
|
|
392
|
+
} else {
|
|
393
|
+
console.log(json);
|
|
394
|
+
}
|
|
395
|
+
} catch (error) {
|
|
396
|
+
console.error(
|
|
397
|
+
"Error:",
|
|
398
|
+
error instanceof Error ? error.message : String(error)
|
|
399
|
+
);
|
|
400
|
+
process.exit(1);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
main();
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# skill-siteanalyze
|
|
2
|
+
|
|
3
|
+
Analyze any website's design system — detects shadcn/ui, Tailwind, extracts colors, typography, and components via Playwright + Claude Vision.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
skill-siteanalyze <url>
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## What It Does
|
|
12
|
+
|
|
13
|
+
- Navigates to the target URL using Playwright
|
|
14
|
+
- Takes a screenshot and analyzes it with Claude Vision
|
|
15
|
+
- Detects UI framework (shadcn/ui, Tailwind CSS, etc.)
|
|
16
|
+
- Extracts color palette, typography scale, and component patterns
|
|
17
|
+
- Outputs an open-styles compatible design profile
|
|
18
|
+
|
|
19
|
+
## Output
|
|
20
|
+
|
|
21
|
+
Returns a JSON design profile compatible with open-styles format, including:
|
|
22
|
+
- `colors` — primary, secondary, accent, background, text colors
|
|
23
|
+
- `typography` — font families, sizes, weights
|
|
24
|
+
- `framework` — detected UI framework
|
|
25
|
+
- `components` — identified component patterns
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "skill-siteanalyze",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Analyze any website's design system — detects shadcn/ui, Tailwind, extracts colors, typography, and components",
|
|
5
|
+
"main": "src/index.ts",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": { "skill-siteanalyze": "src/index.ts" },
|
|
8
|
+
"scripts": { "start": "bun run src/index.ts" },
|
|
9
|
+
"keywords": ["design", "shadcn", "tailwind", "colors", "typography", "playwright", "analysis", "open-styles"],
|
|
10
|
+
"license": "Apache-2.0",
|
|
11
|
+
"dependencies": {},
|
|
12
|
+
"devDependencies": { "@types/bun": "latest" }
|
|
13
|
+
}
|