-
Notifications
You must be signed in to change notification settings - Fork 2.2k
Expand file tree
/
Copy pathagentPromptGenerator.ts
More file actions
211 lines (191 loc) · 8.1 KB
/
agentPromptGenerator.ts
File metadata and controls
211 lines (191 loc) · 8.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
/**
* Generate AGENTS.md and CLAUDE.md for captured website projects.
*
* Writes the same content to both filenames so any AI agent auto-discovers it:
* - AGENTS.md — universal convention (Cursor, Codex, Gemini CLI, Windsurf, Aider, Jules)
* - CLAUDE.md — Claude Code convention
*
* This file generates a DATA INVENTORY that tells the AI agent what files
* exist and what they contain. The actual workflow lives in the
* website-to-hyperframes skill — this file points agents there.
*/
import { writeFileSync, readdirSync, existsSync } from "node:fs";
import { join } from "node:path";
import type { DesignTokens } from "./types.js";
import type { AnimationCatalog } from "./animationCataloger.js";
import type { CatalogedAsset } from "./assetCataloger.js";
/**
* Infer a human-readable role hint from a hex color based on luminance and saturation.
* Not a substitute for DESIGN.md — just helps orient agents scanning the brand summary.
*/
function inferColorRole(hex: string): string {
const r = parseInt(hex.slice(1, 3), 16) / 255;
const g = parseInt(hex.slice(3, 5), 16) / 255;
const b = parseInt(hex.slice(5, 7), 16) / 255;
if (isNaN(r) || isNaN(g) || isNaN(b)) return "color";
const max = Math.max(r, g, b);
const min = Math.min(r, g, b);
const luminance = 0.2126 * r + 0.7152 * g + 0.0722 * b;
const saturation = max === 0 ? 0 : (max - min) / max;
if (luminance < 0.04) return "bg-dark";
if (luminance > 0.9) return "bg-light";
if (saturation > 0.4 && luminance > 0.05 && luminance < 0.7) return "accent";
if (luminance < 0.2) return "surface-dark";
if (luminance > 0.7) return "surface-light";
return "neutral";
}
export function generateAgentPrompt(
outputDir: string,
url: string,
tokens: DesignTokens,
_animations: AnimationCatalog | undefined, // reserved for future animation summary
hasScreenshot: boolean,
hasLottie?: boolean,
hasShaders?: boolean,
_catalogedAssets?: CatalogedAsset[], // reserved for future asset inventory
_detectedLibraries?: string[],
): void {
const prompt = buildPrompt(outputDir, url, tokens, hasScreenshot, hasLottie, hasShaders);
writeFileSync(join(outputDir, "AGENTS.md"), prompt, "utf-8");
writeFileSync(join(outputDir, "CLAUDE.md"), prompt, "utf-8");
writeFileSync(join(outputDir, ".cursorrules"), prompt, "utf-8");
}
function buildPrompt(
outputDir: string,
url: string,
tokens: DesignTokens,
hasScreenshot: boolean,
hasLottie?: boolean,
hasShaders?: boolean,
): string {
const title = tokens.title || new URL(url).hostname.replace(/^www\./, "");
const colorSummary = tokens.colors
.slice(0, 10)
.map((hex) => `${hex} (${inferColorRole(hex)})`)
.join(", ");
const fontSummary =
tokens.fonts
.map(
(f) =>
f.family +
(f.variable && f.weightRange
? ` (${f.weightRange[0]}-${f.weightRange[1]} variable)`
: f.weights.length > 0
? ` (${f.weights.join(",")})`
: ""),
)
.join(", ") || "none detected";
// Build the data inventory table rows
// Helper: find all contact sheet pages for a given base name. Matches the
// exact base file plus paginated variants only (e.g. `contact-sheet.jpg`,
// `contact-sheet-2.jpg`, `contact-sheet-3.jpg`). The "-NNN" suffix is digits
// only, so unrelated files that happen to share the prefix (notably the
// `contact-sheet-svgs.jpg` SVG fallback sheet in assets/) don't get mixed in.
function contactSheetRows(dir: string, baseFile: string, label: string): string[] {
const fullDir = join(outputDir, dir);
if (!existsSync(fullDir)) return [];
const baseName = baseFile.replace(/\.jpg$/, "");
// Escape regex metacharacters in baseName so future callers can pass
// filenames containing `.`, `+`, `(`, etc. without the regex breaking.
const escapedBase = baseName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const paginatedRe = new RegExp(`^${escapedBase}(?:-(\\d+))?\\.jpg$`);
// Sort by the numeric page suffix so `contact-sheet-10.jpg` lands after
// `contact-sheet-2.jpg`, not before (default string sort orders them
// lexicographically and breaks at 10+ pages). Unpaginated `contact-sheet.jpg`
// gets page 0 so it sorts first if it co-exists with paginated files.
const all = readdirSync(fullDir)
.filter((f) => paginatedRe.test(f))
.map((f) => ({ name: f, page: parseInt(f.match(paginatedRe)?.[1] ?? "0", 10) }))
.sort((a, b) => a.page - b.page)
.map((entry) => entry.name);
if (all.length === 0) return [];
if (all.length === 1) {
return [`| \`${dir}/${all[0]}\` | ${label} |`];
}
return all.map((f, i) => `| \`${dir}/${f}\` | ${label} — page ${i + 1} of ${all.length} |`);
}
const tableRows: string[] = [];
if (hasScreenshot) {
const screenshotRows = contactSheetRows(
"screenshots",
"contact-sheet.jpg",
"**View this first.** All scroll screenshots in labeled grid — see the entire page at a glance",
);
if (screenshotRows.length > 0) {
tableRows.push(...screenshotRows);
} else {
tableRows.push(
"| `screenshots/contact-sheet.jpg` | **View this first.** All scroll screenshots in one labeled grid. |",
);
}
tableRows.push(
"| `screenshots/scroll-*.png` | Individual viewport screenshots if you need detail on a specific section. |",
);
}
tableRows.push(
`| \`extracted/tokens.json\` | Design tokens: ${tokens.colors.length} colors, ${tokens.fonts.length} fonts, ${tokens.headings?.length ?? 0} headings, ${tokens.ctas?.length ?? 0} CTAs |`,
);
// design-styles.json is written from a try/catch in capture/index.ts and
// gets skipped when the live-DOM style extraction fails. Only list it in the
// agent prompt when it actually exists, so the agent isn't pointed at a 404.
if (existsSync(join(outputDir, "extracted", "design-styles.json"))) {
tableRows.push(
"| `extracted/design-styles.json` | Computed styles from live DOM: typography hierarchy, button/card/nav styles, spacing scale, border-radius, box shadows. Primary data source for DESIGN.md. |",
);
}
tableRows.push(
"| `extracted/asset-descriptions.md` | One-line description of every downloaded asset. Read this for asset selection — only open individual files for safe-zone checking. |",
);
tableRows.push(
"| `extracted/visible-text.txt` | Page text in DOM order, prefixed with HTML tag (`[h1]`, `[p]`, `[a]`). Use as context — rephrase freely. |",
);
if (hasLottie) {
tableRows.push(
"| `extracted/lottie-manifest.json` | Lottie animations with previews at `assets/lottie/previews/`. |",
);
}
if (hasShaders) {
tableRows.push("| `extracted/shaders.json` | WebGL shader source (GLSL). |");
}
// Asset contact sheets — dynamically list all pages
const assetSheetRows = contactSheetRows(
"assets",
"contact-sheet.jpg",
"Downloaded images in labeled grid — view before opening individual files",
);
if (assetSheetRows.length > 0) {
tableRows.push(...assetSheetRows);
} else {
tableRows.push("| `assets/contact-sheet.jpg` | All downloaded images in one labeled grid. |");
}
// SVG contact sheets — check both assets/svgs/ and assets/ root fallback
const svgSubdirRows = contactSheetRows(
"assets/svgs",
"contact-sheet.jpg",
"SVGs rendered as thumbnails in labeled grid",
);
const svgRootRows = contactSheetRows(
"assets",
"contact-sheet-svgs.jpg",
"SVGs rendered as thumbnails in labeled grid",
);
const svgRows = svgSubdirRows.length > 0 ? svgSubdirRows : svgRootRows;
if (svgRows.length > 0) {
tableRows.push(...svgRows);
}
tableRows.push("| `assets/` | Individual downloaded images, SVGs, and font files. |");
// Brand summary — just the essentials
const brandLines: string[] = [];
brandLines.push(`- **Colors**: ${colorSummary || "see tokens.json"}`);
brandLines.push(`- **Fonts**: ${fontSummary}`);
return `# ${title}
Source: ${url}
To create a video from this capture, use the \`website-to-hyperframes\` skill.
## What's in This Capture
| File | Contents |
|------|----------|
${tableRows.join("\n")}
## Brand Summary
${brandLines.join("\n")}
`;
}