diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2381a76..88c008e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,7 +6,7 @@ Get it running first, so you know the environment is sound: ```bash npm install -npm test # 199 of them, ~1.5s, no network +npm test # 250 of them, ~1.5s, no network npm run dev # demo mode; no keys needed ``` @@ -59,6 +59,14 @@ Fetch before you write. Every url, endpoint and command in that table was checke `role` carries more weight than `blocks`. None of the five sources ships a marketing page section, so a source improves a block loom already renders rather than replacing one, and a `summary` written like a block catalogue sends an agent looking for a hero that does not exist. Check the licence in the same pass: one of the five forbids redistributing a substantial part of its collection, which is fine for a site built with it and not fine for anything vendored here. +## Adding an MCP tool + +`lib/mcp.ts` only. `scripts/mcp.mjs` is the stdio pump and should not need touching. A tool is one entry in `TOOLS`: a name, a title, a description written for a model deciding whether to call it, a JSON Schema for its arguments, and a `run` returning `ok(payload)` or `badArgument(message)`. + +Read the answer out of `lib/plan.ts`, `lib/themes.ts`, `lib/catalog.ts` or `lib/sources.ts` when the call arrives. A tool holding its own copy of the block list goes on answering confidently after the list changes, and the agent on the other end has no way to notice. A test calls every tool the server advertises, so a name added to the list without a handler fails there rather than in front of an agent. + +Say what you did not find. An argument the tool does not recognise gets an empty result and an explanation, never a guess, and a fallback is labelled as one — `themeVars()` answers an unknown theme with `forest`, and an agent handed those values unlabelled pastes a green button in believing it asked for `terminal`. The JSON-RPC framing is written out in the same file and stays that way: no dependency was added for any of this, and none should be. + ## On what to ask a model This is the one place the project has an opinion. Please follow it when changing things: diff --git a/README.md b/README.md index 1d9c704..ce55327 100644 --- a/README.md +++ b/README.md @@ -177,7 +177,7 @@ The editor UI is a separate matter: eight languages, picked from `navigator.lang ## Export -Five formats, all derived from **the same rendered result**. There is no second copy of the layout code. +Six formats, all derived from **the same rendered result**. There is no second copy of the layout code. | Export | Size | For | |---|---|---| @@ -186,6 +186,7 @@ Five formats, all derived from **the same rendered result**. There is no second | `spec.json` | a few KB | Archive it, or feed another renderer | | `site.registry.json` | `Site.tsx`, wrapped | Install the page into a project that already uses shadcn/ui | | `AGENTS.md` | a page | Hand the page to a coding agent: tokens, blocks, and where components come from | +| `bundle.zip` | the other five | Hand the whole thing over at once, with a `CLAUDE.md` pointing at the brief | ### Site.tsx @@ -227,6 +228,14 @@ The item carries the same `Site.tsx` source; it is not a second rendering of the A short brief for the coding agent that picks the export up: `spec.json` is the authoritative description of the page, here are the 17 theme tokens with their values, here are the blocks this page actually has, and here are the component sources relevant to those blocks. It is written to be read once before work starts, not as documentation for a person. +### bundle.zip + +The other five in one archive, plus a `CLAUDE.md` whose entire content is `@AGENTS.md` — Claude Code looks for that filename and imports what the line points at, so the brief gets read instead of sitting unopened next to the markup. It is a pointer, not a second copy. + +Five buttons are five chances to drop a file, and the one that gets dropped is `AGENTS.md`, because it is the only one that does not look like the page. What the receiving agent has then is markup with no account of what may be edited, what the theme contract is, or where better components live. + +The zip is written by hand, STORE with no compression — the alternative was a dependency, and the payload is text that gets unzipped once on arrival. Two details are load-bearing. Sizes and CRCs are measured on UTF-8 bytes rather than string length, because the copy is routinely CJK and a length taken off the JavaScript string is a smaller number; the archive then opens in whichever tool ignores the field and fails everywhere else. And the timestamp is fixed rather than read from the clock, so the same page exported twice is byte-identical and a test can say so. + ## Components worth stealing The workflow is the lazy one — point a coding agent at a good library and let it pick, adapt and paste. `lib/sources.ts` is the table it reads: five libraries, each with its role, licence, install command, the machine-readable endpoints that answered when they were checked, the slots it can improve, and the caveat that bites. @@ -249,6 +258,32 @@ Every url and endpoint in that table was fetched and checked rather than recalle npm run sources # re-checks each endpoint and reports how many components it still lists ``` +## Reachable without a browser + +That table ranks the five by how much of each one a machine can reach unattended — three publish an `llms.txt`, one runs an MCP server, three install from a CLI. loom, which wrote the table, published none of it: everything it knew about its own blocks, themes and sources was reachable only by a person clicking in a tab. These are the surfaces it was grading others on. + +### MCP server + +```bash +claude mcp add loom -- npm run --silent mcp +``` + +Five read-only tools: the component source table, the sources for one named block, the 13 blocks with their variants and which archetypes require them, the six themes with the rubric Jev chooses from, and one theme's 17 tokens with their values. + +Every answer is read out of `lib/plan.ts`, `lib/themes.ts` and `lib/sources.ts` when the call arrives; nothing is restated in `lib/mcp.ts`. A tool carrying its own copy of the block list keeps answering confidently after the list changes, and the agent on the other end has no way to notice. + +The JSON-RPC framing is written out rather than taken from the MCP SDK, because **nothing here added a dependency**. A tools-only server owes a client `initialize`, `tools/list`, `tools/call`, and the discipline of returning nothing at all for `notifications/initialized` — a reply nobody is waiting for puts every later response against the wrong request. `scripts/mcp.mjs` is only the pump: it splits stdin on newlines and keeps the tail of a partial line, because a chunk boundary is not a message boundary and a real client sends the handshake fast enough for two messages to land together. + +Two of the answers are deliberately more than a lookup. A block name loom does not have comes back as an empty list *with an explanation*, never a guess, and a real block that no source covers says so — an empty list on its own reads like a failed lookup. An unknown theme name comes back with the fallback palette **labelled as the fallback**: `themeVars()` answers anything it does not recognise with `forest`, which is right for rendering and wrong to hand an agent unlabelled, because it would paste a green button in believing it had asked for `terminal`. + +### /llms.txt + +The same format three of the five catalogued sources publish, generated per request from `lib/plan.ts`, `lib/themes.ts` and `lib/sources.ts`, so it cannot drift from what the deployment is actually running. A test walks the real tables, so adding a block, a theme or a source without touching `lib/llms.ts` fails rather than ships a document that omits it. + +It follows llmstxt.org literally: one H1, a blockquote, prose that contains no heading, then H2 sections in which every line is a link item. That last rule is why the blocks, themes, tokens, exports and sources are prose rather than sections of their own — none of them has a URL, and inventing `/docs/blocks` to satisfy the shape would send an agent to a 404. Other people's endpoints appear as code spans, so every link an agent can follow is one this origin answers. + +The origin comes from the request rather than from a build-time constant, since the same build answers on localhost, a preview host and a custom domain, and a baked-in address is the error that only shows up in the one environment nobody tested. The two sections that are real links are `POST /api/generate` and `POST /api/edit`, with their request bodies and the shape of what comes back. + ## Themes are data
@@ -301,12 +336,12 @@ The audit needs a running server and `npx playwright install chromium`, so it st ```bash -npm test # 199 of them, ~1.5s, no network +npm test # 250 of them, ~1.5s, no network ``` They cover **the three rules taken back from the model** — selling-point count decides grid vs list, tier count decides single vs comparison, whether there is a UI screenshot decides the hero layout. If any of these drift, the decision quietly goes back to a model that cannot make it, so they are the ones that must not move. -Also covered: readiness gating (an empty array counts as missing, not ready), `asSettled` completion ordering, and the JSX serializer's sharp edges — custom properties need `as CSSProperties`, semicolons inside a gradient are not separators, text containing braces must be wrapped, and the `blk-in` animation class and `