This commit is contained in:
2026-04-12 01:06:31 +07:00
commit 10d660cbcb
1066 changed files with 228596 additions and 0 deletions

View File

@@ -0,0 +1,171 @@
---
name: ck:stitch
description: "AI design generation with Google Stitch. Generate UI designs from text prompts, export Tailwind/HTML/DESIGN.md, orchestrate design-to-code pipeline. Use for rapid prototyping, UI generation, design exploration."
license: MIT
allowed-tools:
- Bash
- Read
- Write
- Edit
argument-hint: "[design prompt or action]"
metadata:
author: claudekit
version: "1.0.0"
---
# Google Stitch — AI Design Generation
Generate high-fidelity UI designs from text prompts via Google Stitch. Export Tailwind/HTML, orchestrate design-to-code pipelines with existing UI skills.
**Free tier:** 400 credits/day + 15 redesign credits/day. Resets at midnight UTC.
## Setup
### 1. API Key
Get an API key at https://stitch.withgoogle.com → Settings → API Keys.
Add `STITCH_API_KEY=sk_...` to `~/.opencode/.env` (or `~/.opencode/skills/.env`).
Running `install.sh` auto-adds the placeholder if missing — just fill in the value.
### 2. Install SDK
```bash
cd ~/.opencode/skills/stitch/scripts && npm install
```
Or run `~/.opencode/skills/install.sh` which handles this automatically.
### 3. Optional
```bash
# In ~/.opencode/.env
STITCH_PROJECT_ID="my-project" # Default project (auto-creates "claudekit-default" if unset)
STITCH_QUOTA_LIMIT="200" # Override daily limit
```
### 4. MCP Server (optional)
Add to `~/.opencode/.mcp.json` for native design context in Claude Code:
```json
{
"mcpServers": {
"stitch": {
"command": "npx",
"args": ["@_davideast/stitch-mcp", "proxy"],
"env": { "STITCH_API_KEY": "${STITCH_API_KEY}" }
}
}
}
```
See `references/stitch-mcp-setup.md` for alternative options (gcloud, auto-installer).
## Quick Start
```bash
# Check quota
npx tsx scripts/stitch-quota.ts check
# Generate design
npx tsx scripts/stitch-generate.ts "A checkout page with payment form and cart summary"
# Export as HTML + DESIGN.md
npx tsx scripts/stitch-export.ts <screen-id> --format all --output ./stitch-exports/
```
## Actions
### generate
Generate UI design from text prompt.
```bash
npx tsx scripts/stitch-generate.ts "<prompt>" [--project <id>] [--device MOBILE|DESKTOP|TABLET] [--variants <count>]
```
Returns: screen ID, preview image URL. With `--variants`: additional design alternatives.
### export
Export generated design as HTML/Tailwind, screenshot, or DESIGN.md.
```bash
npx tsx scripts/stitch-export.ts <screen-id> [--format html|image|all] [--output <dir>]
```
Outputs:
- `design.html` — Semantic HTML with Tailwind CSS classes
- `design.png` — Screenshot of the design
- `DESIGN.md` — Agent-readable design spec (colors, typography, spacing, components)
### quota
Check and manage daily quota.
```bash
npx tsx scripts/stitch-quota.ts check # Show remaining credits
npx tsx scripts/stitch-quota.ts increment # Bump after generation
npx tsx scripts/stitch-quota.ts reset # Force reset (auto-resets daily)
```
### edit
Refine an existing design.
```typescript
const editedScreen = await screen.edit("Make the header darker and add a search bar");
```
## Orchestration Pipeline
### Design-to-Code Flow
1. **Check quota** — Run `stitch-quota.ts check`. If exhausted, suggest `ck:ui-ux-pro-max` fallback.
2. **Generate** — Run `stitch-generate.ts` with user's design prompt
3. **Review** — Show generated design image to user for feedback
4. **Variants** (optional) — Generate alternatives if user wants exploration
5. **Export** — Run `stitch-export.ts --format all` to get HTML + DESIGN.md
6. **Implement** — Hand off exported artifacts to implementation skill:
- `ck:frontend-design` — React/Vue/Svelte components from Tailwind export
- `ck:ui-ux-pro-max` — Full page layouts with style guide integration
- `ck:ui-styling` — Design token extraction from DESIGN.md
7. **Track quota** — Run `stitch-quota.ts increment`
### Handoff Protocol
- Export creates `DESIGN.md` in project root or plan directory
- Implementation skills detect `DESIGN.md` and use it as design spec
- DESIGN.md takes precedence over text descriptions when present
- If no DESIGN.md exists, skills fall back to normal text-based design flow
See `references/design-to-code-pipeline.md` for detailed patterns and examples.
## Quota Management
- 400 credits/day + 15 redesign/day, resets at midnight UTC
- Local tracking via `~/.claudekit/.stitch-quota.json`
- Warns when remaining credits < 20%
- **Fallback:** When exhausted, use `ck:ui-ux-pro-max` for text-based design generation
See `references/quota-management.md` for strategies.
## Limitations
- **No React export** HTML/Tailwind only; Claude converts to React/Vue components
- **Non-responsive layouts** Must add breakpoints manually during implementation
- **No animations** Static designs only; add micro-interactions in code
- **Single-user** No multiplayer/collaboration features
- **Hard daily quota** No paid tier to increase limits
- **Generic output risk** Combine with style guides for differentiation
## References
| Topic | File |
|-------|------|
| SDK API | `references/stitch-sdk-api.md` |
| MCP Setup | `references/stitch-mcp-setup.md` |
| Pipeline Patterns | `references/design-to-code-pipeline.md` |
| Quota Strategy | `references/quota-management.md` |

View File

@@ -0,0 +1,11 @@
{
"mcpServers": {
"stitch": {
"command": "npx",
"args": ["@_davideast/stitch-mcp", "proxy"],
"env": {
"STITCH_API_KEY": "${STITCH_API_KEY}"
}
}
}
}

View File

@@ -0,0 +1,128 @@
# Design-to-Code Pipeline Patterns
Orchestration workflows chaining Stitch design generation with ClaudeKit implementation skills.
## Basic Pipeline
```
Prompt -> Stitch Generate -> Export HTML -> Claude Implements Components
```
### Steps
1. User describes desired UI in natural language
2. Run `stitch-generate.ts` with the prompt
3. Show preview image to user for approval
4. Run `stitch-export.ts --format all` to get HTML + DESIGN.md
5. Hand DESIGN.md to implementation skill
6. Implementation skill reads DESIGN.md and codes components
### Example
```bash
# 1. Generate
npx tsx stitch-generate.ts "E-commerce product page with image gallery, price, reviews, add-to-cart button"
# 2. Export
npx tsx stitch-export.ts <screen-id> --format all --output ./stitch-exports/
# 3. Implementation skill reads ./stitch-exports/DESIGN.md
# 4. Activate ck:frontend-design to implement React components from the spec
```
## Advanced Pipeline (with Variants)
```
Prompt -> Generate -> Variants -> User Picks -> Export -> Implement
```
### Steps
1. Generate base design
2. Generate 2-3 variants with different aspects (color, layout)
3. Show all variants to user
4. User selects preferred variant
5. Export selected variant
6. Hand off to implementation
### Example
```bash
# Generate with variants
npx tsx stitch-generate.ts "SaaS pricing page" --variants 3
# User reviews images, picks variant 2
npx tsx stitch-export.ts <variant-2-screen-id> --format all
```
## DESIGN.md Specification
DESIGN.md is an agent-readable markdown file containing extracted design tokens.
### Structure
```markdown
# Design System
## Colors
- `bg-blue-600` (primary background)
- `text-gray-900` (body text)
- `border-gray-200` (dividers)
## Typography
- `text-2xl font-bold` (headings)
- `text-base font-normal` (body)
## Spacing
- `p-4` (card padding)
- `gap-6` (grid gaps)
## Components
- `<nav>` (top navigation)
- `<section>` (content sections)
- `<form>` (user inputs)
- `<button>` (actions)
## Notes
- Generated by Google Stitch AI
- Tailwind CSS utility classes
```
### How Implementation Skills Consume It
1. Skill checks for `DESIGN.md` in project root or plan directory
2. If found: reads tokens and uses them as constraints for implementation
3. If not found: falls back to text-based design (existing behavior)
4. DESIGN.md takes precedence over verbal design descriptions
## Skill Routing
| Design Need | Skill | What It Does |
|-------------|-------|-------------|
| React/Vue/Svelte components | `ck:frontend-design` | Converts Tailwind HTML to framework components |
| Full page with style guide | `ck:ui-ux-pro-max` | Builds complete layouts with design system |
| Design token extraction | `ck:ui-styling` | Extracts shadcn/Tailwind tokens from DESIGN.md |
## Handoff Protocol
1. `ck:stitch` exports artifacts to `./stitch-exports/` (or plan directory)
2. Copy `DESIGN.md` to project root if implementation skill expects it there
3. Activate target skill with instruction: "Implement from DESIGN.md in project root"
4. Target skill reads DESIGN.md, applies tokens, generates code
5. No modifications needed to existing skills — they read DESIGN.md opportunistically
## Quota-Aware Pipeline
Always check quota before generating:
```bash
# Pre-check
npx tsx stitch-quota.ts check
# If exhausted (exit code 2):
# -> Skip Stitch, use ck:ui-ux-pro-max for text-based design instead
# If available:
npx tsx stitch-generate.ts "<prompt>"
npx tsx stitch-quota.ts increment
```

View File

@@ -0,0 +1,64 @@
# Quota Management
Google Stitch free tier quota tracking and conservation strategies.
## Limits
| Pool | Credits/Day | Reset |
|------|-------------|-------|
| Daily Credits (generate, variants) | 400 | Midnight UTC |
| Redesign Credits (edit_screens) | 15 | Midnight UTC |
Each generation = 1 credit. Each variant = 1 credit. Each edit = 1 redesign credit (separate pool).
## Local Tracking
Stitch SDK has no programmatic quota check endpoint. ClaudeKit tracks locally:
**File:** `~/.claudekit/.stitch-quota.json`
```json
{
"date": "2026-03-23",
"count": 42,
"limit": 400
}
```
**Auto-reset:** When `date` != today (UTC), count resets to 0.
**Override limit:** `export STITCH_QUOTA_LIMIT="300"` (if Google increases limits).
## Warning Thresholds
| Remaining | Action |
|-----------|--------|
| > 20% | Normal operation |
| < 20% | `[!] Low quota` warning printed |
| 0 | `[X] Exhausted` exit code 2, suggest fallback |
## Conservation Tips
1. **Use variants instead of regenerating** 3 variants = 3 credits vs regenerating 3 times = 3 credits, but variants are more purposeful
2. **Use `screen.edit()` to refine** Editing costs 1 credit but preserves context
3. **Export early** Don't regenerate just to see the design again; export HTML/image once
4. **Batch planning** Plan all designs for the day, generate in one session
5. **Review prompts** Better prompts = fewer regenerations
## Fallback Workflow
When quota is exhausted:
1. `stitch-quota.ts check` returns exit code 2
2. Skill prints: "Daily quota exhausted. Use ck:ui-ux-pro-max as fallback."
3. Activate `ck:ui-ux-pro-max` with the same design prompt
4. `ui-ux-pro-max` generates text-based design spec (no external API needed)
5. Proceed with implementation using text-based spec
## Drift Warning
Local tracking can drift if user generates designs outside ClaudeKit (via Stitch web UI or other tools). If you hit `RATE_LIMITED` error despite local tracker showing credits available:
1. Run `npx tsx stitch-quota.ts reset`
2. Set count to match actual usage (or leave at 0 if unknown)
3. Stitch API itself enforces the real limit local tracker is advisory only

View File

@@ -0,0 +1,95 @@
# Stitch MCP Server Setup
Three options for connecting Google Stitch as an MCP server with Claude Code.
## Option A: API Key (Recommended for Most Users)
Simplest setup. No Google Cloud dependency.
### 1. Get API Key
1. Sign in at https://stitch.withgoogle.com
2. Go to Settings -> API Keys
3. Click "Generate New Key"
4. Copy `sk_...` key
### 2. Add to `.opencode/.mcp.json`
```json
{
"mcpServers": {
"stitch": {
"command": "npx",
"args": ["@_davideast/stitch-mcp", "proxy"],
"env": {
"STITCH_API_KEY": "sk_your_key_here"
}
}
}
}
```
### 3. Verify
Restart Claude Code. You should see Stitch tools available:
- `create_project` — Create new design project
- `generate_screen` — Generate UI from prompt
- `export_html` — Export as HTML/Tailwind
- `export_image` — Export screenshot
## Option B: Google Cloud (For GCP Users)
Uses gcloud credentials. Zero API key management.
### 1. Setup gcloud
```bash
gcloud auth login
gcloud config set project YOUR_PROJECT_ID
gcloud beta services mcp enable stitch.googleapis.com
```
### 2. Add to `.opencode/.mcp.json`
```json
{
"mcpServers": {
"stitch": {
"command": "npx",
"args": ["-y", "stitch-mcp"],
"env": {
"GOOGLE_CLOUD_PROJECT": "your-gcp-project-id"
}
}
}
}
```
## Option C: Auto-Installer (Interactive)
Guided wizard, auto-detects environment.
```bash
npx stitch-mcp-auto
# Opens browser at http://localhost:8086
# Follow wizard to configure
```
## Troubleshooting
| Symptom | Fix |
|---------|-----|
| "AUTH_FAILED" on startup | Verify API key or re-run `gcloud auth login` |
| Tools not appearing | Restart Claude Code after config change |
| Timeout on generation | Stitch is processing; wait 10-30s for complex designs |
| "RATE_LIMITED" errors | Daily quota exceeded; wait until midnight UTC |
## MCP Config Location
| OS | Path |
|----|------|
| macOS | `~/Library/Application Support/Claude/claude_desktop_config.json` |
| Windows | `%AppData%\Claude\claude_desktop_config.json` |
| Linux | `~/.config/Claude/claude_desktop_config.json` |
For Claude Code CLI: use `.opencode/.mcp.json` in project root (preferred for ClaudeKit).

View File

@@ -0,0 +1,136 @@
# Stitch SDK API Reference
Condensed reference for `@google/stitch-sdk`. Agent-optimized — covers common operations only.
## Installation
```bash
npm install @google/stitch-sdk
# Optional: Vercel AI SDK integration
npm install @google/stitch-sdk ai
```
## Authentication
```bash
export STITCH_API_KEY="sk_..." # From https://stitch.withgoogle.com/settings/api
```
SDK auto-reads `STITCH_API_KEY` from environment. No explicit config needed.
## Core API
### Stitch (root singleton)
```typescript
import { stitch } from "@google/stitch-sdk";
const projects = await stitch.projects(); // List all projects
const project = stitch.project("my-id"); // Get project handle (sync)
```
### Project
```typescript
const project = stitch.project("project-123"); // sync — returns handle
// Generate screen from prompt (positional params, uppercase device types)
const screen = await project.generate(
"Login page with email/password",
"MOBILE" // optional: "MOBILE" | "DESKTOP" | "TABLET" | "AGNOSTIC"
);
const screens = await project.screens(); // List all screens
const screen = await project.getScreen(screenId); // Get specific screen
```
### Screen
```typescript
// Export
const htmlUrl = await screen.getHtml(); // Returns download URL (HTML + Tailwind)
const imageUrl = await screen.getImage(); // Returns download URL (PNG screenshot)
// Edit/refine (positional: prompt, deviceType?, modelId?)
const edited = await screen.edit("Make colors darker, add search bar");
// Generate variants (positional: prompt, variantOptions, deviceType?, modelId?)
const variants = await screen.variants("Different color schemes", {
variantCount: 3, // 1-5 variants
creativeRange: "medium", // "low" | "medium" | "high"
aspects: ["COLOR_SCHEME"] // "COLOR_SCHEME" | "LAYOUT" | etc.
});
```
### StitchToolClient (low-level MCP access)
```typescript
import { StitchToolClient } from "@google/stitch-sdk";
const client = new StitchToolClient({ apiKey: "sk_..." });
const tools = await client.listTools();
const result = await client.callTool("create_project", { title: "My App" });
```
### Vercel AI SDK Integration
```typescript
import { stitchTools } from "@google/stitch-sdk/ai";
import { generateText } from "ai";
const { text } = await generateText({
model: yourModel,
tools: stitchTools(),
prompt: "Create a modern dashboard"
});
```
## Type Definitions
```typescript
type DeviceType = "DEVICE_TYPE_UNSPECIFIED" | "MOBILE" | "DESKTOP" | "TABLET" | "AGNOSTIC";
type ModelId = "MODEL_ID_UNSPECIFIED" | "GEMINI_3_PRO" | "GEMINI_3_FLASH";
interface Stitch {
projects(): Promise<Project[]>;
createProject(title?: string): Promise<Project>;
project(id: string): Project; // sync — returns handle, no API call
}
interface Project {
id: string;
generate(prompt: string, deviceType?: DeviceType, modelId?: ModelId): Promise<Screen>;
screens(): Promise<Screen[]>;
getScreen(screenId: string): Promise<Screen>;
}
interface Screen {
id: string;
getHtml(): Promise<string>; // Download URL
getImage(): Promise<string>; // Download URL
edit(prompt: string, deviceType?: DeviceType, modelId?: ModelId): Promise<Screen>;
variants(prompt: string, variantOptions: any, deviceType?: DeviceType, modelId?: ModelId): Promise<Screen[]>;
}
class StitchError extends Error {
code: "AUTH_FAILED" | "NOT_FOUND" | "RATE_LIMITED" | string;
}
```
## Error Handling
| Code | Meaning | Action |
|------|---------|--------|
| `AUTH_FAILED` | Bad/missing API key | Check `STITCH_API_KEY` env var |
| `NOT_FOUND` | Screen/project doesn't exist | Verify ID |
| `RATE_LIMITED` | Daily quota exceeded | Wait until midnight UTC or use fallback |
```typescript
try {
const screen = await project.generate(prompt);
} catch (error) {
if (error.code === "RATE_LIMITED") {
console.error("Quota exceeded — use ck:ui-ux-pro-max fallback");
}
}
```

View File

@@ -0,0 +1,17 @@
{
"name": "stitch-scripts",
"version": "1.0.0",
"type": "module",
"description": "ClaudeKit Stitch skill SDK wrapper scripts",
"scripts": {
"generate": "tsx stitch-generate.ts",
"export": "tsx stitch-export.ts",
"quota": "tsx stitch-quota.ts"
},
"dependencies": {
"@google/stitch-sdk": ">=0.0.3 <1.0.0"
},
"devDependencies": {
"tsx": "^4.0.0"
}
}

View File

@@ -0,0 +1,173 @@
/**
* stitch-export.ts — Export Stitch designs as HTML, image, or DESIGN.md.
*
* Usage:
* npx tsx stitch-export.ts <screen-id> [--project <id>] [--format html|image|all] [--output <dir>]
*
* Env: STITCH_API_KEY (required), STITCH_PROJECT_ID (optional default)
*/
import { stitch } from "@google/stitch-sdk";
import fs from "fs";
import path from "path";
// -- Argument parsing --
const args = process.argv.slice(2);
function getFlag(name: string): string | undefined {
const idx = args.indexOf(`--${name}`);
if (idx === -1 || idx + 1 >= args.length) return undefined;
return args[idx + 1];
}
// Extract positional args (skip flags and their values)
function getPositionalArgs(): string[] {
const positional: string[] = [];
for (let i = 0; i < args.length; i++) {
if (args[i].startsWith("--")) {
i++; // skip flag value
} else {
positional.push(args[i]);
}
}
return positional;
}
const screenId = getPositionalArgs()[0];
const projectId =
getFlag("project") || process.env.STITCH_PROJECT_ID || "claudekit-default";
const format = getFlag("format") || "all";
const outputDir = getFlag("output") || "./stitch-exports";
if (!screenId) {
console.error("Usage: npx tsx stitch-export.ts <screen-id> [--project <id>] [--format html|image|all] [--output <dir>]");
process.exit(1);
}
if (!process.env.STITCH_API_KEY) {
console.error("[X] STITCH_API_KEY not set. Get one at https://stitch.withgoogle.com/settings/api");
process.exit(1);
}
// -- Helpers --
async function downloadFile(url: string, dest: string): Promise<void> {
const response = await fetch(url);
if (!response.ok) throw new Error(`Download failed: ${response.status} ${response.statusText}`);
const buffer = Buffer.from(await response.arrayBuffer());
fs.writeFileSync(dest, buffer);
}
/**
* Generate a DESIGN.md from HTML content by extracting Tailwind classes,
* color values, typography patterns, and component structure.
*/
function generateDesignMd(html: string): string {
const lines: string[] = ["# Design System", "", "Auto-generated from Google Stitch export.", ""];
// Extract colors from Tailwind classes and inline styles
const colorMatches = html.match(/(?:bg|text|border)-(?:\w+)-(\d+)/g) || [];
const uniqueColors = [...new Set(colorMatches)].slice(0, 20);
if (uniqueColors.length > 0) {
lines.push("## Colors", "");
uniqueColors.forEach((c) => lines.push(`- \`${c}\``));
lines.push("");
}
// Extract typography classes
const textMatches = html.match(/(?:text-(?:xs|sm|base|lg|xl|2xl|3xl|4xl|5xl)|font-(?:thin|light|normal|medium|semibold|bold|extrabold))/g) || [];
const uniqueText = [...new Set(textMatches)].slice(0, 15);
if (uniqueText.length > 0) {
lines.push("## Typography", "");
uniqueText.forEach((t) => lines.push(`- \`${t}\``));
lines.push("");
}
// Extract spacing patterns
const spacingMatches = html.match(/(?:p|m|gap|space)-(?:x|y)?-?\d+/g) || [];
const uniqueSpacing = [...new Set(spacingMatches)].slice(0, 15);
if (uniqueSpacing.length > 0) {
lines.push("## Spacing", "");
uniqueSpacing.forEach((s) => lines.push(`- \`${s}\``));
lines.push("");
}
// Extract component-level structure from HTML tags
const componentMatches = html.match(/<(section|nav|header|footer|main|aside|form|button|input|table|dialog)[^>]*>/gi) || [];
const uniqueComponents = [...new Set(componentMatches.map((c) => c.match(/<(\w+)/)?.[1] || ""))].filter(Boolean);
if (uniqueComponents.length > 0) {
lines.push("## Components", "");
uniqueComponents.forEach((c) => lines.push(`- \`<${c}>\``));
lines.push("");
}
lines.push("## Notes", "", "- Generated by Google Stitch AI", "- Tailwind CSS utility classes used throughout", "- Review and customize colors/typography for brand alignment");
return lines.join("\n");
}
// -- Main --
async function main() {
try {
fs.mkdirSync(outputDir, { recursive: true });
console.error(`[i] Exporting screen ${screenId} from project ${projectId}`);
// Resolve project handle — for default project, find by title first
let project;
if (projectId === "claudekit-default") {
const projects = await stitch.projects();
const found = projects.find(p => p.data?.title === "claudekit-default");
project = found || stitch.project(projectId);
} else {
project = stitch.project(projectId);
}
const screen = await project.getScreen(screenId!);
const exported: Record<string, string> = {};
// Export HTML
if (format === "html" || format === "all") {
const htmlUrl = await screen.getHtml();
const htmlPath = path.join(outputDir, "design.html");
await downloadFile(htmlUrl, htmlPath);
exported.html = htmlPath;
console.error(`[OK] HTML exported: ${htmlPath}`);
// Generate DESIGN.md from HTML
if (format === "all") {
const htmlContent = fs.readFileSync(htmlPath, "utf-8");
const designMd = generateDesignMd(htmlContent);
const designPath = path.join(outputDir, "DESIGN.md");
fs.writeFileSync(designPath, designMd);
exported.designMd = designPath;
console.error(`[OK] DESIGN.md generated: ${designPath}`);
}
}
// Export image
if (format === "image" || format === "all") {
const imageUrl = await screen.getImage();
const imagePath = path.join(outputDir, "design.png");
await downloadFile(imageUrl, imagePath);
exported.image = imagePath;
console.error(`[OK] Image exported: ${imagePath}`);
}
// Output result JSON to stdout
console.log(JSON.stringify({ screenId, projectId, format, exported }, null, 2));
} catch (error: unknown) {
const err = error as { code?: string; message?: string };
if (err.code === "NOT_FOUND") {
console.error(`[X] Screen "${screenId}" not found in project "${projectId}".`);
} else if (err.code === "AUTH_FAILED") {
console.error("[X] Authentication failed. Check STITCH_API_KEY env var.");
} else {
console.error(`[X] Export error: ${err.message || error}`);
}
process.exit(1);
}
}
main();

View File

@@ -0,0 +1,182 @@
/**
* stitch-generate.ts — Generate UI designs from text prompts via Google Stitch SDK.
*
* Usage:
* npx tsx stitch-generate.ts "<prompt>" [--project <id>] [--device mobile|desktop|tablet] [--variants <count>]
*
* Env: STITCH_API_KEY (required), STITCH_PROJECT_ID (optional default)
*/
import { stitch } from "@google/stitch-sdk";
import fs from "fs";
import path from "path";
import os from "os";
// -- Quota helpers (inline to avoid cross-script import issues) --
const QUOTA_DIR = path.join(os.homedir(), ".claudekit");
const QUOTA_FILE = path.join(QUOTA_DIR, ".stitch-quota.json");
// Stitch free tier: 400 daily credits. No API to fetch real usage.
const DEFAULT_LIMIT = parseInt(process.env.STITCH_QUOTA_LIMIT || "400", 10);
interface QuotaState { date: string; count: number; limit: number; }
function todayUTC(): string { return new Date().toISOString().slice(0, 10); }
function loadQuota(): QuotaState {
try {
if (fs.existsSync(QUOTA_FILE)) {
const data = JSON.parse(fs.readFileSync(QUOTA_FILE, "utf-8"));
if (data.date !== todayUTC()) return { date: todayUTC(), count: 0, limit: data.limit || DEFAULT_LIMIT };
return data;
}
} catch { /* corrupted — start fresh */ }
return { date: todayUTC(), count: 0, limit: DEFAULT_LIMIT };
}
function saveQuota(state: QuotaState): void {
fs.mkdirSync(QUOTA_DIR, { recursive: true });
fs.writeFileSync(QUOTA_FILE, JSON.stringify(state, null, 2));
}
// -- Argument parsing (minimal, no deps) --
const args = process.argv.slice(2);
function getFlag(name: string): string | undefined {
const idx = args.indexOf(`--${name}`);
if (idx === -1 || idx + 1 >= args.length) return undefined;
return args[idx + 1];
}
// Extract positional args (skip flags and their values)
function getPositionalArgs(): string[] {
const positional: string[] = [];
for (let i = 0; i < args.length; i++) {
if (args[i].startsWith("--")) {
i++; // skip flag value
} else {
positional.push(args[i]);
}
}
return positional;
}
const prompt = getPositionalArgs()[0];
const projectId =
getFlag("project") || process.env.STITCH_PROJECT_ID || "claudekit-default";
// SDK expects uppercase device types: MOBILE, DESKTOP, TABLET, AGNOSTIC
const deviceFlag = getFlag("device");
const DEVICE_MAP: Record<string, "MOBILE" | "DESKTOP" | "TABLET"> = {
mobile: "MOBILE", desktop: "DESKTOP", tablet: "TABLET",
};
const deviceType = deviceFlag
? DEVICE_MAP[deviceFlag.toLowerCase()] || (deviceFlag.toUpperCase() as "MOBILE" | "DESKTOP" | "TABLET")
: undefined;
const variantCount = getFlag("variants") ? parseInt(getFlag("variants")!, 10) : 0;
if (!prompt) {
console.error("Usage: npx tsx stitch-generate.ts <prompt> [--project <id>] [--device mobile|desktop|tablet] [--variants <count>]");
process.exit(1);
}
if (!process.env.STITCH_API_KEY) {
console.error("[X] STITCH_API_KEY not set. Get one at https://stitch.withgoogle.com/settings/api");
process.exit(1);
}
// -- Main --
async function main() {
try {
// Pre-check quota — 1 credit per generate, 1 per variant
const creditsNeeded = 1 + variantCount;
const quota = loadQuota();
const remaining = quota.limit - quota.count;
if (remaining < creditsNeeded) {
console.error(`[X] Not enough credits: need ${creditsNeeded}, have ${remaining}/${quota.limit}.`);
console.error("[i] Use ck:ui-ux-pro-max as fallback, or wait until midnight UTC.");
process.exit(2);
}
console.error(`[i] Credits: ${remaining}/${quota.limit} remaining (this run costs ${creditsNeeded})`);
console.error(`[i] Prompt: "${prompt}"`);
// Resolve project — use existing or create if "claudekit-default" doesn't exist
const isDefaultProject = projectId === "claudekit-default";
let resolvedProjectId = projectId;
if (isDefaultProject) {
const projects = await stitch.projects();
const existing = projects.find(p => p.data?.title === "claudekit-default");
if (existing) {
resolvedProjectId = existing.id;
console.error(`[i] Using project: ${resolvedProjectId}`);
} else {
console.error("[i] Creating default project 'claudekit-default'...");
const created = await stitch.createProject("claudekit-default");
resolvedProjectId = created.id;
console.error(`[OK] Created project: ${resolvedProjectId}`);
}
} else {
console.error(`[i] Using project: ${resolvedProjectId}`);
}
// Always use a fresh handle for generation
const project = stitch.project(resolvedProjectId);
// SDK signature: generate(prompt, deviceType?, modelId?)
const screen = await project.generate(prompt!, deviceType);
const imageUrl = await screen.getImage();
const result: Record<string, unknown> = {
screenId: screen.id,
projectId: resolvedProjectId,
imageUrl,
prompt,
};
// Generate variants if requested
if (variantCount > 0) {
console.error(`[i] Generating ${variantCount} variant(s)...`);
const variants = await screen.variants("Generate design variants", {
variantCount,
creativeRange: "medium",
});
result.variants = await Promise.all(
variants.map(async (v) => ({
screenId: v.id,
imageUrl: await v.getImage(),
}))
);
}
// Auto-increment quota tracker
const postQuota = loadQuota();
postQuota.count += creditsNeeded;
saveQuota(postQuota);
const postRemaining = postQuota.limit - postQuota.count;
console.error(`[OK] Quota updated: ${postQuota.count}/${postQuota.limit} used (${postRemaining} remaining)`);
result.creditsUsed = creditsNeeded;
result.creditsRemaining = postRemaining;
// Output JSON to stdout (logs go to stderr)
console.log(JSON.stringify(result, null, 2));
} catch (error: unknown) {
const err = error as { code?: string; message?: string };
if (err.code === "RATE_LIMITED") {
// Auto-sync local tracker — API is the source of truth
const q = loadQuota();
q.count = q.limit;
saveQuota(q);
console.error("[X] Daily quota exceeded (local tracker synced). Try tomorrow or use ck:ui-ux-pro-max.");
} else if (err.code === "AUTH_FAILED") {
console.error("[X] Authentication failed. Check STITCH_API_KEY env var.");
} else {
console.error(`[X] Stitch error: ${err.message || error}`);
}
process.exit(1);
}
}
main();

View File

@@ -0,0 +1,123 @@
/**
* stitch-quota.ts — Local quota tracker for Google Stitch daily credits.
*
* Usage:
* npx tsx stitch-quota.ts check # Show remaining credits
* npx tsx stitch-quota.ts increment # Bump count after generation
* npx tsx stitch-quota.ts reset # Force reset counter
*
* Tracks usage in ~/.claudekit/.stitch-quota.json.
* Auto-resets when date changes (UTC midnight).
*/
import fs from "fs";
import path from "path";
import os from "os";
// -- Config --
const QUOTA_DIR = path.join(os.homedir(), ".claudekit");
const QUOTA_FILE = path.join(QUOTA_DIR, ".stitch-quota.json");
// Stitch free tier: 400 daily credits (generate), 15 redesign credits (edit)
// Source: stitch.withgoogle.com dashboard. No API to fetch real usage.
const DEFAULT_LIMIT = parseInt(process.env.STITCH_QUOTA_LIMIT || "400", 10);
const WARN_THRESHOLD = 0.2; // Warn when <20% remaining
interface QuotaState {
date: string;
count: number;
limit: number;
}
// -- Helpers --
function todayUTC(): string {
return new Date().toISOString().slice(0, 10);
}
function loadQuota(): QuotaState {
try {
if (fs.existsSync(QUOTA_FILE)) {
const data = JSON.parse(fs.readFileSync(QUOTA_FILE, "utf-8"));
// Auto-reset if date changed
if (data.date !== todayUTC()) {
return { date: todayUTC(), count: 0, limit: data.limit || DEFAULT_LIMIT };
}
return data;
}
} catch {
// Corrupted file — start fresh
}
return { date: todayUTC(), count: 0, limit: DEFAULT_LIMIT };
}
function saveQuota(state: QuotaState): void {
fs.mkdirSync(QUOTA_DIR, { recursive: true });
fs.writeFileSync(QUOTA_FILE, JSON.stringify(state, null, 2));
}
// -- Commands --
function check(): void {
const state = loadQuota();
saveQuota(state); // Persist auto-reset if date changed
const remaining = state.limit - state.count;
const pct = remaining / state.limit;
console.log(JSON.stringify({
date: state.date,
used: state.count,
remaining,
limit: state.limit,
percentRemaining: Math.round(pct * 100),
}, null, 2));
if (remaining <= 0) {
console.error("[X] Daily quota exhausted. Use ck:ui-ux-pro-max as fallback.");
process.exit(2);
} else if (pct < WARN_THRESHOLD) {
console.error(`[!] Low quota: ${remaining}/${state.limit} credits remaining (${Math.round(pct * 100)}%)`);
} else {
console.error(`[OK] ${remaining}/${state.limit} credits remaining`);
}
}
function increment(): void {
const state = loadQuota();
state.count += 1;
saveQuota(state);
const remaining = state.limit - state.count;
console.error(`[OK] Quota updated: ${state.count}/${state.limit} used (${remaining} remaining)`);
if (remaining <= 0) {
console.error("[!] Daily quota now exhausted.");
} else if (remaining / state.limit < WARN_THRESHOLD) {
console.error(`[!] Low quota warning: ${remaining} credits remaining`);
}
}
function reset(): void {
const state: QuotaState = { date: todayUTC(), count: 0, limit: DEFAULT_LIMIT };
saveQuota(state);
console.error(`[OK] Quota reset: 0/${state.limit} used`);
}
// -- Main --
const command = process.argv[2];
switch (command) {
case "check":
check();
break;
case "increment":
increment();
break;
case "reset":
reset();
break;
default:
console.error("Usage: npx tsx stitch-quota.ts <check|increment|reset>");
process.exit(1);
}