@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.
@@ -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
+ }