Extract eleven topic areas into loadable skills so AGENTS.md can serve as a scannable overview instead of a specification dump. The file shrinks from 1480 to 358 lines (~76%) while keeping every concrete rule, critical code pattern, and pointer that an agent needs on first read. New Ditto-specific skills: - nostr-kinds: NIP-vs-custom-kind decision framework, kind ranges, tag design, content-vs-tags, NIP.md update rule, and Ditto's seven-location UI registration checklist for new kinds (NoteCard, PostDetailPage, extraKinds.ts, KIND_LABELS/KIND_ICONS in CommentContext, WELL_KNOWN_KIND_LABELS in ExternalContentHeader, EmbeddedNote/EmbeddedNaddr, ReplyComposeModal). - nostr-publishing: useNostrPublish, the read-modify-write pattern via fetchFreshEvent + prev for replaceable/addressable events, published_at contract, and d-tag collision prevention. - nostr-queries: the standard useNostr + useQuery pattern, combining kinds into one filter to avoid rate limits, and the NIP-52 validator walkthrough. - theming: @fontsource install flow, the Ditto runtime font-loader path (sanitizeUrl + sanitizeCssString), color scheme variables, useTheme toggle, and the isolate + negative-z-index gotcha. - ci-cd-publishing: Zapstore NIP-46 bunker auth (zsp + nip46-auth.mjs), nsite deploys (nsyte nbunksec + configured relays/servers), and Google Play AAB uploads via fastlane supply (service-account JSON base64 encoding and rotation). - capacitor-compat: WKWebView/WebView limitations, the downloadTextFile / openUrl helpers in src/lib/downloadFile.ts, platform detection, and the full plugin list. - git-workflow: pre-commit validation order and the Regression-of: trailer convention used by the release skill's changelog generator. Ported from mkstack, lightly adapted where needed: - nip19-routing: root-level /:nip19 routing and filter construction patterns (adapted to reference Ditto's existing NIP19Page). - nostr-relay-pools: nostr.relay() and nostr.group() for targeted queries. - nostr-encryption: NIP-44 / NIP-04 via the user's signer. - file-uploads: useUploadFile + Blossom + NIP-94 imeta tag construction. AGENTS.md itself now follows mkstack's density — concrete rules inline, one code example per section, pointer to the matching skill for details. The enumerations that previously bloated it (every shadcn primitive, every hook, every Capacitor plugin, the full NostrMetadata type dump, the NIP-19 prefix reference table, etc.) are either removed in favor of "ls the directory" or moved into their skill.
7.4 KiB
name, description
| name | description |
|---|---|
| nostr-kinds | Decide whether to reuse an existing NIP or mint a new kind, design tag structures that relays can index, choose what goes in content vs. tags, and register a new kind in Ditto's many UI touchpoints (feed cards, detail pages, embedded previews, kind-label maps). |
Nostr Kinds — Design and Registration
Use this skill when introducing a new kind to Ditto, extending an existing NIP with new tags, or deciding whether an existing NIP covers a feature. It covers the decision framework, schema rules, and — critically — the full list of places a new kind must be registered in Ditto's UI.
Choosing Between Existing NIPs and Custom Kinds
- Thorough NIP review first. Browse the NIP index, then read candidate NIPs in detail. The goal is to find the closest existing solution.
- Prefer extending existing NIPs over creating custom kinds, even at the cost of minor schema compromises. Custom kinds fragment the ecosystem.
- When an existing NIP is close but not perfect, use its kind as the base and add domain-specific tags. Document the extension in
NIP.md. - Only mint a new kind when no existing NIP covers the core functionality, the data structure is fundamentally different, or the use case requires different storage characteristics (regular vs. replaceable vs. addressable).
- If a tool to generate a new kind number is available, you MUST call it. Never pick an arbitrary number.
- Custom kinds MUST include a NIP-31
alttag with a human-readable description of the event's purpose.
Example decision:
Need: Equipment marketplace for farmers
Options:
1. NIP-15 (Marketplace) — too structured for peer-to-peer sales
2. NIP-99 (Classifieds) — good fit, extensible with farming tags
3. Custom kind — perfect fit, no interoperability
Decision: NIP-99 + farming-specific tags.
Kind Ranges
An event's kind number determines storage semantics:
- Regular (1000 ≤ kind < 10000) — stored permanently by relays. Notes, articles, etc.
- Replaceable (10000 ≤ kind < 20000) — only the latest event per
pubkey+kindis kept. Profile metadata, contact lists, mute lists. - Addressable (30000 ≤ kind < 40000) — identified by
pubkey+kind+d-tag; only the latest per combo is kept. Long-form content, products, definitions.
Kinds below 1000 are "legacy"; storage is per-kind (e.g. kind 1 is regular, kind 3 is replaceable).
Tag Design Principles
-
Kind = schema, tags = semantics. Don't mint a new kind just to represent a different category of the same data.
-
Relays only index single-letter tags. Use
tfor categories so filters like'#t': ['electronics']work at the relay level. Multi-letter tags (product_type, etc.) force inefficient client-side filtering. -
Filter at the relay, not in JavaScript:
// ❌ Fetch everything, filter locally const events = await nostr.query([{ kinds: [30402] }]); const filtered = events.filter((e) => hasTag(e, 'product_type', 'electronics')); // ✅ Filter at the relay const events = await nostr.query([{ kinds: [30402], '#t': ['electronics'] }]); -
For Ditto-specific niches (community apps, regional variants), tag events with a
tvalue and query on it. Don't do this for generic platforms — it would silo content.
Content vs. Tags
content— large freeform text or existing industry-standard JSON (GeoJSON, FHIR, Tiled maps). Kind 0 is the one exception where structured JSON goes in content.- Tags — queryable metadata, structured data, anything you might filter on.
- Empty content is fine.
content: ""is idiomatic for tag-only events. - If you need to filter by a field, it must be a tag — relays don't index content.
// ✅ Queryable
{ "kind": 30402, "content": "",
"tags": [["d", "product-123"], ["title", "Camera"], ["price", "250"], ["t", "photography"]] }
// ❌ Structured data buried in content
{ "kind": 30402, "content": "{\"title\":\"Camera\",\"price\":250}", "tags": [["d", "product-123"]] }
NIP.md
NIP.md documents Ditto's custom kinds and any extensions to existing NIPs. Whenever you mint a new kind or change a custom schema, create or update NIP.md with the tag list, content format, and intended usage. If a kind you add is effectively the same shape as an existing NIP, note the NIP reference rather than duplicating the spec.
Registering a New Kind in the Ditto UI
When adding support for a new kind, the kind must be registered in multiple locations or it will render incorrectly in certain views (blank content in quote posts, "Kind 12345" as a label, missing action headers, etc.).
Checklist
-
Content card component (
src/components/) — create<MyKindCard>that renders the event's tags/content appropriately. -
Feed rendering (
src/components/NoteCard.tsx):- Add
const isMyKind = event.kind === XXXX;. - Include it in the appropriate group flag (e.g.
isDevKind) or theisTextNoteexclusion list. - Add the content dispatch:
isMyKind ? <MyKindCard event={event} /> : …. - Add an entry to
KIND_HEADER_MAPfor the action header (e.g. "deployed an nsite"). - Import the new component and any new icons (e.g.
Globefromlucide-react).
- Add
-
Detail page (
src/pages/PostDetailPage.tsx):- Mirror the
isMyKinddetection and group/exclusion flags fromNoteCard. - Add the content dispatch for the detail view.
- Add an entry in
shellTitleForKind()for the loading-state title. - Import the new component.
- Mirror the
-
Feed registration (
src/lib/extraKinds.ts):- Add the kind number to an existing feed definition's
extraFeedKindsarray, or create a newExtraKindDefentry.
- Add the kind number to an existing feed definition's
-
Kind-label registries — independent maps that resolve kind → human-readable string/icon. All must be updated:
KIND_LABELSandKIND_ICONSinsrc/components/CommentContext.tsx— used for "Commenting on an nsite" text and inline icons.WELL_KNOWN_KIND_LABELSinsrc/components/ExternalContentHeader.tsx— used in addressable event preview headers.- The icon fallback in
AddressableEventPreviewin the same file.
-
Embedded note cards (
src/components/EmbeddedNote.tsx,src/components/EmbeddedNaddr.tsx) — small preview cards shown inside quote posts, reply-context indicators, and CommentContext hover cards. They are separate components fromNoteCardand render a minimal preview (author + title/content + attachment indicators). Basic rendering works for all kinds automatically, but kinds whose media lives in tags (e.g. kind 20 photos viaimetatags) may need attachment-indicator logic added toEmbeddedNoteCard.Do not confuse these with the
compactprop onNoteCard— that just hides action buttons on the fullNoteCard.EmbeddedNote/EmbeddedNaddrare entirely different components. -
Reply composer (
src/components/ReplyComposeModal.tsx) —EmbeddedPostdelegates to the sharedEmbeddedNote/EmbeddedNaddrcomponents, so no per-kind registration is needed here.
Why so many places?
These are genuinely different UI contexts (feed cards, detail pages, embedded previews, reply previews, comment-context labels) with different rendering requirements. Several of them maintain independent kind-to-label maps that could theoretically be unified. When in doubt, grep the codebase for an existing kind number like 30617 — you'll find every registration point you need to mirror.