diff --git a/layers/registry/server/data/skill-demos.json b/layers/registry/server/data/skill-demos.json index cac6a97b..0787786d 100644 --- a/layers/registry/server/data/skill-demos.json +++ b/layers/registry/server/data/skill-demos.json @@ -14,16 +14,16 @@ "outputFile": "index.html", "shots": [ { - "file": "desktop-9954800c7c.jpg", + "file": "desktop-2a201b3766.jpg", "width": 1440, "height": 4102, "viewport": "desktop", "alt": "Desktop screenshot of the page the Agent built for: Build a single-file landing page for Tidepool, a tide-chart app for surfers. Inline all CSS and JavaScript. Save it as i" }, { - "file": "mobile-c67e258443.jpg", + "file": "mobile-da8e537494.jpg", "width": 390, - "height": 5091, + "height": 5065, "viewport": "mobile", "alt": "Phone screenshot of the page the Agent built for: Build a single-file landing page for Tidepool, a tide-chart app for surfers. Inline all CSS and JavaScript. Save it as i" } @@ -55,6 +55,68 @@ "authorName": "Cathryn Lavery", "sourceUrl": "https://github.com/cathrynlavery/diagram-design/blob/main/skills/diagram-design/SKILL.md" }, + { + "owner": "elayadesign", + "repo": "ai-design-skills", + "name": "landing-page-design", + "makes": "landing-page", + "authorName": "Elaya", + "sourceUrl": "https://github.com/elayadesign/ai-design-skills/blob/main/skills/landing-page-design/SKILL.md", + "prompt": "Build a landing page for Glossmere, a fictional invoicing app for freelance translators that bills per word and per language pair. Make your own assumptions about anything I left out. Save it as index.html, with all CSS and JavaScript inline.", + "agent": "Claude Code", + "agentVersion": "2.1.291", + "model": "claude-opus-5-5", + "skillCommit": "1c1e97cb9878e236552c772092dda7adcdddbcb2", + "recordedAt": "2026-10-06", + "outputFile": "index.html", + "shots": [ + { + "file": "desktop-b4753ac09d.jpg", + "width": 1440, + "height": 6000, + "viewport": "desktop", + "alt": "Desktop screenshot of the page the Agent built for: Build a landing page for Glossmere, a fictional invoicing app for freelance translators that bills per word and per lang" + }, + { + "file": "mobile-4d54ba1970.jpg", + "width": 390, + "height": 6000, + "viewport": "mobile", + "alt": "Phone screenshot of the page the Agent built for: Build a landing page for Glossmere, a fictional invoicing app for freelance translators that bills per word and per lang" + } + ] + }, + { + "owner": "emilkowalski", + "repo": "skills", + "name": "animate", + "makes": "ui-component", + "authorName": "Emil Kowalski", + "sourceUrl": "https://github.com/emilkowalski/skills/blob/main/skills/animate/SKILL.md", + "prompt": "Build hold.html: a film-roll card for Rollwick, a fictional film-photo journal app, with a hold-to-delete button. Holding fills the button over about two seconds; releasing early springs it back. Inline all CSS and JavaScript.", + "agent": "Claude Code", + "agentVersion": "2.1.291", + "model": "claude-opus-5-5", + "skillCommit": "e8a175de22ae1e49370fc144c1f3bb9aeedf988d", + "recordedAt": "2026-10-06", + "outputFile": "hold.html", + "shots": [ + { + "file": "desktop-92230f46d7.jpg", + "width": 1440, + "height": 900, + "viewport": "desktop", + "alt": "Desktop screenshot of the page the Agent built for: Build hold.html: a film-roll card for Rollwick, a fictional film-photo journal app, with a hold-to-delete button. Holdin" + }, + { + "file": "mobile-89cf3092a6.jpg", + "width": 390, + "height": 844, + "viewport": "mobile", + "alt": "Phone screenshot of the page the Agent built for: Build hold.html: a film-roll card for Rollwick, a fictional film-photo journal app, with a hold-to-delete button. Holdin" + } + ] + }, { "owner": "emilkowalski", "repo": "skills", @@ -86,6 +148,37 @@ "authorName": "Emil Kowalski", "sourceUrl": "https://github.com/emilkowalski/skills/blob/main/skills/emil-design-eng/SKILL.md" }, + { + "owner": "gamedev-skills", + "repo": "awesome-gamedev-agent-skills", + "name": "game-feel", + "makes": "game", + "authorName": "gamedev-skills", + "sourceUrl": "https://github.com/gamedev-skills/awesome-gamedev-agent-skills/blob/main/skills/disciplines/game-feel/SKILL.md", + "prompt": "Make a tiny arcade game called Hailbroom: sweep a broom left and right with the arrow keys or mouse to bat falling hailstones back into the sky. Make every hit feel punchy. Save it as game.html with all CSS and JavaScript inline.", + "agent": "Claude Code", + "agentVersion": "2.1.291", + "model": "claude-opus-5-5", + "skillCommit": "d4b0e35550c55ae70bdfcab4ef5a0e94610438a9", + "recordedAt": "2026-10-06", + "outputFile": "game.html", + "shots": [ + { + "file": "desktop-ebb9d5ee7c.jpg", + "width": 1440, + "height": 900, + "viewport": "desktop", + "alt": "Desktop screenshot of the page the Agent built for: Make a tiny arcade game called Hailbroom: sweep a broom left and right with the arrow keys or mouse to bat falling hails" + }, + { + "file": "mobile-986a78ae76.jpg", + "width": 390, + "height": 844, + "viewport": "mobile", + "alt": "Phone screenshot of the page the Agent built for: Make a tiny arcade game called Hailbroom: sweep a broom left and right with the arrow keys or mouse to bat falling hails" + } + ] + }, { "owner": "heygen-com", "repo": "hyperframes", @@ -234,6 +327,61 @@ } ], "pin": 2 + }, + { + "owner": "ntcoding", + "repo": "claude-skillz", + "name": "data-visualization", + "makes": "chart", + "authorName": "Nick Tune", + "sourceUrl": "https://github.com/NTCoding/claude-skillz/blob/main/data-visualization/SKILL.md", + "prompt": "Build a one-page dashboard for Wrenfold Bikes, a town bike share. This week's rides by station: Harbour 412, Station Road 356, Market 241, College 198. Monday to Sunday: 180, 195, 170, 210, 262, 102, 88. Save it as dashboard.html with all CSS and JavaScript inline.", + "agent": "Claude Code", + "agentVersion": "2.1.291", + "model": "claude-opus-5-5", + "skillCommit": "21c61010e6081ced1eac8e45e3f788b2c4358bcf", + "recordedAt": "2026-10-06", + "outputFile": "dashboard.html", + "shots": [ + { + "file": "desktop-b2aa8f1a8c.jpg", + "width": 1440, + "height": 905, + "viewport": "desktop", + "alt": "Desktop screenshot of the page the Agent built for: Build a one-page dashboard for Wrenfold Bikes, a town bike share. This week's rides by station: Harbour 412, Station Roa" + }, + { + "file": "mobile-2c917bf37f.jpg", + "width": 390, + "height": 1751, + "viewport": "mobile", + "alt": "Phone screenshot of the page the Agent built for: Build a one-page dashboard for Wrenfold Bikes, a town bike share. This week's rides by station: Harbour 412, Station Roa" + } + ] + }, + { + "owner": "plannotator", + "repo": "effective-html", + "name": "html", + "makes": "slides", + "authorName": null, + "sourceUrl": "https://github.com/plannotator/effective-html/blob/main/skills/html/SKILL.md", + "prompt": "Make a 7-slide pitch deck for Brothwheel, a neighborhood soup co-op that sells soup in returnable jars, where arrow keys or clicks advance the slides. Save it as deck.html, one file with all CSS and JavaScript inline.", + "agent": "Claude Code", + "agentVersion": "2.1.291", + "model": "claude-opus-5-5", + "skillCommit": "e1e71db7ab69f2fc674225805ab8e35814d98e53", + "recordedAt": "2026-10-06", + "outputFile": "deck.html", + "shots": [ + { + "file": "desktop-2f941e684e.jpg", + "width": 1440, + "height": 900, + "viewport": "desktop", + "alt": "Desktop screenshot of the page the Agent built for: Make a 7-slide pitch deck for Brothwheel, a neighborhood soup co-op that sells soup in returnable jars, where arrow keys" + } + ] } ] } diff --git a/layers/registry/server/demos/elayadesign/ai-design-skills/landing-page-design/index.html b/layers/registry/server/demos/elayadesign/ai-design-skills/landing-page-design/index.html new file mode 100644 index 00000000..d489a8b9 --- /dev/null +++ b/layers/registry/server/demos/elayadesign/ai-design-skills/landing-page-design/index.html @@ -0,0 +1,1445 @@ + + + + + +Glossmere · Invoicing for freelance translators + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ +
+
+

Invoicing for freelance translators

+

Bill every word
at the right rate,
in every language pair

+

Drop in the analysis log from your CAT tool. Glossmere applies your rate for that pair, discounts fuzzy matches by your grid, adds the right VAT, and drafts the invoice in your client's currency.

+ + + +
+ +

Used by 2,814 freelance translators in 46 countries

+
+
+ + + +

Reads analysis logs from Trados Studio, memoQ, and Phrase

+
+ +
+
+

Why Glossmere

+

A spreadsheet cannot read a CAT log

+

Most translators we interviewed rebuild each invoice by hand. They copy the match bands, apply the grid, look up the rate, and convert the currency. That takes about 20 minutes per client, every month.

+
+
+
+

Invoicing in a spreadsheet

+
    +
  • Copy up to 14 match bands from each log, cell by cell
  • +
  • Remember which client pays €0.13 and which pays €0.12
  • +
  • Resend invoices that an agency rejected for a missing PO number
  • +
  • Look up the exchange rate, then explain it when the client asks
  • +
+
+
+

Invoicing in Glossmere

+
    +
  • Drop the log in, and weighted words appear for each band
  • +
  • Your rate card picks the rate for each pair and client
  • +
  • Each client can require a PO number before you send
  • +
  • The exchange rate locks on the issue date and prints on the PDF
  • +
+
+
+
+ 3 h 12 min → 41 min +

Median time members spend on invoicing each month, before and after Glossmere. Survey of 612 members, March 2026.

+
+
+ +
+
+

Features

+

Built for the way translators bill

+

Per word, per pair, per client. Glossmere does the arithmetic, so each invoice matches the job you delivered.

+
+
+
+
+
+ +

A rate for every language pair

+

Set EN→DE at €0.13 and DE→EN at €0.11 once. Every line picks the right rate, and a client can keep its own rates when you agreed on different ones.

+
+
+
EN→DE€0.13 per word
+
DE→EN€0.11 per word
+
EN→SV€0.14 per word
+
FR→EN€0.12 per word
+
JA→EN¥14 per character
+
+
+
+
+ +

Your fuzzy match grid, applied

+

Glossmere reads Trados Studio, memoQ, and Phrase logs, then weights each match band by your own percentages.

+
+
No match100%
+
75 to 94%60%
+
95 to 99%40%
+
Repeats25%
+
+
+
+ +

Minimums and surcharges

+

Short jobs bill at your €35 minimum. Weekend or 24 hour delivery adds your surcharge, and the PDF shows the reason on the line.

+
+
+ +

VAT and currency, sorted

+

EU reverse charge with the right legal note, 31 currencies, and an exchange rate that locks on the issue date.

+
+
+ +

Paid 9.4 days sooner

+

Reminders go out 3 days before and 7 days after the due date, in your words and in your client's language.

+

Median across 1,906 members, April to June 2026.

+
+
+
+ +
+
+

You translate the words.
Glossmere counts them,
prices them by the pair,
and gets every one paid.

+
+
+ +
+
+

How it works

+

From CAT log to sent invoice in three steps

+

Setup takes about 6 minutes. After that, a typical invoice takes under 2.

+
+
    +
  1. +
    01
    +

    Set your rate card once

    +

    Add each language pair, your rate per word or character, and your fuzzy grid. Copy a grid to another client in one click.

    +
  2. +
  3. +
    02
    +

    Drop in the analysis log

    +

    Drag the export from Trados Studio, memoQ, or Phrase onto the client. Glossmere reads each match band and drafts the lines.

    +
  4. +
  5. +
    03
    +

    Check it and send

    +

    Add a PO number if the client needs one, then send. Clients pay by SEPA transfer or card, and you see the payment the same day.

    +
  6. +
+ +
+

Try the rate engine with your own numbers

+

Pick a language pair, enter the words from a recent job, and see what the line should bill.

+
+
+
+
+
+
+ + +
+
+
+
Billed at 100%
+ +

+
+
+
Billed at 60%
+ +

+
+
+
Billed at 25%
+ +

+
+
+ + +
+

+
+ +
+
+
+

Draft invoice

+

Lindqvist Juridik AB, demo

+
+ +
+ +
    + +
    + Total before VAT + €0.00 +
    +

    Lines under €35.00 bill at the minimum fee. Weights: new 100%, fuzzy 60%, repeats 25%.

    +

    +
    +
    + +
    + +
    +
    +

    From translators

    +

    Translators who stopped invoicing on Sunday nights

    +
    +
    +

    61,409

    invoices sent by members last quarter

    +

    9.4 days

    sooner paid, median across members

    +

    41 min

    spent on invoicing each month

    +
    +
    +
    +

    “I bill four agencies, and each one has its own fuzzy grid. Glossmere keeps all four, and my invoices stopped coming back with questions.”

    +
    Ilse Vandermeer
    DE→NL and EN→NL, legal, Utrecht
    +
    +
    +

    “Japanese source bills by the character and English by the word. Glossmere puts both on one invoice, with the yen rate printed where my clients look for it.”

    +
    Kenji Arai
    JA→EN, game localization, Osaka
    +
    +
    +

    “In my first month it found €412.60 in repetitions I had never billed across three agencies. That covers three years of the subscription.”

    +
    Noor Haddad
    AR→FR and EN→FR, medical, Lyon
    +
    +
    +
    + +
    +
    +

    Pricing

    +

    One plan with every feature, free for 30 days

    +
    +
    +
    +

    Glossmere

    30 days free
    +

    €13per month

    +

    Or €129 a year, which saves €27. Prices exclude VAT.

    +
      +
    • Unlimited invoices and clients
    • +
    • Rate cards for every language pair
    • +
    • A fuzzy match grid for each client
    • +
    • Trados Studio, memoQ, and Phrase import
    • +
    • 31 currencies with VAT handling
    • +
    • Payment reminders in your client's language
    • +
    + Start 30 days free +
    +
    +

    Trying it costs you nothing

    +
      +
    • No card to start

      You add a card only if you decide to stay.

    • +
    • Cancel in two clicks

      From settings, at any time. No email to support and no exit survey.

    • +
    • Refund on annual plans

      Email us within 30 days of paying, and we refund the full €129.

    • +
    • Your data leaves with you

      Export every client, rate card, and invoice as CSV and PDF.

    • +
    +
    +
    +
    + +
    +
    +
    +

    FAQ

    +

    Questions translators ask us

    +

    Something else? Email hello@glossmere.app. A translator on our team replies within one working day.

    +
    +
    +
    +

    +

    Trados Studio 2019 and later (XML analysis reports), memoQ (CSV and HTML analysis exports), and Phrase (CSV analysis exports). For any other tool, type the words for each match band by hand, or email us a sample log and we will add support.

    +
    +
    +

    +

    No. Glossmere reads only the analysis log, which holds file names and word counts for each match band. Your source and target texts stay on your computer.

    +
    +
    +

    +

    Yes. Bill Chinese, Japanese, and Korean source text by the character, German clients by the standard line of 55 characters, and review or MT post editing by the hour. One invoice can mix all of them.

    +
    +
    +

    +

    You set a percentage for each match band, for example 60% for 75 to 94% matches. Glossmere multiplies the words in each band by that percentage and bills the weighted total. Each client can have its own grid.

    +
    +
    +

    +

    Glossmere picks the treatment from your country and your client's: domestic VAT, EU reverse charge with the required note, or no VAT for clients outside the EU. You can override it on any invoice. Check your first invoice with your accountant.

    +
    +
    +

    +

    We email you 3 days before the trial ends. If you add a card, you continue at €13 a month or €129 a year. If you do not, your account becomes read only, and you can still export every invoice.

    +
    +
    +

    +

    Yes. Import clients from a CSV file, and set your next invoice number during setup. Glossmere continues the sequence your accountant already knows.

    +
    +
    +

    +

    You can invoice in 31 currencies. Clients pay by SEPA transfer, card, or PayPal. Card payments cost 1.4% plus €0.25 for European cards, and Glossmere adds no fee of its own.

    +
    +
    +
    +
    + +
    +
    +

    Price your next invoice word by word

    +

    Start your 30 days free. Bring one CAT log and see the weighted total before you decide.

    + +
    +
    +
    + + + + + + +
    + + + + + + diff --git a/layers/registry/server/demos/emilkowalski/skills/animate/hold.html b/layers/registry/server/demos/emilkowalski/skills/animate/hold.html new file mode 100644 index 00000000..14d2f551 --- /dev/null +++ b/layers/registry/server/demos/emilkowalski/skills/animate/hold.html @@ -0,0 +1,392 @@ + + + + + +Rollwick · Lisbon, late light + + + +
    +

    Rollwick Journal

    + +
    +
    +

    Roll 14 · 35mm

    +

    Lisbon, late light

    + +
    +
    Film stock
    Kodak Portra 400
    +
    Camera
    Olympus OM-1
    +
    Frames
    36
    +
    + + + +
    +

    Developed 2 Oct 2026

    + + Press and hold for 2 seconds to delete this roll. +
    +
    + +
    +

    You deleted “Lisbon, late light”.

    + +
    +
    + +

    +
    + + + + diff --git a/layers/registry/server/demos/gamedev-skills/awesome-gamedev-agent-skills/game-feel/game.html b/layers/registry/server/demos/gamedev-skills/awesome-gamedev-agent-skills/game-feel/game.html new file mode 100644 index 00000000..e1d5b5da --- /dev/null +++ b/layers/registry/server/demos/gamedev-skills/awesome-gamedev-agent-skills/game-feel/game.html @@ -0,0 +1,1488 @@ + + + + + +Hailbroom + + + +
    + + +
    + + + diff --git a/layers/registry/server/demos/ntcoding/claude-skillz/data-visualization/dashboard.html b/layers/registry/server/demos/ntcoding/claude-skillz/data-visualization/dashboard.html new file mode 100644 index 00000000..d733c27a --- /dev/null +++ b/layers/registry/server/demos/ntcoding/claude-skillz/data-visualization/dashboard.html @@ -0,0 +1,657 @@ + + + + + + +Wrenfold Bikes: weekly rides + + + +
    +
    +

    Wrenfold Bikes

    +

    Weekly rides

    +

    Rides this week, by station and by day, Monday to Sunday.

    +
    + + + + + +
    +
    +

    +

    Rides by station, this week

    +

    +
    +

    Hover or focus a bar for details. Use the arrow keys to move between bars.

    + +
    + +
    +

    +

    Rides per day, Monday to Sunday. The dashed line is the daily average.

    +

    +
    +

    Hover or focus a day for details. Use the arrow keys to move between days.

    + +
    +
    + +
    + Show the data as tables +
    +
    + + +
    + + + + diff --git a/layers/registry/server/demos/plannotator/effective-html/html/deck.html b/layers/registry/server/demos/plannotator/effective-html/html/deck.html new file mode 100644 index 00000000..34afc783 --- /dev/null +++ b/layers/registry/server/demos/plannotator/effective-html/html/deck.html @@ -0,0 +1,1079 @@ + + + + + + +Brothwheel · Pitch deck + + + + + + +
    +
    + + +
    +
    +

    Member share offering · Fall 2026

    +

    Brothwheel

    +

    Neighborhood soup in glass jars that come back.

    +

    A co-op owned by the cooks who make the soup and the neighbors who eat it.

    +

    Press → or click to continue. Press F for full screen.

    +
    + +
    + + +
    +
    +

    The problem

    +

    Good soup takes hours. Its takeout tub lasts one meal.

    +
    +
    +
    +

    52

    +
    single-use tubs a year, plus their lids, from one household that buys soup once a week.
    +
    +
      +
    • Time

      A proper stock simmers for four hours. Weeknights rarely have four hours.

    • +
    • Waste

      Takeout soup comes in a plastic tub that gets used once.

    • +
    • Idle kitchens

      Community kitchens sit empty most weekday afternoons.

    • +
    +
    +
    + + +
    +
    + +
    +
    +
    +

    How it works

    +

    One jar goes round and round.

    +
    +
      +
    1. Cook. Member cooks make three soups in a shared kitchen.

    2. +
    3. Fill. Liter jars, filled hot, labeled by batch and date.

    4. +
    5. Collect. At our counter, a corner shop shelf, or by cargo bike.

    6. +
    7. Eat. Best within four days, kept chilled.

    8. +
    9. Return. Any pickup point takes the jar back and refunds $3.

    10. +
    11. Wash. A commercial dishwasher sanitizes every jar and lid.

    12. +
    +

    A jar and lid cost us $2.40. The $3 deposit covers any jar that never comes back.

    +
    +
    + + +
    +
    +

    What members get

    +

    Three soups a week, one always vegan.

    +
    + +
    + + +
    +
    +
    +

    The numbers

    +

    Every liter pays its way.

    +
    +

    Where each $12 goes at our year-one target of 500 liters a week.

    +
    +
    Fixed costs
    $1,230 a week for kitchen hours, a part-time coordinator and insurance.
    +
    Break-even
    333 liters a week.
    +
    Surplus
    $620 a week at target. Half goes back to members as a dividend, half to the jar and kitchen reserve.
    +
    +
    +
    + + + + + + + + + + + + +
    Where $12 goes per liter at 500 liters a week
    CostPer liter
    Ingredients$3.40
    Cook wages$3.10
    Fixed costs$2.46
    Shelf partner share$1.20
    Jar washing and losses$0.60
    Surplus$1.24
    +
    +
    + + +
    +
    +

    Year one

    +

    Open small. Break even by month six.

    +
    +
    +

    Target liters sold a week

    + +
      +
    1. +

      Months 1 and 2

      +

      Set up

      +

      Lease Tuesday and Friday kitchen hours. Install the dishwasher. Buy 1,500 jars.

      +
    2. +
    3. +

      Month 3

      +

      Open

      +

      Open the counter with 150 founding members.

      +

      Target: 200 liters a week

      +
    4. +
    5. +

      Months 4 to 6

      +

      Add shelves

      +

      Add four corner shop shelves for pickup and jar returns.

      +

      Target: 340 liters a week, past break-even at 333

      +
    6. +
    7. +

      Months 7 to 12

      +

      Deliver

      +

      Start Friday cargo bike delivery. Grow to 400 members.

      +

      Target: 500 liters a week

      +
    8. +
    +
    +
    + + +
    +
    +
    +

    The ask

    +

    Help us start the wheel.

    +
    +
    +

    $60,000

    +

    opens the kitchen and carries us past break-even.

    +
    +
    +
    Founding member shares, 150 × $100
    $15,000
    +
    Neighbor loans, 3% over five years
    $20,000
    +
    Community lender loan
    $25,000
    +
    +
    +
    +

    Where the $60,000 goes

    +
      +
    • Kitchen fit-out and tilting kettle$18,000
    • +
    • Working capital, 13 weeks$15,900
    • +
    • Commercial dishwasher$9,000
    • +
    • Fridges$7,000
    • +
    • Cargo bike and trailer$6,500
    • +
    • 1,500 jars and lids$3,600
    • +
    +

    Buy a share, lend to the kitchen, or sign up to cook.

    +
    +
    + + +
    +

    +
    + + + + diff --git a/scripts/record-skill-demo.ts b/scripts/record-skill-demo.ts index 6d95334e..affe00b3 100644 --- a/scripts/record-skill-demo.ts +++ b/scripts/record-skill-demo.ts @@ -8,6 +8,7 @@ * `--makes` is the demo's group on /skills/demos: one of DEMO_MAKES in * shared/demo-groups.ts. * pnpm demo:record owner/repo/skill --reshoot + * pnpm demo:record owner/repo/skill --makes slides --prompt "..." --output deck.html --resume /tmp/skilld-demo-XXXX --model claude-opus-5-5 * * 1. Reads the Skill's current source commit from the public API. * 2. Runs Claude Code headless in a fresh temp folder: it loads the Skill with @@ -27,7 +28,9 @@ * code hosts. Third-party Skill text never gets an open shell on this machine. * * `--reshoot` retakes the screenshots of a page demo from its kept output, - * without running the Agent again. + * without running the Agent again. `--resume` finishes a run whose Agent + * already wrote its output into a folder, when a later step failed; give the + * model that run used with `--model`, since its result was not kept. * * It writes nothing to production. Opening a pull request with the result is * the review step: a human approves each demo by merging it. @@ -36,6 +39,7 @@ import type { DemoMakes } from '../shared/demo-groups' import { execFile } from 'node:child_process' import { createHash } from 'node:crypto' +import { existsSync, readdirSync } from 'node:fs' import { copyFile, cp, mkdir, mkdtemp, open, readdir, readFile, rm, stat, writeFile } from 'node:fs/promises' import { homedir, tmpdir } from 'node:os' import { extname, join, relative, resolve, sep } from 'node:path' @@ -88,7 +92,19 @@ const SANDBOX_DOMAINS = [ ] /** Paths no Skill command may read, even inside the sandbox. */ -const SECRET_PATHS = ['~/.ssh', '~/.aws', '~/.config', '~/.gnupg', '~/.netrc', '~/.npmrc', '~/.docker', '~/.kube', '~/.local/share', '~/sites', '~/pkg'] +const SECRET_PATHS = ['~/.ssh', '~/.aws', '~/.config', '~/.gnupg', '~/.netrc', '~/.npmrc', '~/.docker', '~/.kube', '~/sites', '~/pkg'] + +/** The folders in ~/.local/share a run needs: Node and npx live under pnpm, and renderers read fonts. */ +const SHARED_DATA_READABLE = new Set(['pnpm', 'fonts']) + +/** Every secret path, plus each other folder in ~/.local/share, where tools keep their tokens. */ +function deniedPaths(): string[] { + const shared = join(homedir(), '.local/share') + const sharedData = existsSync(shared) + ? readdirSync(shared).filter(entry => !SHARED_DATA_READABLE.has(entry)).map(entry => `~/.local/share/${entry}`) + : [] + return [...SECRET_PATHS, ...sharedData] +} interface Shot { file: string @@ -129,7 +145,7 @@ interface DemoEntry { type OutputKind = 'page' | 'video' type Parsed - = | { _tag: 'record', owner: string, repo: string, name: string, makes: DemoMakes, prompt: string, output: string, kind: OutputKind, seed: string | null, setup: string | null } + = | { _tag: 'record', owner: string, repo: string, name: string, makes: DemoMakes, prompt: string, output: string, kind: OutputKind, seed: string | null, setup: string | null, resume: { dir: string, model: string } | null } | { _tag: 'reshoot', owner: string, repo: string, name: string } | { _tag: 'usage', message: string } @@ -142,6 +158,8 @@ function parseInput(argv: string[]): Parsed { makes: { type: 'string' }, output: { type: 'string', default: 'index.html' }, seed: { type: 'string' }, + resume: { type: 'string' }, + model: { type: 'string' }, setup: { type: 'string' }, reshoot: { type: 'boolean', default: false }, }, @@ -163,7 +181,10 @@ function parseInput(argv: string[]): Parsed { return { _tag: 'usage', message: 'The --output file must be one HTML or MP4 file name, such as index.html or film.mp4.' } if (values.seed && !values.setup) return { _tag: 'usage', message: 'A --seed folder needs a --setup line that tells visitors what the folder held.' } - return { _tag: 'record', owner, repo, name, makes, prompt: values.prompt, output: values.output, kind, seed: values.seed ? resolve(values.seed) : null, setup: values.setup ?? null } + if (values.resume && !values.model) + return { _tag: 'usage', message: 'A --resume folder needs --model: the model the interrupted run used.' } + const resumed = values.resume && values.model ? { dir: resolve(values.resume), model: values.model } : null + return { _tag: 'record', owner, repo, name, makes, prompt: values.prompt, output: values.output, kind, seed: values.seed ? resolve(values.seed) : null, setup: values.setup ?? null, resume: resumed } } interface SkillSource { @@ -188,32 +209,63 @@ async function skillSource(owner: string, repo: string, name: string): Promise path.replace(/^~/, home) +/** + * The sandbox every recorded run works in. A film Skill may run any command in + * it; a page Skill only its allowed one, since nothing else is auto-approved. + */ +function sandboxSettings(kind: OutputKind): string { + const denied = deniedPaths() return JSON.stringify({ sandbox: { enabled: true, - autoAllowBashIfSandboxed: true, + autoAllowBashIfSandboxed: kind === 'video', allowUnsandboxedCommands: false, filesystem: { // The temp folder is the working directory; renderers cache browsers here too. allowWrite: ['.', '~/.npm', '~/.cache/puppeteer', '~/.cache/ms-playwright'], - denyRead: SECRET_PATHS, + denyRead: denied, }, network: { allowedDomains: SANDBOX_DOMAINS }, }, + // A rule path starting `~/` is in the home folder; a bare `/home/...` path would be read as relative. permissions: { - deny: SECRET_PATHS.flatMap(path => [`Read(${expand(path)}/**)`, `Edit(${expand(path)}/**)`]), + deny: denied.flatMap(path => [`Read(${path}/**)`, `Edit(${path}/**)`]), }, }) } -async function record(cwd: string, skillRef: string, prompt: string, kind: OutputKind): Promise<{ model: string, version: string }> { - const instruction = `First run \`npx skilld run ${skillRef}\` and follow the Skill it prints. Then do this task in the current folder:\n\n${prompt}` - const access = kind === 'video' - ? ['--settings', sandboxSettings(), '--allowedTools', 'Bash,Read,Write,Edit'] - : ['--allowedTools', `Bash(npx skilld run ${skillRef}),Read,Write,Edit`] +/** + * The exact ref `skilld run` loads right now, pinned to its commit. The Agent + * runs this pinned ref, so the demo records the commit that actually ran; the + * registry's own commit can lag the Repository. A Skill that does not run + * fails here, before any Agent time is spent. + */ +async function pinnedRun(skillRef: string): Promise<{ ref: string, commit: string }> { + const dir = await mkdtemp(join(tmpdir(), 'skilld-pin-')) + const { stdout, stderr } = await run('npx', ['-y', 'skilld', 'run', skillRef], { cwd: dir, timeout: 120_000, maxBuffer: 32 * 1024 * 1024 }) + const match = /skilld install '([^']+#commit:([0-9a-f]{40}))'/.exec(`${stdout}\n${stderr}`) + if (!match?.[1] || !match[2]) + throw new Error(`\`skilld run ${skillRef}\` printed no pinned commit, so the demo could not record what ran.`) + return { ref: match[1], commit: match[2] } +} + +async function record(cwd: string, pinnedRef: string, prompt: string, kind: OutputKind): Promise<{ model: string, version: string }> { + const instruction = `First run \`npx skilld run '${pinnedRef}'\` and follow the Skill it prints. Then do this task in the current folder:\n\n${prompt}` + // A page Skill gets no shell beyond loading Skills; a film Skill gets a shell. Both run in the sandbox. + // No user or project settings: they would load this machine's CLAUDE.md, Skills and allow rules, and a + // global allow rule then reaches past --allowedTools, which only pre-approves. Only these four tools exist. + const allowed = kind === 'video' ? 'Bash,Read,Write,Edit' : 'Bash(npx skilld run:*),Read,Write,Edit' + const access = [ + '--setting-sources', + '', + '--strict-mcp-config', + '--tools', + 'Bash,Read,Write,Edit', + '--settings', + sandboxSettings(kind), + '--allowedTools', + allowed, + ] const { stdout } = await run('claude', [ '-p', instruction, @@ -235,7 +287,8 @@ async function screenshot(page: string, dir: string, prompt: string): Promise { for (let y = 0; y < document.documentElement.scrollHeight; y += window.innerHeight) { - window.scrollTo(0, y) + window.scrollTo({ top: y, behavior: 'instant' }) await new Promise(done => setTimeout(done, 150)) } - window.scrollTo(0, 0) + window.scrollTo({ top: 0, behavior: 'instant' }) }) // Let entrance animations settle before the picture. await tab.waitForTimeout(1500) @@ -442,12 +496,16 @@ async function main(): Promise { const { owner, repo, name, makes, prompt, output, kind, seed, setup } = input const skillRef = `${owner}/${repo}/${name}` const source = await skillSource(owner, repo, name) + const pinned = await pinnedRun(skillRef) - const cwd = await mkdtemp(join(tmpdir(), 'skilld-demo-')) - if (seed) + const resumed = input.resume + const cwd = resumed ? resumed.dir : await mkdtemp(join(tmpdir(), 'skilld-demo-')) + if (seed && !resumed) await cp(seed, cwd, { recursive: true }) - console.log(`Recording ${skillRef} at ${source.commit.slice(0, 7)} in ${cwd}`) - const agent = await record(cwd, skillRef, prompt, kind) + console.log(`Recording ${skillRef} at ${pinned.commit.slice(0, 7)} in ${cwd}`) + const agent = resumed + ? { model: resumed.model, version: (await run('claude', ['--version'])).stdout.trim().split(' ')[0] ?? 'unknown' } + : await record(cwd, pinned.ref, prompt, kind) const produced = await findOutput(cwd, output) if (!produced) @@ -476,7 +534,7 @@ async function main(): Promise { agent: 'Claude Code', agentVersion: agent.version, model: agent.model, - skillCommit: source.commit, + skillCommit: pinned.commit, recordedAt: new Date().toISOString().slice(0, 10), ...media, }) diff --git a/shared/demo-groups.ts b/shared/demo-groups.ts index 92c666cf..4a53fe03 100644 --- a/shared/demo-groups.ts +++ b/shared/demo-groups.ts @@ -26,6 +26,21 @@ export const DEMO_GROUPS = [ label: 'Diagrams and explainers', line: 'Systems and flows drawn so a reader can follow them.', }, + { + makes: 'slides', + label: 'Slides and decks', + line: 'A talk or a pitch as a deck you can click through.', + }, + { + makes: 'chart', + label: 'Charts and dashboards', + line: 'Data drawn so the point lands at a glance.', + }, + { + makes: 'game', + label: 'Games and playables', + line: 'Small games you can play right here.', + }, ] as const export type DemoMakes = typeof DEMO_GROUPS[number]['makes']