syokan — LLMs summon rich UI.
syokan (召喚, "summon") is a verb. Chant the name of what you want to see, and a view appears:
syokan dashboard.json # syokan your dashboardAn LLM speaks a JSON incantation, and a rich, living interface materializes — no JSX written, no build step. Scattered data — today's RSS, an in-flight PR review, shared meeting notes, a live status board — appears as structured UI only when you need it. Views are ephemeral: summoned when needed, they fade; nothing is hoarded. And anything can chant: Claude Code, a scheduled agent, a CLI one-liner, a webhook.
syokan-demo.mp4
Claude Code chants; a diff-and-graph PR review view is summoned. (re-shootable source)
Summon your own —
brew install wwwyo/tap/syokan, then chant. See Getting started.
日本語版: README.ja.md
Design rationale, directory layout, and development conventions live in AGENTS.md. This README covers usage.
Under the hood, syokan is a personal schema-driven view layer. Instead of writing JSX, an LLM (Claude Code / scheduled agent / CLI) posts a JSON tree, and syokan renders it with predefined React components. Snapshots are ephemeral — summoned views are not meant to stay; data that must persist does not belong here. Deleting a view archives it instead of destroying it: it leaves the list, but syokan snapshots list --archived / syokan snapshots get <id> can still answer "what did I look at yesterday?", and syokan snapshots purge <id> removes the record for good.
A snapshot is a JSON tree of { type, props, children? } nodes. Each type names a catalog component; on receipt, Zod validates the tree and the registry maps it to a React component:
{ "type": "Heading", "props": { "text": "Today" } } → <Heading text="Today" />
It is a CSR app with client-side routing (TanStack Router). / is home, /snapshots/:id is an individual snapshot. Every non-API path returns the SPA HTML, so deep links and reloads just work.
For everyday use, syokan is a single binary (no Bun/Node required; the server lazy-spawns automatically). Supported OS: macOS and Linux — Windows is not supported.
# Homebrew
brew install wwwyo/tap/syokan
# installer script
curl -fsSL https://raw.githubusercontent.com/wwwyo/syokan/main/install.sh | sh
# mise
mise use -g github:wwwyo/syokan@latestsyokan --help # list commands (machine-readable: --help --json)Or grab
syokan-<os>-<arch>from Releases, or build from source (Build). A browser-downloaded binary can be blocked by macOS Gatekeeper — if so, runcodesign --sign - <path>.
Your first summon (the server starts automatically and a view URL is returned):
echo '{"root":{"type":"Heading","props":{"text":"🎉 syokan is set up"}}}' | syokan
syokan open # open homeCheck props with syokan catalog and compose the tree. From there, syokan whatever you want to see.
A tree file needs no envelope at all — just syokan the path. If the input is envelope JSON it is posted as-is; a bare catalog tree is inlined into a self-contained snapshot keyed by the file's absolute path, so re-running the command after an edit updates the same view in place (an LLM keeps rewriting the file and re-posting; the view keeps up). Non-JSON input is rejected — syokan speaks catalog trees only:
syokan dashboard.json # post; edit the file and re-run to update the same viewmise install && bun install
bun run dev # both apps: syokan server (Bun.serve + HMR) and the share Worker (wrangler dev)Root dev fans out to every workspace app (bun --filter '@syokan/*' --parallel dev); each app owns its own dev script. It brings up two processes:
@syokan/appon port5873(https://syokan.localhostvia portless), writing to the repo-local./.syokan-dev/directory, so it never collides with a global install (port5773/ standard XDG directories).@syokan/shareon port8787(wrangler dev, local KV via miniflare). The viewer is bundled once at startup; worker code hot-reloads, but viewer edits need a re-run (no viewer HMR).
In dev the syokan server points SYOKAN_SHARE_API at the local Worker (http://localhost:8787), so publishing/sharing is exercised against local KV — it never touches the production syokan.dev share service. Inside this repo the mise [shell_alias] likewise points the syokan CLI at the dev server (SYOKAN_BASE_URL=http://localhost:5873), so syokan <file> posts to dev, not production; outside the repo syokan is the global install. To skip portless, use PORTLESS=0 bun run dev (default port 5773). To run just one app, use its own dev (e.g. bun --filter @syokan/app dev) — note @syokan/app alone still targets local share on 8787, so publishing needs the share Worker up too.
A snapshot envelope (JSON only; markdown is not rendered — structure prose into catalog nodes, or wrap raw text in Code with no lang) is created with POST /api/snapshots and refreshed in place with PUT /api/snapshots:
The SSOT for these keys is the envelope schema returned by GET /api/catalog (also see syokan catalog) — fetch it rather than hand-copying field docs. It describes the envelope's own keys and leaves root opaque; the node contract lives in items, so validating a whole body means checking both.
POST always creates a fresh snapshot (201); an idempotencyKey just tags it for later PUTs. PUT requires idempotencyKey and targets an existing view by it: a match replaces root/title in place (same id/url; createdAt is kept) and returns 200; no match returns 404 (not_found) — PUT never creates (there is no "create if missing" escape hatch; use POST for that). Validation errors return 400 (invalid_json / validation_failed). CLI commands: syokan --help.
The SSOT for type is the catalog (apps/syokan/src/catalogs). Fetch the manifest to get the props contract and the full type list — syokan catalog (CLI) or:
GET /api/catalog # { items: [{ type, props (JSON Schema), childrenTypes, notes }], envelope: <JSON Schema for the envelope's own keys> }
Review types visually with Storybook (bun run storybook).
Every node also accepts the cross-cutting field id (in-view anchor via Link href:"#<id>", and the identity that lets interactive nodes carry state). On an id-carrying Checklist, a check writes back into the snapshot itself (PATCH /api/snapshots/:id) — progress survives reloads, reaches other devices, and lands in published shares; an id-less Checklist keeps checks in the viewer's browser. Folds and probe re-runs stay viewer-local either way. Probe runs only the predefined read-only checks published in its check props schema (POST /api/probes/run); on public shares, re-run is disabled and probe args/results are stripped at publish unless shareVisible: true.
Every view renders the stored snapshot — nothing references a file or live source, and store mutations (post/update/patch/delete) push a change notification over SSE so open views and the list follow along.
A layout you like can be kept as a template (a saved envelope + title) under ~/.local/share/syokan/templates/. Unlike snapshots, templates persist. syokan only stores and lists them and never interprets their contents (GET/POST/DELETE /api/templates).
Any snapshot can be published to a public URL on syokan.dev — viewable by anyone with the link, no account needed to read:
syokan login # sign in with GitHub (device flow, opens a one-time code)
syokan publish <id> # → https://syokan.dev/shares/<uuid>
syokan publish <id> --expires 30d # up to 30 days
syokan shares # your published shares
syokan unpublish <shareId>Shares are as ephemeral as everything else: they expire after 7 days by default, 30 days at most, then vanish — there is no permanent hosting. Each user can hold up to 100 live shares. Publishing copies the stored view at that moment (later local edits never reach the public URL); republishing mints a new URL. The UI's Share button on a view does the same thing.
By publishing you agree to the terms. Abusive shares can be reported via the "Report abuse" link on any share page.
Display settings (theme / font) persist as a singleton resource at ~/.config/syokan/settings.json (unlike snapshots, they are kept). The browser's localStorage is a cache for instant application; the server is the source of truth, synced on startup, so settings are shared across browsers.
GET /api/settings # { theme, font } (defaults if unset)
PUT /api/settings # partial update (only the keys you send are overwritten). Unknown keys / invalid values: 400
theme: system light dark (SSOT: apps/syokan/src/schema/setting.ts). font: an identifier for a font preset (default system; most presets load from Google Fonts, but system/moralerspace do not). The list and how to extend it live in apps/syokan/src/lib/fonts.ts — adding one entry there adds a font (the actual font is loaded dynamically via <link> on selection and --app-font-* is rewritten; styles.css / index.html stay untouched).
bun run compile # → apps/syokan/dist/syokan (CLI + server + frontend in one binary)
bun run compile:all # → apps/syokan/dist/syokan-<os>-<arch> (cross-compile, for Release distribution)Dual-mode (entry.ts): a normal launch is the CLI; the server re-execs the binary itself with SYOKAN_SERVE=1. The global binary uses port 5773, and persistence follows the XDG base directories (settings = ~/.config/syokan/, templates = ~/.local/share/syokan/, snapshots + runtime/log = ~/.local/state/syokan/). Override locations with XDG_{CONFIG,DATA,STATE}_HOME (absolute paths only; relative values are ignored). When upgrading from the old layout, templates are migrated to the new location automatically on startup. To distribute: bump version in apps/syokan/package.json through an ordinary PR (main is ruleset-guarded, so the bump commit cannot be pushed directly), then run bun run release on the merged main — it tags that commit and pushes the tag alone. CI then cross-compiles, gates on a smoke test of the compiled binaries (Linux + macOS), publishes the Release with checksums.txt, and bumps the Homebrew formula.
- Design rationale, directory layout, development conventions: AGENTS.md
{ "root": { "type": "Stack", "props": {}, "children": [ /* ... */ ] }, "title": "Today's RSS", "idempotencyKey": "rss-2026-06-20" }