wren/glyph Public Public: anyone can read this; nobody can change it.

forked from apps/glyph

workbenchthis machine
Files 308f4625e0ac59206bd37f1f78c4ecba8ca65968
164 lines · 9.4 KB · ~2.4k tok Raw History
1
# glyph
2
3
Type a prompt, get SVG logos and icons drawn by Claude; preview in your theme, copy or download.
4
5
Describe something — "a hex chip with a circuit b", "a lighthouse for a
6
monitoring app" — pick a kind (logo mark, app icon, badge, spot
7
illustration), and Claude draws four different takes as SVG in about a
8
minute. Switch palette (Sage, Moss, Neon, Mono, Blueprint) to see them in
9
any theme, then **Copy** the SVG or **Save** it as a file in that palette.
10
11
## What is real
12
13
- **The drawing is real**: the app runs Claude Code (`claude -p`, no tools,
14
  model `claude-opus-5-5`) with the token in `CLAUDE_CODE_OAUTH_TOKEN`. Set it
15
  in Settings → App → Environment (make one with `claude setup-token`).
16
  Without it a drawing fails at once with a sentence saying exactly that.
17
- **Every SVG is rebuilt from an allowlist** (`src/glyph.ts` `sanitize`):
18
  shapes, local gradients and clip paths, `title` and plain attributes
19
  only. No script, style, link, image, `url(…)` other than one pointing at
20
  a gradient or clip inside the same SVG (`url(#id)`), or event handler
21
  survives, and previews are `<img>` data URIs, so
22
  nothing in a drawing can run on the page. A saved file is served with a
23
  sandboxing CSP.
24
- **Colours**: Claude draws in three placeholder colours (ink, glow and
25
  paper, `design/palettes.json`) and builds depth from gradients and
26
  opacity between them; a palette swaps ink and glow for its own and paper
27
  for its ground. Downloads carry the palette's real colours. Drawings are
28
  asked for on a 256-unit canvas with layered, detailed shapes, so a
29
  drawing takes a little longer than a flat one.
30
- **Drawings are private to whoever asked** (`contract.json`); an admin sees
31
  everyone's. One drawing at a time per person, three at once for everyone.
32
  A drawing left half done by a restart is marked failed with a reason.
33
- Also an API (`/api/drawings`, `POST` answers `202` then the row fills in;
34
  listen on `/events` for `drawing.changed`), MCP tools (`draw`,
35
  `get_drawing`, `list_drawings`, `delete_drawing`) and a terminal (list and
36
  ask; a terminal can't show pictures).
37
- On the forge an app reaches the internet only through its egress proxy:
38
  `forge.json` declares `api.anthropic.com` and `claude.ai`.
39
40
This is an app: the forge runs it. Production follows the default branch;
41
every open pull gets a preview at its own address with its own copy of the
42
data. Everything the app keeps lives under `FORGE_APP_DATA`, which is what
43
makes "production data" and "preview data" two different things.
44
45
```sh
46
PORT=8080 FORGE_APP_DATA=./data bun run src/server.ts   # run it here
47
bun test                                                # what `forge.json`'s app.test runs on every push
48
FORGE_APP_DATA=./data bun run src/tty.ts                # the terminal
49
FORGE_APP_DATA=./data bun run src/db.ts --check         # schema version, integrity
50
bun run lint:design                                     # no colour but the tokens'
51
bun run contract:emit                                   # rewrite contract/openapi.json from the routes
52
```
53
54
`app.test` in `forge.json` runs beside `app.run` in every deploy, not before
55
it: the preview comes up the moment the app answers, and says `live`, or
56
`serving · tests failed` the moment the suite disagrees.
57
58
## The shape
59
60
One implementation, many doors. `src/actions.ts` is every operation as a
61
plain function — `(db, who, input)` in, data or a named failure out — and
62
the page, the JSON API, the MCP server and the terminal all call it and
63
decide nothing themselves. `AGENTS.md` says how to add to it.
64
65
| file                               | what it is                                                     |
66
| ---------------------------------- | -------------------------------------------------------------- |
67
| `src/actions.ts`                   | start, list, read, delete a drawing; the audit trail, read     |
68
| `src/glyph.ts`                     | the ask, the reply read back, the SVG allowlist, palettes      |
69
| `src/draw.ts`, `src/claude.ts`     | the work after the response: Claude Code, run with no tools    |
70
| `design/palettes.json`             | the drawing palettes and the three placeholder colours         |
71
| `contract.json`                    | who may do what: roles, and the least role each verb needs     |
72
| `src/contract.ts`, `src/can.ts`    | the table, read at boot; the forge's evaluator, verbatim       |
73
| `src/security.ts`                  | the door: rationing, same origin, the table before the body    |
74
| `src/audit.ts`                     | every change and every refused one, as a row                   |
75
| `src/who.ts`                       | who is asking, from the forge's signed headers                 |
76
| `src/db.ts`                        | SQLite under `FORGE_APP_DATA`, WAL, forward-only migrations    |
77
| `src/live.ts`                      | the in-process bus and `GET /events`                           |
78
| `src/mcp.ts`                       | the actions as MCP tools at `POST /mcp`                        |
79
| `src/routes.ts`                    | every path, in one table, each API route described beside it   |
80
| `src/openapi.ts`                   | the routes as OpenAPI; committed at `contract/openapi.json`    |
81
| `src/page.ts`                      | the HTML, rendered on the server, with its share metadata      |
82
| `design/tokens.json`, `src/tokens.ts` | the look as data, light and dark; compiled to `/tokens.css` |
83
| `src/http.ts`                      | the floor: headers, body cap, files                            |
84
| `src/server.ts`                    | boot: open the database, serve                                 |
85
| `src/tty.ts`                       | the terminal, over the same actions                            |
86
| `src/env.ts`                       | what the forge told this process                               |
87
| `static/app.css`, `static/live.js` | the look in the tokens' words, and the board that stays current |
88
| `static/glyph.js`                  | copy, the palette swapped in place, the example prompts        |
89
90
## What every app stands on
91
92
- **The door.** Before a route sees a request, `src/security.ts` rations
93
  writes (and a stranger's API calls), refuses a write from another site,
94
  and turns away — before the body is read — anyone the table lets through
95
  a resource's door for no row at all: 401 for a stranger, 403 for anyone
96
  else. The action still decides about the real row.
97
- **The audit trail.** Every change, and every change refused, is a row:
98
  who, which resource and verb, which row, when, and whether they were
99
  let. Admins read it at `GET /api/audit`. A row that cannot be written
100
  is a log line, never a failed request.
101
- **The API, described.** `GET /openapi.json` is `contract/openapi.json`,
102
  written from the routes by `bun run contract:emit`; each operation names
103
  its cell of the table (`x-contract`). `bun test` fails while it is stale.
104
- **Up, or not.** `GET /healthz` answers `{ ok, version, uptime }`; the
105
  forge waits on it before it sends anyone to a deploy.
106
- **The look.** `design/tokens.json` holds every colour, space and size,
107
  light and dark; nothing else spells out a colour (`bun run lint:design`),
108
  and every pair a person reads meets 4.5:1 in both themes.
109
- **The open web.** A shared link unfurls (description, Open Graph, a
110
  card); `/robots.txt` keeps crawlers to the page, and out of previews
111
  entirely; `/.well-known/security.txt` sends a reporter to the forge
112
  that runs the app.
113
114
## Who you are
115
116
The forge says who is asking on every request it proxies, and the app
117
verifies the signature (`src/who.ts`); nothing else is trusted. Three tiers:
118
119
- **anon** — anyone. Sees the page and is asked to sign in; nothing else.
120
- **user** — signed in to the forge and able to see this app. Draws, as
121
  themselves, and sees and removes their own drawings.
122
- **admin** — able to write this repository. Sees and removes any drawing.
123
124
"Sign in" is `/__forge/signin?to=<path>` and "Sign out" is
125
`/__forge/signout`; the forge answers both on the app's own host, and
126
`/__forge/report` too: it sends anyone, with no account, to report a
127
problem on this app's repository, if that repository is public.
128
129
## Any interface
130
131
The same actions, the same rules, at every door. A script is the person
132
whose forge grant it carries; a bare script is anonymous.
133
134
```sh
135
curl -X POST https://<app>/api/drawings -H 'content-type: application/json' -d '{"prompt":"a fox made of circuit traces","kind":"mark"}'
136
curl https://<app>/api/drawings/1          # status: drawing → done, with svgs[]
137
curl -X DELETE https://<app>/api/drawings/1
138
```
139
140
As an MCP server, for Claude Code or Claude Desktop — `draw`,
141
`get_drawing`, `list_drawings`, `delete_drawing`:
142
143
```sh
144
claude mcp add --transport http glyph https://<app>/mcp
145
```
146
147
```json
148
{
149
  "mcpServers": { "glyph": { "type": "http", "url": "https://<app>/mcp" } }
150
}
151
```
152
153
The page stays current over `/events`: a drawing fills in on its own when
154
Claude is done, without a reload. Without JavaScript it is the same page, one
155
reload behind.
156
157
The terminal opens with the exact command in `FORGE_APP_SSH` when the app
158
runs (`ssh -p 2222 <name>@<forge>` — the app is the login name, the forge
159
the host; `<name>--<pull>@<forge>` for a preview's). It is `src/tty.ts` on
160
a real pty over the same data, as the account at the keyboard.
161
162
The forge sets `PORT`, `FORGE_APP_DATA`, `FORGE_APP_ENV` (`production` or
163
`preview`), `FORGE_APP_URL`, `FORGE_APP_COMMIT`, `FORGE_APP_SSH` and
164
`FORGE_APP_IDENTITY_KEY` when it starts the app. `forge.json` says how.
↑↓ navigate ↵ open esc close
↑↓ navigate ↵ open esc close

Keyboard shortcuts