zibel
by 286486
README.md
# Kalamo
A vector drawing tool that runs in the browser. Each person brings their own AI agent over MCP, and people and agents edit the same document on an Illustrator-style canvas.

An Agent drew this over MCP: Live Shapes, Bezier paths and text in two Layers, with each label centred from the bounds the server measured, then exported to PNG with `kalamo_export`.
Kalamo comes from Greek *kálamos*, the reed pen, the first drawing instrument. The same word means "pen" as Arabic *qalam*, Turkish *kalem* and Hindi *kalam*. Say it KAH-lah-moh; in Chinese it is 卡拉莫 (kǎ lā mò).
## Status
Milestone M0 is in progress (issue #1). A local Worker already takes MCP calls to create a Document, draw shapes into it, read its outline and render it to PNG. A browser viewer shows each Document live as an Agent draws, and a person can select, move and delete what it drew.
Start here. Domain vocabulary is in [CONTEXT.md](CONTEXT.md) and architecture decisions in [docs/adr/](docs/adr/).
| Document | Contents |
|---|---|
| [docs/REQUIREMENTS.md](docs/REQUIREMENTS.md) | Requirements v0.4 (Chinese). Scope, functional requirements, MCP tool surface, architecture, milestones, Illustrator feature mapping |
| [docs/research/01-illustrator-core-features.md](docs/research/01-illustrator-core-features.md) | Adobe Illustrator tools, panels, workflows and scripting DOM, from the official user guide |
| [docs/research/02-web-vector-tech-landscape.md](docs/research/02-web-vector-tech-landscape.md) | Figma, Penpot, Excalidraw, tldraw and Graphite architecture; rendering, boolean, text, freehand and sync library choices |
| [docs/research/03-mcp-design-tool-patterns.md](docs/research/03-mcp-design-tool-patterns.md) | How existing MCP servers expose design tools, and what breaks |
## Local development
Needs Node 22 or later.
```sh
corepack enable
pnpm install
pnpm check # typecheck, Biome, and Vitest inside workerd
pnpm test:e2e # Playwright smoke test against its own wrangler dev (needs `pnpm exec playwright install chromium`)
pnpm dev # builds the web app, then wrangler dev: viewer at http://localhost:8787, MCP at /mcp
```
Every MCP request needs `Authorization: Bearer <dev token>`. Copy `apps/edge/.dev.vars.example` to `apps/edge/.dev.vars` and set the `DEV_TOKENS` token-to-Agent-Actor pairs. Keep its `AUTH_MODE="dev"`: without it the Worker runs in GitHub mode and answers every request `server misconfigured` (see Hosting). Documents are stored under `.wrangler/state` and survive a restart of `pnpm dev`. The viewer lists them at http://localhost:8787 and opens one at `/docs/<docId>`: Space-drag or scroll to pan, Ctrl+scroll or pinch to zoom, Z then click (Alt+click) to zoom in (out), Ctrl+0 to fit the Artboards, Ctrl+1 for 100%. Click an object to select it (Shift-click toggles, Alt+Shift-click removes) or drag a marquee over several; drag the Selection to move it, press Delete or Backspace to delete it, Ctrl+A to select all and Ctrl+Shift+A to deselect. Each move or delete is one Transaction by the Actor `user`. Ctrl+Z undoes the Document's latest Transaction, whoever made it, including an Agent's whole Transaction in one step, and Ctrl+Shift+Z redoes it; both are Transactions too. The local viewer has no login (dev mode). The first `pnpm dev` asks once to apply the local D1 migration that indexes Documents.
To connect Claude Code, copy [examples/claude-code.mcp.json](examples/claude-code.mcp.json) to `.mcp.json`, or run:
```sh
claude mcp add --transport http kalamo http://localhost:8787/mcp -H "Authorization: Bearer YOUR_TOKEN"
```
The server is named `kalamo`, its tools are `kalamo_*` (`kalamo_doc_create`, `kalamo_export`, …), its resources `skill://kalamo/*`, and `kalamo_export` takes the format `kalamo_json`. In Claude Code, allow its tools with `mcp__kalamo__*`. A client set up before the product was renamed Kalamo (ADR-0069) must be added again under this name, and its permission allowlist and any tool names in prompts updated: the old names have no aliases.
## Hosting
The Worker runs in one of two auth modes, set by `AUTH_MODE` (ADR-0047):
- `github`, the default in `apps/edge/wrangler.jsonc`: people sign in to the browser app with GitHub. Any value other than `dev` means `github`.
- `dev`: MCP takes the `DEV_TOKENS` Bearer tokens and browsers are not signed in. GitHub mode ignores `DEV_TOKENS`. It is safe only on a private network, and only with long random tokens.
GitHub mode enforces the free beta's quotas, each failing `LIMIT_EXCEEDED` (ADR-0048): 50 owned Documents, 200 MB of image files per owner, 500 `render` and 200 `export` calls per User per UTC day, and 20 browser connections per Document. Dev mode enforces none of them.
GitHub mode needs a GitHub OAuth App (GitHub > Settings > Developer settings > OAuth Apps) whose authorization callback URL is `<APP_ORIGIN>/auth/github/callback`, for example `https://kalamo.example.workers.dev/auth/github/callback`. Kalamo asks it for no scopes. The Worker then needs these secrets, and answers every request 500 `server misconfigured` while one is missing:
- `APP_ORIGIN`: the browser app's origin, such as `https://kalamo.example.workers.dev`, with no trailing slash. It is also the OAuth issuer that MCP clients authorize with.
- `MCP_ORIGIN` (optional): the origin MCP clients connect to, such as `https://mcp.kalamo.example`, if it differs from `APP_ORIGIN`. Both hosts must route to the Worker.
- `GITHUB_CLIENT_ID` and `GITHUB_CLIENT_SECRET`: the OAuth App's.
GitHub mode also needs the `OAUTH_KV` KV namespace, where `@cloudflare/workers-oauth-provider` keeps MCP OAuth grants and tokens (hashed, with encrypted props). The provider needs no secret of its own.
MCP clients connect to `<MCP_ORIGIN>/mcp` with no token, for example `claude mcp add --transport http kalamo https://kalamo.example.workers.dev/mcp`. The client discovers the authorization server, opens GitHub sign-in and a Kalamo consent page in the browser, and becomes an Agent of that person, named after the client and the login. The account menu's Connected Agents lists and revokes them.
```sh
pnpm exec wrangler login
cp apps/edge/.deploy.vars.example apps/edge/.deploy.vars # fill in the secrets above
pnpm exec wrangler d1 create kalamo --location apac --binding DB --update-config -c apps/edge/wrangler.jsonc
pnpm exec wrangler kv namespace create kalamo-oauth --binding OAUTH_KV --update-config -c apps/edge/wrangler.jsonc
pnpm deploy:check
pnpm run deploy
```
`pnpm run deploy` (plain `pnpm deploy` is pnpm's own command) builds the web app, applies remote D1 migrations, uploads `apps/edge/.deploy.vars` as encrypted Worker secrets, and deploys the Worker, Durable Object, and static assets. It never reads `.dev.vars`, so a deployed Worker runs in GitHub mode unless the deploy sets `--var AUTH_MODE:dev` on purpose. The resulting `workers.dev` URL needs no domain configuration; add a custom domain later in Cloudflare if wanted, and update `APP_ORIGIN` and the OAuth App's callback URL to match.
### No hosted deployment
Nothing hosts the editor now. Its Worker `kalamo`, D1 database `kalamo`, R2 bucket `kalamo-images` and KV namespace `kalamo-oauth` were deleted from Cloudflare on 2026-09-30, and `https://kalamo.woodywang2013.workers.dev` answers 404. Only the landing page is deployed (below).
To host the editor again, create the storage first; the ids in `apps/edge/wrangler.jsonc` name the deleted resources:
```sh
pnpm exec wrangler d1 create kalamo --binding DB --update-config -c apps/edge/wrangler.jsonc
pnpm exec wrangler r2 bucket create kalamo-images
pnpm exec wrangler kv namespace create kalamo-oauth --binding OAUTH_KV --update-config -c apps/edge/wrangler.jsonc
```
Then deploy as above. For dev mode, put only `DEV_TOKENS` in `apps/edge/.deploy.vars` and run `pnpm deploy:check` and `pnpm run deploy --var AUTH_MODE:dev`, which overrides the config's `github`.
## Landing page
The landing page `site/public/index.html`, with its social preview image `og.png` (a 1200×630 screenshot of the hero; retake it when the hero changes), is its own static-only Worker, `kalamo-site` (Workers Static Assets, no script, no bindings), configured in `site/wrangler.jsonc`. It is separate from the editor's Worker `kalamo`, so deploying one never replaces the other; today it is the only one deployed. `site/public/_redirects` is deploy-time config, not an uploaded file: it serves the page at `/` and answers 404 for `/index.html`; every other path is a bodiless 404. An encoded or doubled-slash spelling of `/index.html` (`/%69ndex.html`, `//index.html`) may instead get a bodiless 307 to `/index.html`, which then 404s: the asset worker redirects to the decoded path when it differs from the one requested.
```sh
pnpm deploy:site:check # dry run: "No bindings found."; WRANGLER_LOG=debug lists the assets
pnpm deploy:site
```
The config turns off the `workers.dev` and preview URLs and has no `routes`. The `kalamo.cc` custom domain is bound to `kalamo-site` outside wrangler, through the account's custom domains (`PUT /accounts/<account>/workers/domains` with `hostname`, `service`, `zone_id` and `environment: "production"`), which also created the apex DNS record. wrangler 4.136.3 publishes custom domains only when the config lists a `custom_domain` route, so `pnpm deploy:site` leaves that binding alone. Do not add a `routes` entry to `site/wrangler.jsonc` without a ticket: wrangler would then own the binding and could replace it.
To verify a deploy, where `<account>` is the account id from `wrangler whoami`:
```sh
pnpm exec wrangler deployments list --name kalamo-site
curl -s -o /dev/null -w '%{http_code}\n' https://kalamo-site.woodywang2013.workers.dev/ # 404: workers.dev is off
curl -sI https://kalamo.cc/ # 200
curl -sI https://kalamo.cc/index.html # 404
```
and check that the account's custom domains (`GET /accounts/<account>/workers/domains`) list `kalamo.cc` for `kalamo-site` and nothing else for it.
To roll back:
- Bad content: `pnpm exec wrangler rollback --name kalamo-site <previous version id>`, or check out the last good commit and run `pnpm deploy:site`. Neither touches the domain binding.
- Remove the landing page, in this order: delete the custom domain `kalamo.cc` from `kalamo-site` (dashboard, or `DELETE /accounts/<account>/workers/domains/<domain id>`), which removes the apex DNS record it created; run `pnpm exec wrangler delete --name kalamo-site`; then check that public DNS gives no A or AAAA for `kalamo.cc`.
Always pass `--name kalamo-site` to `wrangler delete`, and never run it from `apps/edge`.
## What it is meant to do
Three kinds of work, all producing editable vector output rather than images:
- Charts and diagrams, from data or from a Mermaid description
- Illustration, icons and logos, with Bezier paths, boolean operations, gradients and an appearance stack
- Freehand drawing with pressure, fitted to editable curves
Agents drive it over MCP. Every edit a person can make in the UI has a matching tool call, and every write returns both the affected node IDs and an optional rendered preview, so an agent can check its own work.
## Planned shape
- Document model is a flat, ID-keyed scene graph serialised as readable JSON
- Rendering starts on Canvas2D and moves to Skia via CanvasKit as object counts grow
- Boolean operations use Skia PathOps; text is shaped with HarfBuzz
- Hosted on Cloudflare, with one Durable Object per document holding authoritative state; the same Worker bundle runs locally under `wrangler dev` and self-hosted under workerd
- MCP is stateless Streamable HTTP only: no stdio, no MCP sessions, every request carries its own token and document address
- Monorepo under `apps/` and `packages/`, laid out in REQUIREMENTS.md section 8.3
## Licence
Apache-2.0. See [LICENSE](LICENSE) and [NOTICE](NOTICE).
The Kalamo name and logo are not covered by that licence.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessUnresponsive