diff --git a/AGENTS.md b/AGENTS.md index 0cef486..00f4762 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -30,6 +30,9 @@ tests the previous build. `VITE_ISKETCH_API` names a server. - Per shape behaviour lives in `src/domain/nodeMeta.js`, and each outline in `src/domain/shapes.js`. Add an entry there rather than branching on type in a component. +- `plugin/server/isketch-mcp.mjs` is a bundle of `src/mcp/server.js` and its `src/domain/` + dependencies, for the Claude Code plugin. After changing either, run `npm run plugin` and + commit the rebuilt bundle; CI fails if it is stale. - State has three owners and no copies: TanStack Query owns the document, the route owns which node is open, Pinia owns viewport, history, theme and toasts. Do not mirror one in another. diff --git a/README.md b/README.md index 591968d..f99657b 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,8 @@ Claude, Copilot or any coding agent reads without guessing, and can edit back. [](https://github.com/raj-khan/flow/actions/workflows/ci.yml) [](LICENSE) + + https://github.com/user-attachments/assets/cbfbc844-7373-4891-a985-50e2870fe1b5 diff --git a/backlog/tasks/fl-91 - Launch-kit.md b/backlog/tasks/fl-91 - Launch-kit.md index 3a32d50..e5bb737 100644 --- a/backlog/tasks/fl-91 - Launch-kit.md +++ b/backlog/tasks/fl-91 - Launch-kit.md @@ -1,10 +1,11 @@ --- id: FL-91 title: Launch kit -status: To Do +status: In Progress assignee: - '@raj-khan' created_date: '2026-09-25 17:39' +updated_date: '2026-09-27 14:52' labels: - launch milestone: m-4 @@ -31,6 +32,24 @@ Mostly the owner's to do, listed so it is not forgotten. - [ ] #1 A 60 second demo video (made with idemo.video) and a README hero GIF of the loop - [ ] #2 Posts: Show HN, Product Hunt, r/ClaudeAI, r/ChatGPTCoding, dev.to, X - [ ] #3 Listings: awesome-mcp-servers, awesome-claude-code, AlternativeTo against Excalidraw, draw.io and Eraser -- [ ] #4 An optional, removable Made with isketch mark on exported PNGs and hosted pages +- [x] #4 An optional, removable Made with isketch mark on exported PNGs and hosted pages + +## Implementation Plan + + + +1. What lives in the repo, done here: a README hero GIF of the loop (recorded with the clip, encoded in the browser with gifenc, since there is no ffmpeg), the landing clip re-recorded with the current UI, and an optional Made with isketch mark on exported pictures (on by default, one checkbox to take it off, remembered) and hosted pages. +2. What is the owners: the 60 second demo video (idemo.video), the posts and the listings; drafted ready to post in docs/launch.md, with the registry and marketplace steps in docs/listings.md. + + + +## Implementation Notes + + + +Done in the repo: docs/loop.gif (92 frames, 3.1 MB, 720px) from npm run clip, which also re-records public/demo.webm; renderSvg credit option; Export checkbox; hosted page footer credit. Verified: renderSvg spec, export e2e (mark on by default, off when unticked and remembered), server test for the page footer; vitest 312, e2e 146, server 16. + +Left for the owner: AC #1 needs the idemo.video demo (the GIF half is done); AC #2 posts and AC #3 listings are drafted in docs/launch.md and docs/listings.md and need the owners accounts, after isketch.online is live. + diff --git a/docs/launch.md b/docs/launch.md new file mode 100644 index 0000000..dcc11ec --- /dev/null +++ b/docs/launch.md @@ -0,0 +1,90 @@ +# Launch kit + +Drafts to post, in the owner's voice. Each points at the README, whose hero GIF shows the loop. +Post after isketch.online serves the app and the hosted server, so every link works, and after the +60 second demo video (made with idemo.video) is uploaded to the README. + +## Show HN + +**Title:** Show HN: isketch, a sketchpad your coding agent reads exactly + +**Text:** + +I kept sketching architectures in Excalidraw, screenshotting them, and pasting the picture into +Claude. The agent then guessed at boxes and arrows from pixels: misread names, lost arrow +directions, and nothing it wrote back could go on the diagram. + +isketch is a sketchpad where every sketch is also plain text, a `.flow` file with ids, kinds, +directions and notes. Copy for AI puts a Markdown brief on the clipboard; an MCP server lets +Claude Code or any agent list, read, validate, render and edit the diagrams in your repo; and when +the agent writes the file, the open canvas shows the change as it happens. + +It imports draw.io, Excalidraw, Mermaid, docker-compose, OpenAPI, SQL, Prisma and Drizzle, +`isketch scan` drafts a diagram of a whole repository, and pull requests get a visual diff. It works +offline, needs no account, and is MIT licensed. + +https://github.com/raj-khan/flow + +## Product Hunt + +- **Tagline:** Sketch it, hand it to your agent +- **Description:** A sketchpad whose output is agent-ready: every diagram is also text that Claude, + Copilot or any coding agent reads exactly and edits back. Hand-drawn look, draw.io and + Excalidraw import, an MCP server, and a Claude Code plugin. +- **First comment:** why screenshots fail agents (names misread, directions lost, nothing to write + back), the loop in one line, and one ask: which import should come next. + +## r/ClaudeAI + +**Title:** I stopped pasting diagram screenshots into Claude. Now it reads and edits the diagram itself. + +**Body:** the loop (sketch, Copy for AI or the MCP server, Claude builds, Claude updates the +`.flow` file, the canvas shows it live), the plugin install lines, and a short clip. Ask what +people would want Claude to do with a diagram next. + +```text +/plugin marketplace add raj-khan/flow +/plugin install isketch@isketch +``` + +## r/ChatGPTCoding + +**Title:** Diagrams your coding agent can read: text, not pixels + +**Body:** the same loop, told for any agent: Draft with your agent (describe, paste the answer), +Copy for AI, and the `.flow` file an agent edits in the repo. Lead with the before and after of a +screenshot against a brief. + +## dev.to + +**Title:** Stop screenshotting your Excalidraw for Claude + +**Outline:** + +1. The screenshot habit, and three ways it fails an agent. +2. What an agent needs instead: ids, kinds, directions, notes, as text. +3. The `.flow` format in ten lines. +4. The loop in practice: sketch, brief, build, the agent keeps the diagram true. +5. Bringing existing diagrams along: draw.io, Excalidraw, Mermaid, schemas, `isketch scan`. +6. Where it goes next. + +## X + +1. Pasting a diagram screenshot into Claude makes it guess. isketch makes every sketch text it + reads exactly. (GIF) +2. Copy for AI: a Markdown brief with every shape, what it means, and every connection in words. +3. Or skip the clipboard: the MCP server lets Claude Code read, write and render your diagrams, + and the open canvas shows its edits live. +4. Bring what you have: draw.io, Excalidraw, Mermaid, compose, OpenAPI, SQL, Prisma, Drizzle. +5. MIT, offline, no account: github.com/raj-khan/flow + +## Listings + +Ready to paste; the MCP registry and marketplaces are in [listings.md](listings.md). + +- **awesome-mcp-servers:** `- [raj-khan/flow](https://github.com/raj-khan/flow) - isketch: read, +write, validate, render and diff .flow architecture diagrams an agent reads exactly.` +- **awesome-claude-code:** under plugins, `isketch: the .flow diagram format as a skill, with an MCP +server to read, write and render diagrams in your repo.` +- **AlternativeTo:** list isketch as an alternative to Excalidraw, draw.io and Eraser, with the + tagline and the GIF; tags: diagrams, whiteboard, AI, developer tools. diff --git a/docs/loop.gif b/docs/loop.gif new file mode 100644 index 0000000..d17aeaa Binary files /dev/null and b/docs/loop.gif differ diff --git a/e2e/export.spec.js b/e2e/export.spec.js index 5dfa19c..66758f9 100644 --- a/e2e/export.spec.js +++ b/e2e/export.spec.js @@ -42,3 +42,16 @@ test('downloads a sketch as SVG, carrying its handwriting font', async ({ page } expect(svg).toMatch(/^" ].filter(Boolean).join("\n")}\n`; } diff --git a/public/demo.webm b/public/demo.webm index f118596..f194d80 100644 Binary files a/public/demo.webm and b/public/demo.webm differ diff --git a/scripts/make-demo-clip.mjs b/scripts/make-demo-clip.mjs index c8225e1..612b54d 100644 --- a/scripts/make-demo-clip.mjs +++ b/scripts/make-demo-clip.mjs @@ -2,10 +2,12 @@ * Records the landing clip: the real app sketching, Copy for AI, then an * agent's answer building from the brief. One take, scripted, against the * production build on a local server; the webm is committed as - * public/demo.webm. Run with `npm run clip`. + * public/demo.webm. The same take, as screenshots, becomes docs/loop.gif for + * the README, encoded in the browser with gifenc. Run with `npm run clip`. */ import { spawn } from 'node:child_process' -import { cp, mkdtemp, readdir, rm, readFile } from 'node:fs/promises' +import { cp, mkdtemp, readdir, rm, readFile, writeFile } from 'node:fs/promises' +import { createRequire } from 'node:module' import { tmpdir } from 'node:os' import { join } from 'node:path' @@ -33,6 +35,18 @@ try { // 1 · Sketch: the starter diagram, a shape moved, a title renamed in place. await page.goto(`http://localhost:${PORT}/flow`) await nodeAt(page, 'b6a0c1').waitFor({ state: 'visible' }) + // The README's GIF: a screenshot every few frames, with when it was taken. + /** @type {{ at: number, jpeg: string }[]} */ + const frames = [] + let filming = true + const film = (async () => { + while (filming) { + const jpeg = await page.screenshot({ type: 'jpeg', quality: 85 }).catch(() => null) + if (jpeg) frames.push({ at: Date.now(), jpeg: jpeg.toString('base64') }) + await pause(90) + } + })() + await pause(1200) const card = nodeAt(page, 'b6a0c1') @@ -47,6 +61,8 @@ try { await pause(400) await page.keyboard.type('Support bot', { delay: 45 }) await page.keyboard.press('Enter') + // The click that began the rename opened the details too; the take is about the canvas. + await page.keyboard.press('Escape') await pause(1000) // 2 · Hand it over: the brief goes onto the clipboard. @@ -60,6 +76,9 @@ try { await page.locator('.clip-code').waitFor({ state: 'visible' }) await pause(2800) + filming = false + await film + await writeGif(frames) await context.close() } finally { await browser.close() @@ -75,6 +94,54 @@ if (webm) { } await rm(take, { recursive: true, force: true }) +/** + * The frames as a looping GIF, 720px wide: decoded and quantised in the + * browser (Node has no image decoder here), each shown for as long as it was + * on screen. + * @param {{ at: number, jpeg: string }[]} frames + */ +async function writeGif(frames) { + const encoder = await readFile( + createRequire(import.meta.url).resolve('gifenc/dist/gifenc.esm.js'), + 'utf8', + ) + const page = await browser.newPage() + await page.goto(`http://localhost:${PORT}/docs/format/`) + const bytes = await page.evaluate( + async ({ code, frames }) => { + const url = URL.createObjectURL(new Blob([code], { type: 'text/javascript' })) + const { GIFEncoder, quantize, applyPalette } = await import(url) + const width = 720 + const height = 405 + const canvas = new globalThis.OffscreenCanvas(width, height) + const context = canvas.getContext('2d', { willReadFrequently: true }) + const gif = GIFEncoder() + for (let index = 0; index < frames.length; index++) { + const image = await globalThis.createImageBitmap( + await (await fetch(`data:image/jpeg;base64,${frames[index].jpeg}`)).blob(), + ) + context.drawImage(image, 0, 0, width, height) + const { data } = context.getImageData(0, 0, width, height) + const palette = quantize(data, 256) + const next = frames[index + 1]?.at ?? frames[index].at + 1500 + gif.writeFrame(applyPalette(data, palette), width, height, { + palette, + delay: Math.max(20, next - frames[index].at), + }) + } + gif.finish() + return Array.from(gif.bytes()) + }, + { code: encoder, frames }, + ) + await page.close() + const gif = new URL('../docs/loop.gif', import.meta.url).pathname + await writeFile(gif, Buffer.from(bytes)) + console.log( + `docs/loop.gif (${frames.length} frames, ${(bytes.length / 1024 / 1024).toFixed(1)} MB)`, + ) +} + /** @param {string} url */ async function waitUntil(url) { for (let attempt = 0; attempt < 60; attempt++) { @@ -127,14 +194,17 @@ function addChat() { chat.innerHTML = `
**Support bot** → **Escalation** … +**Business Hours** → **Support bot**: Failure +**Support bot** → **Add Comment #1** … -The brief itself stays on your clipboard — -in the real thing you paste all of it here.+(In the real thing, the whole brief is pasted here.)
const escalation = await page.getByRole('button', { name: 'Away' })
-// support-bot/escalation.ts — from the sketch, not a guess
+ // support-bot.ts: named from the sketch, not guessed
+export async function handle(conversation) {
+ if (!isBusinessHours()) return supportBot(conversation)
+ return welcomeMessage(conversation)
+}
`
document.body.append(chat)
setTimeout(() => chat.classList.add('typing'), 2400)
diff --git a/server/src/diagrams/page.ts b/server/src/diagrams/page.ts
index e0c8967..508a199 100644
--- a/server/src/diagrams/page.ts
+++ b/server/src/diagrams/page.ts
@@ -69,7 +69,7 @@ export function renderPage(input: {
For people and AI agents alike: every shape, what it is for, every connection, and the source.
${escape(brief)}
-
+