Uniques & the store
A unique is a static web app plus a manifest plus a guide. The engine serves it, the shell gives it a companion, chat and components, and LIP carries what you do to Claude and what Claude decides back.
What a unique is
The engine knows nothing about graphs. A unique is a repository with a manifest, a guide and a static UI; the engine serves it, gives it the companion and the protocol, and that is all. Uniques are plain HTML, ES-module JavaScript and CSS: no build step, no CDN, no server-side code. Because the host never executes unique code (it only serves static files and routes JSON envelopes), third-party uniques are safe to install straight from a git URL.
Defaults vs the store
defaults.json in the engine repo is the list of uniques every installation gets. They are installed from git into ~/.lets/uniques the first time the engine runs or the first time you open one; node engine/bin/lets.mjs defaults does it by hand, and LETS_NO_DEFAULTS=1 opts out. Today the defaults are graph and den. Defaults are trusted by the list and still validated before they are served. More defaults will be added there as they mature.
Everything else comes from a store: a git repository whose index.json lists uniques by git URL. Install with /lets store <name>: Claude syncs, searches, shows you the inspection (permissions, problems, verified flag), and installs only after you say yes. The first store-only uniques by the maintainers are cocode, a code window you talk through (/lets store cocode), and mors, a Morse key you talk through (/lets store mors).
| Unique | Repo | Default | In lets-store |
|---|---|---|---|
| graph: talk in diagrams | lets-unique-graph | yes | yes, verified |
| den: Doto's home new in 0.6.0 | lets-unique-den | yes | yes, verified |
| cocode: talk in code new | lets-unique-cocode | no | yes, verified |
| mors: talk in Morse new | lets-unique-mors | no | yes, verified |
| pulse: a dial you talk through (template) | lets-unique-template | no | yes, verified |
New candidates grow in the store first. Once a unique is solid, propose it for defaults.json. See the uniques page for cards.
Authoring a unique
The folder
my-unique/
unique.json # manifest (required)
GUIDE.md # instructions for Claude (strongly recommended)
icon.svg # 64×64
schemas/*.json # optional JSON Schemas for your events/commands
ui/index.html # entry (declared in the manifest)
ui/app.js
ui/style.css
Rules: name is lowercase kebab (^[a-z][a-z0-9-]{1,31}$); entry, guide and icon stay inside the folder; your domains are yours (lip, session, entity, chat, component, media, ui are reserved); every event and command you declare starts with one of your domains.
The manifest
The full format is in the protocol, §6. Keep summary lines short: they are shown to Claude on open. Declare capabilities honestly (the shell only enables paste/upload when media.image or clipboard is listed) and permissions (fs.write: ["uploads"] is the only write a unique can ask for). Declare permissions even when empty: "fs.write": [] means "writes nothing".
The optional session block new in 0.4.0 names which of your types are the export ("Send to Claude"), the snapshot (reply to a state query) and the restore command, so the engine can save and resume your windows. Without it the host assumes <domain>.export, <domain>.snapshot and <domain>.load.
"session": { "export": "graph.export", "snapshot": "graph.snapshot", "restore": "graph.load" }
The UI
Inside a unique the host serves the SDK at /sdk/…, so a unique's page imports it like this:
<body data-unique="my-unique">
<link rel="stylesheet" href="/sdk/lets-shell.css">
<script type="module" src="./app.js"></script>
import { Lets } from '/sdk/lets-client.js';
import { Shell } from '/sdk/lets-shell.js';
const lets = Lets.connect({ unique: 'my-unique', version: '0.1.0', caps: ['entity', 'chat', 'components'] });
const shell = Shell.mount(lets, {
root: document.body,
context: () => ({ /* what the companion should know when summoned/clicked */ }),
quickSuggestions: (ctx) => [{ id: 'help', label: 'What can I do here?' }],
});
lets.on('my-unique.thing.set', (data) => { /* a command from Claude */ });
button.onclick = () => lets.emit('my-unique.thing.changed', { value });
The shell handles entity.*, chat.*, component.* and ui.* for you; never re-implement them. Use shell.onMedia(cb) to place pasted images, shell.onInline(cb) to place inline components on your canvas, and shell.toast() for feedback. For the theme, use the CSS variables in lets-shell.css (--lets-bg, --lets-accent, …) so your unique matches the companion.
The guide
GUIDE.md is read by Claude once per open. It is the most important file you write. Write it for a smart colleague:
- one line per event: what it means and what to do;
- the commands, with tiny JSON examples;
- etiquette: animate, do not rearrange what the human made, keep labels short;
- the companion's role in this medium.
Keep it under about 1,200 words.
Start from the template
lets-unique-template is pulse: a full-screen dark-violet mood dial. You drag a ring to set a value from 0 to 100 and pick one of three mode chips (calm / focused / wild); the companion reacts, and Claude can move the dial itself. It demonstrates the whole contract in a handful of files. Template 0.2.0 makes the dial keyboard-accessible, demonstrates Send to Claude and resume with a session block, and adds validate scaffolding.
- On GitHub, "Use this template" and name the new repo
lets-unique-<name>. - Clone it and rename
pulseeverywhere: manifestname,domains, event and command types,data-unique,Lets.connect. - Build. Bump
versionwhenever you change events or commands.
The reference implementation is graph: its CONTRACT.md, GUIDE.md and ui/graph.js show every part of the contract in use.
Develop & validate
Uniques are served by the lets host, never opened from disk. Put yours where the host looks, then run the host standalone from a lets checkout:
# option a: any folder (point the registry at the PARENT of your unique folders)
LETS_UNIQUES_DIRS=/path/to/parent node engine/bin/lets.mjs serve --open my-unique --verbose
# option b: project-local
mkdir -p .lets/uniques && ln -s /path/to/my-unique .lets/uniques/my-unique
# option c: user-wide (where defaults and store installs live)
cp -r my-unique ~/.lets/uniques/
# validate the manifest
node engine/bin/lets.mjs validate ./my-unique
--verbose prints every up-event. Use the companion's chat to simulate Claude, or run the real thing: claude --plugin-dir /path/to/lets then /lets:lets my-unique. The quality bar is zero console errors in Chromium and Edge.
Publish to a store
- Push your unique to GitHub as its own repo (or a folder in one).
- Validate it with the engine:
node <lets>/engine/bin/lets.mjs validate <your-unique-dir>. - Fork lets-store, add an entry to
index.json(keep entries sorted byname) and update the top-levelupdateddate. - Run the store validator:
npm run validate. - Open a pull request. The checklist in the store's
CONTRIBUTING.mdmust be satisfied; CI runs the validator on every push and PR.
New entries land unverified. A maintainer reviews the manifest and flips the flag in a follow-up commit.
Index entry fields
| Field | Meaning |
|---|---|
name | Unique id, ^[a-z][a-z0-9-]{1,31}$; must match the manifest name. Unique within the index. |
title | Human title shown in listings. |
description | One line: what the human does with it. |
repo | Git URL the host clones from. Must be https://…, git@… or ssh://…. |
path | Folder inside the repo that holds unique.json (empty string = repo root). |
ref | Branch or tag to track (default main). |
sha | Optional commit pin: the host clones exactly this commit instead of moving with ref. |
manifestSha256 | Optional SHA-256 of unique.json, so the host can detect a manifest that changed since review. |
verified | true = reviewed by a store maintainer (below). |
tags | Free-form discovery tags. |
author | GitHub handle of the unique's author. |
What "verified" means
An entry marked "verified": true has had its manifest reviewed by a store maintainer, who checked that:
- the manifest validates against the LIP unique schema (
unique.schema.json); - the unique is static-only: HTML/JS/CSS, ES modules, no build step, no executables, no binaries, no network CDN references;
- every event and command type it declares starts with a domain it owns (none of the reserved
lip.*,session.*,entity.*,chat.*,component.*,media.*,ui.*); - every permission it needs (
fs.write,net) is declared in the manifest, so the host can show you what the unique may do before it is served.
verified is a review mark, not a cryptographic guarantee. You still see the manifest and its permissions when you install. Pinning sha (and ideally manifestSha256) freezes the reviewed content, so a later push to the branch cannot silently change what was reviewed.
Security model
- Uniques are static: the host serves files and never executes them server-side.
- Everything is served from loopback with a per-host random token, and pages get a CSP of
default-src 'self': no external CDN, no inline exfiltration. - Permissions (
fs.write,net) are declared in the manifest, validated before a unique is served, and enforced by the host: uploads land only under~/.lets/uploads/<unique>/and are size-capped. - Message types are namespaced by domain; a unique cannot forge another unique's or the shell's messages.
- Store installs go through
inspect(clone to staging, validate, hash check, executable scan) and are copied into~/.lets/uniques/<name>only after you confirm.
Design for the future
Think in acts, not widgets. Your events should say what the human meant (timeline.span.set, not mouse.drag). Your commands should be idempotent and animatable. If your medium has no pointer (voice, gesture, AR), you still emit entity.summon, component.action and media.*; the shell for that medium decides how those feel.