launchframe 0.4.10 → 0.4.12

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.
Files changed (37) hide show
  1. package/.amazonq/cli-agents/launchframe.json +1 -1
  2. package/.amazonq/rules/project.md +6 -7
  3. package/.augment/commands/launchframe.md +16 -0
  4. package/.claude/skills/launchframe/SKILL.md +16 -0
  5. package/.clinerules +6 -7
  6. package/.codex/skills/launchframe/SKILL.md +16 -0
  7. package/.continue/commands/launchframe.md +16 -0
  8. package/.continue/rules/project.md +8 -8
  9. package/.cursor/commands/launchframe.md +16 -0
  10. package/.gemini/commands/launchframe.toml +16 -0
  11. package/.github/copilot-instructions.md +6 -7
  12. package/.github/skills/launchframe/SKILL.md +16 -0
  13. package/.gitignore +4 -0
  14. package/.opencode/commands/launchframe.md +16 -0
  15. package/.windsurf/workflows/launchframe.md +16 -0
  16. package/AGENTS.md +1 -1
  17. package/docs/research/example.com/page-inspection/example.com-2026-05-15T21-24-43-488Z/README.txt +16 -0
  18. package/docs/research/example.com/page-inspection/example.com-2026-05-15T21-24-43-488Z/body-outer.html +2 -0
  19. package/docs/research/example.com/page-inspection/example.com-2026-05-15T21-24-43-488Z/capture-meta.json +19 -0
  20. package/docs/research/example.com/page-inspection/example.com-2026-05-15T21-24-43-488Z/document.html +2 -0
  21. package/docs/research/example.com/page-inspection/example.com-2026-05-15T21-24-43-488Z/inline-styles.json +7 -0
  22. package/docs/research/example.com/page-inspection/example.com-2026-05-15T21-24-43-488Z/motion-summary.json +18 -0
  23. package/docs/research/example.com/page-inspection/example.com-2026-05-15T21-49-07-887Z/README.txt +17 -0
  24. package/docs/research/example.com/page-inspection/example.com-2026-05-15T21-49-07-887Z/ai-page-bundle.txt +29 -0
  25. package/docs/research/example.com/page-inspection/example.com-2026-05-15T21-49-07-887Z/body-outer.html +2 -0
  26. package/docs/research/example.com/page-inspection/example.com-2026-05-15T21-49-07-887Z/capture-meta.json +21 -0
  27. package/docs/research/example.com/page-inspection/example.com-2026-05-15T21-49-07-887Z/document.html +2 -0
  28. package/docs/research/example.com/page-inspection/example.com-2026-05-15T21-49-07-887Z/inline-styles.json +7 -0
  29. package/docs/research/example.com/page-inspection/example.com-2026-05-15T21-49-07-887Z/motion-summary.json +18 -0
  30. package/docs/research/page-captures/example.com-2026-05-15T21-21-31-863Z/README.txt +16 -0
  31. package/docs/research/page-captures/example.com-2026-05-15T21-21-31-863Z/body-outer.html +2 -0
  32. package/docs/research/page-captures/example.com-2026-05-15T21-21-31-863Z/capture-meta.json +19 -0
  33. package/docs/research/page-captures/example.com-2026-05-15T21-21-31-863Z/document.html +2 -0
  34. package/docs/research/page-captures/example.com-2026-05-15T21-21-31-863Z/inline-styles.json +7 -0
  35. package/docs/research/page-captures/example.com-2026-05-15T21-21-31-863Z/motion-summary.json +18 -0
  36. package/package.json +5 -3
  37. package/scripts/page-inspection-dump.mjs +418 -0
@@ -0,0 +1,418 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Capture a live page into files you can attach for an AI agent:
4
+ * — Full serialized HTML after JavaScript runs (like "Copy ▸ outerHTML" on <html>)
5
+ * — Every CSS response the browser loads (plus all inline <style> blocks)
6
+ * — Motion digest: @keyframes blocks, animation* / transition* declarations
7
+ *
8
+ * Prerequisite (once per machine / after npm install):
9
+ * npx playwright install chromium
10
+ *
11
+ * Usage:
12
+ * node scripts/page-inspection-dump.mjs https://example.com
13
+ * node scripts/page-inspection-dump.mjs https://example.com --out ./my-capture
14
+ * node scripts/page-inspection-dump.mjs https://example.com --out-parent docs/research/example.com/page-inspection --scroll-full
15
+ * node scripts/page-inspection-dump.mjs https://example.com --viewport 390,844 --scroll-full
16
+ *
17
+ * Options:
18
+ * --out <dir> Exact output directory (default: docs/research/page-captures/<host>-<stamp>)
19
+ * --out-parent <dir> Parent dir; writes <host>-<iso-stamp>/ inside it (--out wins if both set)
20
+ * --viewport <W>,<H> Browser viewport (default: 1440,900)
21
+ * --wait-until <mode> load | domcontentloaded | networkidle (default: networkidle)
22
+ * --timeout <ms> Navigation timeout (default: 90000)
23
+ * --scroll-full Scroll to the bottom slowly to trigger lazy-loaded regions
24
+ * --no-css-files Skip writing individual .css files (still builds motion from inline + collected text)
25
+ * --no-ai-bundle Skip ai-page-bundle.txt (HTML + all CSS in one .txt for LLM attach)
26
+ */
27
+
28
+ import { chromium } from "playwright";
29
+ import { mkdir, writeFile } from "node:fs/promises";
30
+ import { createHash } from "node:crypto";
31
+ import { dirname, join, resolve } from "node:path";
32
+ import { fileURLToPath } from "node:url";
33
+
34
+ const __dirname = dirname(fileURLToPath(import.meta.url));
35
+
36
+ function parseArgs(argv) {
37
+ const positional = [];
38
+ const flags = new Set();
39
+ /** @type {Record<string, string>} */
40
+ const opts = {};
41
+ for (let i = 2; i < argv.length; i++) {
42
+ const a = argv[i];
43
+ if (a === "--") continue;
44
+ if (a.startsWith("--")) {
45
+ const key = a.slice(2);
46
+ if (
47
+ key === "scroll-full" ||
48
+ key === "no-css-files" ||
49
+ key === "no-ai-bundle"
50
+ ) {
51
+ flags.add(key);
52
+ continue;
53
+ }
54
+ const next = argv[i + 1];
55
+ if (!next || next.startsWith("--")) {
56
+ console.error(`Missing value for --${key}`);
57
+ process.exit(1);
58
+ }
59
+ opts[key] = next;
60
+ i++;
61
+ continue;
62
+ }
63
+ positional.push(a);
64
+ }
65
+ return { positional, flags, opts };
66
+ }
67
+
68
+ function sanitizeFilenamePart(s) {
69
+ return s.replace(/[^a-zA-Z0-9._-]+/g, "_").slice(0, 120);
70
+ }
71
+
72
+ /**
73
+ * @param {string} css
74
+ * @returns {{ name: string, block: string }[]}
75
+ */
76
+ function extractKeyframeBlocks(css) {
77
+ const out = [];
78
+ const re = /@keyframes\s+([^/{[\s]+)\s*\{/gi;
79
+ let m;
80
+ while ((m = re.exec(css)) !== null) {
81
+ const name = m[1].trim();
82
+ const openIdx = m.index + m[0].length - 1;
83
+ if (css[openIdx] !== "{") continue;
84
+ let depth = 0;
85
+ for (let i = openIdx; i < css.length; i++) {
86
+ const c = css[i];
87
+ if (c === "{") depth++;
88
+ else if (c === "}") {
89
+ depth--;
90
+ if (depth === 0) {
91
+ out.push({
92
+ name,
93
+ block: css.slice(m.index, i + 1).trim(),
94
+ });
95
+ break;
96
+ }
97
+ }
98
+ }
99
+ }
100
+ return out;
101
+ }
102
+
103
+ /**
104
+ * @param {string} css
105
+ * @returns {string[]}
106
+ */
107
+ function extractMotionLines(css) {
108
+ const lines = css.split(/\r?\n/);
109
+ const hits = [];
110
+ for (const line of lines) {
111
+ const t = line.trim();
112
+ if (!t || t.startsWith("/*")) continue;
113
+ const lower = t.toLowerCase();
114
+ if (
115
+ lower.includes("animation:") ||
116
+ lower.includes("animation-name:") ||
117
+ lower.includes("animation-duration:") ||
118
+ lower.includes("animation-timing-function:") ||
119
+ lower.includes("animation-delay:") ||
120
+ lower.includes("animation-iteration-count:") ||
121
+ lower.includes("animation-direction:") ||
122
+ lower.includes("animation-fill-mode:") ||
123
+ lower.includes("animation-play-state:") ||
124
+ lower.includes("transition:") ||
125
+ lower.includes("transition-property:") ||
126
+ lower.includes("transition-duration:") ||
127
+ lower.includes("transition-timing-function:") ||
128
+ lower.includes("transition-delay:") ||
129
+ lower.includes("will-change:") ||
130
+ lower.includes("transform:") ||
131
+ lower.includes("@keyframes")
132
+ ) {
133
+ hits.push(t);
134
+ }
135
+ }
136
+ return hits;
137
+ }
138
+
139
+ /**
140
+ * @param {import('playwright').Page} page
141
+ */
142
+ async function scrollFullPage(page) {
143
+ await page.evaluate(async () => {
144
+ const delay = (ms) => new Promise((r) => setTimeout(r, ms));
145
+ const step = Math.max(240, Math.floor(window.innerHeight * 0.85));
146
+ const max = Math.max(
147
+ document.documentElement.scrollHeight,
148
+ document.body?.scrollHeight ?? 0,
149
+ );
150
+ for (let y = 0; y < max; y += step) {
151
+ window.scrollTo(0, y);
152
+ await delay(120);
153
+ }
154
+ window.scrollTo(0, 0);
155
+ await delay(200);
156
+ });
157
+ }
158
+
159
+ async function runCapture() {
160
+ const { positional, flags, opts } = parseArgs(process.argv);
161
+ const urlRaw = positional[0];
162
+ if (!urlRaw) {
163
+ console.error(`Usage: node scripts/page-inspection-dump.mjs <url> [options]`);
164
+ process.exit(1);
165
+ }
166
+
167
+ let url;
168
+ try {
169
+ url = new URL(urlRaw).toString();
170
+ } catch {
171
+ console.error(`Invalid URL: ${urlRaw}`);
172
+ process.exit(1);
173
+ }
174
+
175
+ const viewportParts = (opts.viewport ?? "1440,900").split(",").map((s) => s.trim());
176
+ const vw = Number(viewportParts[0]) || 1440;
177
+ const vh = Number(viewportParts[1] ?? viewportParts[0]) || 900;
178
+
179
+ const waitUntil = opts["wait-until"] ?? "networkidle";
180
+ if (!["load", "domcontentloaded", "networkidle", "commit"].includes(waitUntil)) {
181
+ console.error(`Invalid --wait-until: ${waitUntil}`);
182
+ process.exit(1);
183
+ }
184
+
185
+ const navigationTimeout = Number(opts.timeout ?? "90000");
186
+
187
+ const host = new URL(url).hostname || "page";
188
+ const stamp = new Date().toISOString().replace(/[:.]/g, "-");
189
+ const defaultOut = join(
190
+ __dirname,
191
+ "..",
192
+ "docs",
193
+ "research",
194
+ "page-captures",
195
+ `${sanitizeFilenamePart(host)}-${stamp}`,
196
+ );
197
+ let outDir;
198
+ if (opts.out) {
199
+ outDir = resolve(process.cwd(), opts.out);
200
+ } else if (opts["out-parent"]) {
201
+ outDir = join(
202
+ resolve(process.cwd(), opts["out-parent"]),
203
+ `${sanitizeFilenamePart(host)}-${stamp}`,
204
+ );
205
+ } else {
206
+ outDir = defaultOut;
207
+ }
208
+
209
+ await mkdir(outDir, { recursive: true });
210
+
211
+ /** @type {{ url: string; bytes: number; text: string }[]} */
212
+ const cssBodies = [];
213
+
214
+ const browser = await chromium.launch({ headless: true });
215
+ try {
216
+ const context = await browser.newContext({
217
+ viewport: { width: vw, height: vh },
218
+ userAgent:
219
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 LaunchFramePageDump/1",
220
+ });
221
+ const page = await context.newPage();
222
+
223
+ page.on("response", async (response) => {
224
+ try {
225
+ const ct = (response.headers()["content-type"] ?? "").toLowerCase();
226
+ const rt = response.request().resourceType();
227
+ if (rt !== "stylesheet" && !ct.includes("text/css")) return;
228
+ const buf = await response.body();
229
+ const text = buf.toString("utf8");
230
+ cssBodies.push({ url: response.url(), bytes: buf.byteLength, text });
231
+ } catch {
232
+ // ignore
233
+ }
234
+ });
235
+
236
+ await page.goto(url, {
237
+ waitUntil: /** @type {'load'} */ (waitUntil),
238
+ timeout: navigationTimeout,
239
+ });
240
+
241
+ if (flags.has("scroll-full")) {
242
+ await scrollFullPage(page);
243
+ }
244
+
245
+ const [fullHtml, bodyOuter, inlineStyles, scriptInventory] = await page.evaluate(() => {
246
+ const styles = Array.from(document.querySelectorAll("style")).map((el, i) => ({
247
+ index: i,
248
+ media: el.getAttribute("media") ?? "",
249
+ text: el.textContent ?? "",
250
+ }));
251
+ const scripts = Array.from(document.querySelectorAll("script")).map((el, i) => ({
252
+ index: i,
253
+ src: el.getAttribute("src") ?? "",
254
+ type: el.getAttribute("type") ?? "",
255
+ async: el.hasAttribute("async"),
256
+ defer: el.hasAttribute("defer"),
257
+ inlineChars: (el.textContent ?? "").length,
258
+ }));
259
+ return [
260
+ document.documentElement.outerHTML,
261
+ document.body ? document.body.outerHTML : "",
262
+ styles,
263
+ scripts,
264
+ ];
265
+ });
266
+
267
+ await writeFile(join(outDir, "document.html"), fullHtml, "utf8");
268
+ if (bodyOuter) {
269
+ await writeFile(join(outDir, "body-outer.html"), bodyOuter, "utf8");
270
+ }
271
+
272
+ await writeFile(
273
+ join(outDir, "inline-styles.json"),
274
+ JSON.stringify(inlineStyles, null, 2),
275
+ "utf8",
276
+ );
277
+
278
+ if (!flags.has("no-css-files")) {
279
+ for (let i = 0; i < cssBodies.length; i++) {
280
+ const { url: cssUrl, text } = cssBodies[i];
281
+ const hash = createHash("sha256").update(text).digest("hex").slice(0, 12);
282
+ let base = "stylesheet";
283
+ try {
284
+ const u = new URL(cssUrl);
285
+ base = sanitizeFilenamePart(
286
+ (u.pathname.split("/").filter(Boolean).pop() || "stylesheet").replace(/\.css$/i, ""),
287
+ );
288
+ } catch {
289
+ base = "stylesheet";
290
+ }
291
+ const filename = `${String(i + 1).padStart(3, "0")}-${base}-${hash}.css`;
292
+ await writeFile(join(outDir, filename), text, "utf8");
293
+ }
294
+ }
295
+
296
+ const inlineCssText = inlineStyles
297
+ .map((s) => `/* inline style #${s.index} media=${s.media || "all"} */\n${s.text}`)
298
+ .join("\n\n");
299
+
300
+ const aggregatedCss = [
301
+ inlineCssText,
302
+ ...cssBodies.map((c) => `/* network stylesheet: ${c.url} */\n${c.text}`),
303
+ ].join("\n\n");
304
+
305
+ let aiBundleBytes = 0;
306
+ if (!flags.has("no-ai-bundle")) {
307
+ const banner = (title) =>
308
+ `\n${"=".repeat(80)}\n${title}\n${"=".repeat(80)}\n\n`;
309
+ const bundle = [
310
+ banner("LAUNCHFRAME AI PAGE BUNDLE"),
311
+ `Source URL: ${url}\n`,
312
+ `Generated: ${new Date().toISOString()}\n`,
313
+ `Viewport: ${vw}x${vh}\n`,
314
+ `Wait until: ${waitUntil}\n\n`,
315
+ "Single plain-text file: POST-JS HTML plus all captured CSS (inline + network).\n",
316
+ "Use document.html / *.css separately if you hit tool context limits.\n",
317
+ banner("PART 1 — HTML (document.documentElement.outerHTML)"),
318
+ fullHtml,
319
+ banner("PART 2 — CSS (inline <style> tags, then network stylesheets)"),
320
+ aggregatedCss,
321
+ banner("END OF BUNDLE"),
322
+ ].join("");
323
+ await writeFile(join(outDir, "ai-page-bundle.txt"), bundle, "utf8");
324
+ aiBundleBytes = Buffer.byteLength(bundle, "utf8");
325
+ if (bundle.length > 12_000_000) {
326
+ console.warn(
327
+ `warning: ai-page-bundle.txt is large (~${Math.round(bundle.length / 1e6)}M chars); consider --no-ai-bundle and attach document.html + motion-summary only`,
328
+ );
329
+ }
330
+ }
331
+
332
+ const keyframes = extractKeyframeBlocks(aggregatedCss);
333
+ const motionLines = extractMotionLines(aggregatedCss);
334
+
335
+ /** @type {Record<string, unknown>} */
336
+ const motionSummary = {
337
+ sourceUrl: url,
338
+ generatedAt: new Date().toISOString(),
339
+ viewport: { width: vw, height: vh },
340
+ counts: {
341
+ keyframeRules: keyframes.length,
342
+ motionRelatedLines: motionLines.length,
343
+ inlineStyleTags: inlineStyles.length,
344
+ networkStylesheets: cssBodies.length,
345
+ scriptElements: scriptInventory.length,
346
+ },
347
+ keyframes: keyframes.map((k) => ({
348
+ name: k.name,
349
+ blockChars: k.block.length,
350
+ block:
351
+ k.block.length > 12000
352
+ ? `${k.block.slice(0, 12000)}\n/* …truncated… */\n`
353
+ : k.block,
354
+ })),
355
+ motionLinesSample: motionLines.slice(0, 800),
356
+ motionLinesTotal: motionLines.length,
357
+ };
358
+
359
+ await writeFile(
360
+ join(outDir, "motion-summary.json"),
361
+ JSON.stringify(motionSummary, null, 2),
362
+ "utf8",
363
+ );
364
+
365
+ /** @type {Record<string, unknown>} */
366
+ const meta = {
367
+ sourceUrl: url,
368
+ generatedAt: new Date().toISOString(),
369
+ viewport: { width: vw, height: vh },
370
+ waitUntil,
371
+ options: {
372
+ scrollFull: flags.has("scroll-full"),
373
+ wroteCssFiles: !flags.has("no-css-files"),
374
+ wroteAiBundle: !flags.has("no-ai-bundle"),
375
+ },
376
+ outputs: {
377
+ documentHtmlBytes: Buffer.byteLength(fullHtml, "utf8"),
378
+ bodyOuterHtmlBytes: bodyOuter ? Buffer.byteLength(bodyOuter, "utf8") : 0,
379
+ ...(aiBundleBytes ? { aiPageBundleBytes: aiBundleBytes } : {}),
380
+ networkStylesheets: cssBodies.map((c) => ({ url: c.url, bytes: c.bytes })),
381
+ },
382
+ scripts: scriptInventory,
383
+ };
384
+
385
+ await writeFile(join(outDir, "capture-meta.json"), JSON.stringify(meta, null, 2), "utf8");
386
+
387
+ await writeFile(
388
+ join(outDir, "README.txt"),
389
+ [
390
+ "LaunchFrame page inspection capture",
391
+ "====================================",
392
+ `Source: ${url}`,
393
+ "",
394
+ "Files:",
395
+ " ai-page-bundle.txt — **ALL-IN-ONE for LLMs:** HTML + inline/network CSS as plain text",
396
+ " document.html — Full <html> outerHTML after JS (same HTML as Part 1 of bundle)",
397
+ " body-outer.html — <body> outerHTML only (smaller)",
398
+ " inline-styles.json — All inline <style> tag contents",
399
+ " motion-summary.json— @keyframes + animation/transition-related lines",
400
+ " capture-meta.json — Viewport, stylesheet URLs, <script> inventory",
401
+ " *.css — Stylesheets observed on the network (unless --no-css-files)",
402
+ "",
403
+ "Notes:",
404
+ " • Some sites lazy-load CSS; use --scroll-full to force more requests.",
405
+ " • Cross-origin styles you cannot access via JS are still captured here if the browser downloaded them.",
406
+ " • Motion inside bundled JS (not CSS) is not fully extracted—check script src in capture-meta.json.",
407
+ "",
408
+ ].join("\n"),
409
+ "utf8",
410
+ );
411
+
412
+ console.log(outDir);
413
+ } finally {
414
+ await browser.close();
415
+ }
416
+ }
417
+
418
+ await runCapture();