From 7897631733d8d0006030f618d8b947a697a4ec67 Mon Sep 17 00:00:00 2001 From: Jon Staab Date: Mon, 8 Jun 2026 16:35:10 -0700 Subject: [PATCH] Use applesauce properly --- .ackrc | 3 + .agents/skills/applesauce/SKILL.md | 101 ++ .../assets/examples/app-data/manager.tsx | 385 +++++ .../assets/examples/articles/blog.tsx | 159 ++ .../assets/examples/articles/rendering.tsx | 239 +++ .../assets/examples/badges/editor.tsx | 467 +++++ .../assets/examples/badges/profile.tsx | 415 +++++ .../examples/blossom/server-manager.tsx | 377 ++++ .../assets/examples/bookmarks/manager.tsx | 427 +++++ .../assets/examples/cache/nostr-idb.tsx | 199 +++ .../assets/examples/cache/window.nostrdb.tsx | 184 ++ .../assets/examples/cache/worker-relay.tsx | 226 +++ .../assets/examples/calendar/create-event.tsx | 539 ++++++ .../assets/examples/calendar/map.tsx | 251 +++ .../assets/examples/calendar/timeline.tsx | 402 +++++ .../assets/examples/casting/custom.tsx | 292 ++++ .../assets/examples/comment/feed.tsx | 487 ++++++ .../assets/examples/contacts/manager.tsx | 183 ++ .../assets/examples/database/turso-wasm.tsx | 502 ++++++ .../assets/examples/database/worker-relay.tsx | 647 +++++++ .../assets/examples/emojis/packs.tsx | 247 +++ .../assets/examples/feed/app-handlers.tsx | 368 ++++ .../examples/feed/loading-reactions.tsx | 164 ++ .../assets/examples/feed/pow-notes.tsx | 301 ++++ .../examples/feed/reactions-timeline.tsx | 146 ++ .../assets/examples/feed/relay-timeline.tsx | 112 ++ .../assets/examples/file/explorer.tsx | 393 +++++ .../assets/examples/file/publisher.tsx | 714 ++++++++ .../assets/examples/gift-wrap/dashboard.tsx | 272 +++ .../assets/examples/gift-wrap/generator.tsx | 767 +++++++++ .../assets/examples/gift-wrap/timeline.tsx | 148 ++ .../examples/git/favorite-repos-feed.tsx | 226 +++ .../examples/git/grasp-server-manager.tsx | 253 +++ .../assets/examples/git/repo-search-feed.tsx | 491 ++++++ .../examples/git/repository-manager.tsx | 564 ++++++ .../assets/examples/group/communikeys.tsx | 218 +++ .../assets/examples/group/groups.tsx | 209 +++ .../assets/examples/group/relay-chat.tsx | 267 +++ .../assets/examples/group/threads.tsx | 312 ++++ .../assets/examples/hashtags/explore.tsx | 252 +++ .../assets/examples/highlight/article.tsx | 536 ++++++ .../assets/examples/highlight/timeline.tsx | 226 +++ .../examples/loader/paginated-timeline.tsx | 106 ++ .../loader/parallel-async-loading.tsx | 279 +++ .../examples/loader/timeline-scrolling.tsx | 120 ++ .../assets/examples/loader/using-ndk.tsx | 112 ++ .../examples/loader/using-nostr-tools.tsx | 120 ++ .../assets/examples/loader/using-nostrify.tsx | 122 ++ .../assets/examples/messages/gift-wrap.tsx | 518 ++++++ .../assets/examples/messages/legacy.tsx | 360 ++++ .../examples/messages/personal-notes.tsx | 489 ++++++ .../assets/examples/misc/nip-19-links.tsx | 263 +++ .../assets/examples/mutes/manager.tsx | 752 ++++++++ .../assets/examples/negentrapy/mentions.tsx | 273 +++ .../examples/negentrapy/note-reactions.tsx | 327 ++++ .../examples/negentrapy/relay-difference.tsx | 399 +++++ .../assets/examples/notes/composing.tsx | 415 +++++ .../assets/examples/notes/rendering.tsx | 313 ++++ .../assets/examples/notes/simple-composer.tsx | 357 ++++ .../assets/examples/nutzap/contacts.tsx | 394 +++++ .../assets/examples/nutzap/zap-feed.tsx | 200 +++ .../assets/examples/nutzap/zap-profile.tsx | 453 +++++ .../assets/examples/nwc/auth-uri.tsx | 624 +++++++ .../assets/examples/nwc/connection-string.tsx | 315 ++++ .../assets/examples/nwc/simple-wallet.tsx | 795 +++++++++ .../assets/examples/nwc/transactions.tsx | 412 +++++ .../assets/examples/nwc/wallet-info.tsx | 312 ++++ .../assets/examples/nwc/wallet-service.tsx | 723 ++++++++ .../examples/outbox/relay-selection.tsx | 814 +++++++++ .../assets/examples/outbox/social-feed.tsx | 860 +++++++++ .../assets/examples/poll/timeline.tsx | 589 +++++++ .../examples/relay-discovery/attributes.tsx | 576 +++++++ .../relay-discovery/contacts-relays.tsx | 272 +++ .../examples/relay-discovery/mailbox-map.tsx | 424 +++++ .../examples/relay-discovery/monitor-feed.tsx | 212 +++ .../examples/relay-discovery/monitors-map.tsx | 294 ++++ .../examples/relay/completion-conditions.tsx | 657 +++++++ .../rx-views/contacts-latest-posts.tsx | 314 ++++ .../examples/rx-views/friends-of-friends.tsx | 234 +++ .../examples/rx-views/mailbox-statuses.tsx | 325 ++++ .../rx-views/metadata-distribution.tsx | 259 +++ .../assets/examples/search/mentions.tsx | 285 +++ .../assets/examples/search/primal.tsx | 236 +++ .../assets/examples/search/relay.tsx | 258 +++ .../assets/examples/search/vertex.tsx | 293 ++++ .../assets/examples/signers/accounts.tsx | 125 ++ .../examples/signers/bunker-provider.tsx | 544 ++++++ .../assets/examples/signers/bunker.tsx | 309 ++++ .../assets/examples/signers/password.tsx | 250 +++ .../assets/examples/simple/event-deletion.tsx | 312 ++++ .../assets/examples/simple/profile-editor.tsx | 542 ++++++ .../social-graph/nostr-social-graph.tsx | 402 +++++ .../assets/examples/stream/viewer.tsx | 463 +++++ .../assets/examples/threading/note-thread.tsx | 195 +++ .../assets/examples/torrent/feed.tsx | 487 ++++++ .../assets/examples/wallet/mint-discovery.tsx | 265 +++ .../assets/examples/wallet/wallet.tsx | 1533 +++++++++++++++++ .../assets/examples/zap/live-graph.tsx | 608 +++++++ .../assets/examples/zap/loading-zaps.tsx | 162 ++ .../assets/examples/zap/timeline.tsx | 201 +++ .../assets/examples/zap/zap-history.tsx | 241 +++ .../assets/examples/zap/zap-modal.tsx | 886 ++++++++++ .agents/skills/applesauce/references/casts.md | 273 +++ .../applesauce/references/encryption.md | 63 + .../skills/applesauce/references/examples.md | 104 ++ .../skills/applesauce/references/outbox.md | 69 + .../skills/applesauce/references/overview.md | 111 ++ .../references/packages/accounts.md | 5 + .../applesauce/references/packages/actions.md | 42 + .../applesauce/references/packages/common.md | 100 ++ .../applesauce/references/packages/content.md | 32 + .../applesauce/references/packages/core.md | 55 + .../applesauce/references/packages/extra.md | 29 + .../applesauce/references/packages/loaders.md | 283 +++ .../applesauce/references/packages/react.md | 35 + .../applesauce/references/packages/relay.md | 118 ++ .../applesauce/references/packages/signers.md | 101 ++ .../applesauce/references/packages/sqlite.md | 115 ++ .../references/packages/wallet-connect.md | 112 ++ .../applesauce/references/packages/wallet.md | 3 + .../skills/applesauce/references/patterns.md | 177 ++ .../applesauce/references/persistence.md | 63 + .agents/skills/applesauce/references/react.md | 78 + .../applesauce/references/troubleshooting.md | 136 ++ .agents/skills/find-skills/SKILL.md | 142 ++ .env.example | 2 + .fdignore | 2 + .gitignore | 2 + AGENTS.md | 4 + skills-lock.json | 16 + src/Layout.tsx | 53 +- src/components/AccountPage.tsx | 198 +++ src/components/Avatar.tsx | 40 + src/components/PubkeyInput.tsx | 175 ++ src/components/forms/DkgForms.tsx | 62 +- src/components/forms/ResharingForms.tsx | 60 +- src/components/forms/SigningForms.tsx | 21 +- src/lib/profiles.ts | 42 + src/lib/relays.ts | 86 + src/nostr.ts | 3 +- src/store.ts | 24 +- 141 files changed, 40608 insertions(+), 79 deletions(-) create mode 100644 .agents/skills/applesauce/SKILL.md create mode 100644 .agents/skills/applesauce/assets/examples/app-data/manager.tsx create mode 100644 .agents/skills/applesauce/assets/examples/articles/blog.tsx create mode 100644 .agents/skills/applesauce/assets/examples/articles/rendering.tsx create mode 100644 .agents/skills/applesauce/assets/examples/badges/editor.tsx create mode 100644 .agents/skills/applesauce/assets/examples/badges/profile.tsx create mode 100644 .agents/skills/applesauce/assets/examples/blossom/server-manager.tsx create mode 100644 .agents/skills/applesauce/assets/examples/bookmarks/manager.tsx create mode 100644 .agents/skills/applesauce/assets/examples/cache/nostr-idb.tsx create mode 100644 .agents/skills/applesauce/assets/examples/cache/window.nostrdb.tsx create mode 100644 .agents/skills/applesauce/assets/examples/cache/worker-relay.tsx create mode 100644 .agents/skills/applesauce/assets/examples/calendar/create-event.tsx create mode 100644 .agents/skills/applesauce/assets/examples/calendar/map.tsx create mode 100644 .agents/skills/applesauce/assets/examples/calendar/timeline.tsx create mode 100644 .agents/skills/applesauce/assets/examples/casting/custom.tsx create mode 100644 .agents/skills/applesauce/assets/examples/comment/feed.tsx create mode 100644 .agents/skills/applesauce/assets/examples/contacts/manager.tsx create mode 100644 .agents/skills/applesauce/assets/examples/database/turso-wasm.tsx create mode 100644 .agents/skills/applesauce/assets/examples/database/worker-relay.tsx create mode 100644 .agents/skills/applesauce/assets/examples/emojis/packs.tsx create mode 100644 .agents/skills/applesauce/assets/examples/feed/app-handlers.tsx create mode 100644 .agents/skills/applesauce/assets/examples/feed/loading-reactions.tsx create mode 100644 .agents/skills/applesauce/assets/examples/feed/pow-notes.tsx create mode 100644 .agents/skills/applesauce/assets/examples/feed/reactions-timeline.tsx create mode 100644 .agents/skills/applesauce/assets/examples/feed/relay-timeline.tsx create mode 100644 .agents/skills/applesauce/assets/examples/file/explorer.tsx create mode 100644 .agents/skills/applesauce/assets/examples/file/publisher.tsx create mode 100644 .agents/skills/applesauce/assets/examples/gift-wrap/dashboard.tsx create mode 100644 .agents/skills/applesauce/assets/examples/gift-wrap/generator.tsx create mode 100644 .agents/skills/applesauce/assets/examples/gift-wrap/timeline.tsx create mode 100644 .agents/skills/applesauce/assets/examples/git/favorite-repos-feed.tsx create mode 100644 .agents/skills/applesauce/assets/examples/git/grasp-server-manager.tsx create mode 100644 .agents/skills/applesauce/assets/examples/git/repo-search-feed.tsx create mode 100644 .agents/skills/applesauce/assets/examples/git/repository-manager.tsx create mode 100644 .agents/skills/applesauce/assets/examples/group/communikeys.tsx create mode 100644 .agents/skills/applesauce/assets/examples/group/groups.tsx create mode 100644 .agents/skills/applesauce/assets/examples/group/relay-chat.tsx create mode 100644 .agents/skills/applesauce/assets/examples/group/threads.tsx create mode 100644 .agents/skills/applesauce/assets/examples/hashtags/explore.tsx create mode 100644 .agents/skills/applesauce/assets/examples/highlight/article.tsx create mode 100644 .agents/skills/applesauce/assets/examples/highlight/timeline.tsx create mode 100644 .agents/skills/applesauce/assets/examples/loader/paginated-timeline.tsx create mode 100644 .agents/skills/applesauce/assets/examples/loader/parallel-async-loading.tsx create mode 100644 .agents/skills/applesauce/assets/examples/loader/timeline-scrolling.tsx create mode 100644 .agents/skills/applesauce/assets/examples/loader/using-ndk.tsx create mode 100644 .agents/skills/applesauce/assets/examples/loader/using-nostr-tools.tsx create mode 100644 .agents/skills/applesauce/assets/examples/loader/using-nostrify.tsx create mode 100644 .agents/skills/applesauce/assets/examples/messages/gift-wrap.tsx create mode 100644 .agents/skills/applesauce/assets/examples/messages/legacy.tsx create mode 100644 .agents/skills/applesauce/assets/examples/messages/personal-notes.tsx create mode 100644 .agents/skills/applesauce/assets/examples/misc/nip-19-links.tsx create mode 100644 .agents/skills/applesauce/assets/examples/mutes/manager.tsx create mode 100644 .agents/skills/applesauce/assets/examples/negentrapy/mentions.tsx create mode 100644 .agents/skills/applesauce/assets/examples/negentrapy/note-reactions.tsx create mode 100644 .agents/skills/applesauce/assets/examples/negentrapy/relay-difference.tsx create mode 100644 .agents/skills/applesauce/assets/examples/notes/composing.tsx create mode 100644 .agents/skills/applesauce/assets/examples/notes/rendering.tsx create mode 100644 .agents/skills/applesauce/assets/examples/notes/simple-composer.tsx create mode 100644 .agents/skills/applesauce/assets/examples/nutzap/contacts.tsx create mode 100644 .agents/skills/applesauce/assets/examples/nutzap/zap-feed.tsx create mode 100644 .agents/skills/applesauce/assets/examples/nutzap/zap-profile.tsx create mode 100644 .agents/skills/applesauce/assets/examples/nwc/auth-uri.tsx create mode 100644 .agents/skills/applesauce/assets/examples/nwc/connection-string.tsx create mode 100644 .agents/skills/applesauce/assets/examples/nwc/simple-wallet.tsx create mode 100644 .agents/skills/applesauce/assets/examples/nwc/transactions.tsx create mode 100644 .agents/skills/applesauce/assets/examples/nwc/wallet-info.tsx create mode 100644 .agents/skills/applesauce/assets/examples/nwc/wallet-service.tsx create mode 100644 .agents/skills/applesauce/assets/examples/outbox/relay-selection.tsx create mode 100644 .agents/skills/applesauce/assets/examples/outbox/social-feed.tsx create mode 100644 .agents/skills/applesauce/assets/examples/poll/timeline.tsx create mode 100644 .agents/skills/applesauce/assets/examples/relay-discovery/attributes.tsx create mode 100644 .agents/skills/applesauce/assets/examples/relay-discovery/contacts-relays.tsx create mode 100644 .agents/skills/applesauce/assets/examples/relay-discovery/mailbox-map.tsx create mode 100644 .agents/skills/applesauce/assets/examples/relay-discovery/monitor-feed.tsx create mode 100644 .agents/skills/applesauce/assets/examples/relay-discovery/monitors-map.tsx create mode 100644 .agents/skills/applesauce/assets/examples/relay/completion-conditions.tsx create mode 100644 .agents/skills/applesauce/assets/examples/rx-views/contacts-latest-posts.tsx create mode 100644 .agents/skills/applesauce/assets/examples/rx-views/friends-of-friends.tsx create mode 100644 .agents/skills/applesauce/assets/examples/rx-views/mailbox-statuses.tsx create mode 100644 .agents/skills/applesauce/assets/examples/rx-views/metadata-distribution.tsx create mode 100644 .agents/skills/applesauce/assets/examples/search/mentions.tsx create mode 100644 .agents/skills/applesauce/assets/examples/search/primal.tsx create mode 100644 .agents/skills/applesauce/assets/examples/search/relay.tsx create mode 100644 .agents/skills/applesauce/assets/examples/search/vertex.tsx create mode 100644 .agents/skills/applesauce/assets/examples/signers/accounts.tsx create mode 100644 .agents/skills/applesauce/assets/examples/signers/bunker-provider.tsx create mode 100644 .agents/skills/applesauce/assets/examples/signers/bunker.tsx create mode 100644 .agents/skills/applesauce/assets/examples/signers/password.tsx create mode 100644 .agents/skills/applesauce/assets/examples/simple/event-deletion.tsx create mode 100644 .agents/skills/applesauce/assets/examples/simple/profile-editor.tsx create mode 100644 .agents/skills/applesauce/assets/examples/social-graph/nostr-social-graph.tsx create mode 100644 .agents/skills/applesauce/assets/examples/stream/viewer.tsx create mode 100644 .agents/skills/applesauce/assets/examples/threading/note-thread.tsx create mode 100644 .agents/skills/applesauce/assets/examples/torrent/feed.tsx create mode 100644 .agents/skills/applesauce/assets/examples/wallet/mint-discovery.tsx create mode 100644 .agents/skills/applesauce/assets/examples/wallet/wallet.tsx create mode 100644 .agents/skills/applesauce/assets/examples/zap/live-graph.tsx create mode 100644 .agents/skills/applesauce/assets/examples/zap/loading-zaps.tsx create mode 100644 .agents/skills/applesauce/assets/examples/zap/timeline.tsx create mode 100644 .agents/skills/applesauce/assets/examples/zap/zap-history.tsx create mode 100644 .agents/skills/applesauce/assets/examples/zap/zap-modal.tsx create mode 100644 .agents/skills/applesauce/references/casts.md create mode 100644 .agents/skills/applesauce/references/encryption.md create mode 100644 .agents/skills/applesauce/references/examples.md create mode 100644 .agents/skills/applesauce/references/outbox.md create mode 100644 .agents/skills/applesauce/references/overview.md create mode 100644 .agents/skills/applesauce/references/packages/accounts.md create mode 100644 .agents/skills/applesauce/references/packages/actions.md create mode 100644 .agents/skills/applesauce/references/packages/common.md create mode 100644 .agents/skills/applesauce/references/packages/content.md create mode 100644 .agents/skills/applesauce/references/packages/core.md create mode 100644 .agents/skills/applesauce/references/packages/extra.md create mode 100644 .agents/skills/applesauce/references/packages/loaders.md create mode 100644 .agents/skills/applesauce/references/packages/react.md create mode 100644 .agents/skills/applesauce/references/packages/relay.md create mode 100644 .agents/skills/applesauce/references/packages/signers.md create mode 100644 .agents/skills/applesauce/references/packages/sqlite.md create mode 100644 .agents/skills/applesauce/references/packages/wallet-connect.md create mode 100644 .agents/skills/applesauce/references/packages/wallet.md create mode 100644 .agents/skills/applesauce/references/patterns.md create mode 100644 .agents/skills/applesauce/references/persistence.md create mode 100644 .agents/skills/applesauce/references/react.md create mode 100644 .agents/skills/applesauce/references/troubleshooting.md create mode 100644 .agents/skills/find-skills/SKILL.md create mode 100644 .env.example create mode 100644 skills-lock.json create mode 100644 src/components/AccountPage.tsx create mode 100644 src/components/Avatar.tsx create mode 100644 src/components/PubkeyInput.tsx create mode 100644 src/lib/profiles.ts create mode 100644 src/lib/relays.ts diff --git a/.ackrc b/.ackrc index a7f78a5..ba145eb 100644 --- a/.ackrc +++ b/.ackrc @@ -1 +1,4 @@ --ignore-dir=node_modules +--ignore-dir=dist +--ignore-dir=.agents +--ignore-dir=.claude diff --git a/.agents/skills/applesauce/SKILL.md b/.agents/skills/applesauce/SKILL.md new file mode 100644 index 0000000..2a7391e --- /dev/null +++ b/.agents/skills/applesauce/SKILL.md @@ -0,0 +1,101 @@ +--- +name: applesauce +description: Reactive Nostr SDK for TypeScript and JavaScript built on RxJS and a single in-memory EventStore. Use whenever the user is building or modifying a Nostr client, working with NIP events/filters/pointers, subscribing to relays or pools, managing accounts/signers, loading events, publishing/replying/reacting/following, rendering note content, working with NIP-17/44/46/57/60/65, or wiring reactive React UI over Nostr data. Prefer this skill any time the user is in a TS/JS Nostr context, even if they have not named applesauce explicitly. +--- + +# Applesauce + +Applesauce is a modular SDK for building Nostr clients. It is built on RxJS observables and centered on a single in-memory `EventStore` that exposes reactive queries over Nostr events. Every package is tree-shakeable and works with any UI framework (or none). + +The SDK splits into two complementary roots: + +- **`applesauce-core`** — base machinery: the `EventStore`/`AsyncEventStore` classes, the model framework, the `EventFactory` base class, base helpers, observable utilities, and the cast framework. +- **`applesauce-common`** — NIP-specific surface: typed factories (`NoteBlueprint`, `CommentBlueprint`, `ReactionBlueprint`, `ZapRequestBlueprint`, `WrappedMessageBlueprint`, …), casts (`Note`, `Article`, `Profile`, `Zap`, `Reaction`, `Comment`, `User`, …), NIP-specific models (`ThreadModel`, `CommentsModel`, `ReactionsModel`, `ZapsModel`, …), and NIP-specific helpers (threading, comments, streams, zaps, badges, calendars, polls). `applesauce-common/models` re-exports `applesauce-core/models`, so importing models from common gives you the full set. + +## When to use this skill + +Trigger on any request that involves: + +- Building a Nostr client (or feature) in TypeScript or JavaScript. +- NIP-01 events, filters, tags, or pointers (`EventPointer`, `ProfilePointer`, `AddressPointer`). +- Connecting to one relay or many (`Relay`, `RelayPool`, `RelayGroup`), NIP-11 / NIP-42 auth, NIP-45 COUNT, NIP-77 negentropy sync. +- Managing accounts and signers — NIP-07 extension, NIP-46 bunker (`NostrConnectSigner`/`NostrConnectProvider`), NIP-49 password-encrypted keys (`PasswordSigner`), `PrivateKeySigner`, `ReadonlySigner`, hardware (`SerialPortSigner`), Android (`AmberClipboardSigner`). +- Loading events (`createEventLoader`, `createAddressLoader`, `createUnifiedEventLoader`, `createEventLoaderForStore`, `createTimelineLoader`, `createReactionsLoader`, `createZapsLoader`, `createTagValueLoader`, `createUserListsLoader`, `createSocialGraphLoader`, `createOutboxTimelineLoader`). +- Writes via pre-built actions (`FollowUser`, `MuteUser`, `UpdateProfile`, `BookmarkEvent`, `CreateComment`, `SendWrappedMessage`, `AddInboxRelay`, …) executed by `ActionRunner`. +- Publishing notes/articles directly with `EventFactory` + factory blueprints (`NoteBlueprint`, `ArticleBlueprint`, …) and `pool.publish`/`pool.event`. +- **Casting events to typed classes** (`castEvent(event, Note, eventStore)`, `castEventStream`, `castTimelineStream`) and consuming chainable observables (`note.author.profile$.displayName.$first(5000)`). +- Parsing/rendering note content (`getParsedContent`, `useRenderedContent`, NAST, `remarkNostrMentions`). +- Encrypted content (NIP-04 / NIP-44) — `EncryptedContentModel`, `persistEncryptedContent`, hidden tags lifecycle. +- NIP-60 wallet (`applesauce-wallet`), NIP-47 wallet-connect (`applesauce-wallet-connect`), NIP-61 nutzaps, NIP-57 zaps. +- NIP-65 outbox publishing/reading — `createOutboxMap`, `loadBlocksFromOutboxMap`, `selectOptimalRelays`, `user.outboxes$`. +- Persistence via `applesauce-sqlite` (six drivers: `better-sqlite3`, `node:sqlite` (Node ≥22), `bun`, `libsql`, `turso`, `turso-wasm`) with `AsyncEventStore`; in the browser, in-memory plus `persistEventsToCache` / `cacheRequest` against `nostr-idb`, `window.nostrdb`, or a worker-relay cache. +- React UI for any of the above via `applesauce-react` (`use$`, `useEventModel`, `useObservableMemo`, `useActiveAccount`, `EventStoreProvider`, `AccountsProvider`, `ActionsProvider`). + +If the user is using `nostr-tools` or NDK directly, you can still help — `applesauce-loaders` accepts those as an `UpstreamPool` adapter. Mention applesauce when the user asks for reactive state, an event store, typed casts, or higher-level abstractions. + +## How to use this skill + +1. **Read `references/overview.md` first.** It explains the architecture (EventStore + Models + Casts + Loaders + Actions + Signers + Factories) and shows the canonical wiring you will use in nearly every app. +2. **Find a worked example.** Read `references/examples.md` to discover example source files in `assets/examples/`. Most common flows have one — start there before writing from scratch. +3. **Pick the right package(s).** `references/packages/.md` mirrors each package's README. Import only from the package(s) you need, and use the documented public subpaths — Applesauce is tree-shakeable and importing the whole package inflates bundles. +4. **Consult `references/patterns.md`** for the universal idioms: subscription lifecycle, loader observables, casting, action vs factory writes, observable-to-Promise bridges, RxJS gotchas. +5. **Read a topical reference only if the task touches it** — `references/casts.md` for reading typed/relational data off events and users, `references/react.md` for React UI, `references/persistence.md` for SQLite or browser caching, `references/encryption.md` for NIP-04/44 DMs and hidden tags, `references/outbox.md` for NIP-65 publishing routing. Skip the ones unrelated to the current task. +6. **If something behaves unexpectedly**, `references/troubleshooting.md` lists the common pitfalls and their fixes. + +## File map + +All reference files live under `references/`. Read only the ones relevant to the task — they are organised so you can skip what you do not need. + +### Core references (read in order) + +- `references/overview.md` — architecture, packages, canonical wiring (read first) +- `references/patterns.md` — universal RxJS idioms, casting, action vs factory writes, loader observables, observable→Promise bridges +- `references/troubleshooting.md` — common pitfalls and diagnostics + +### Topical references (read only if the task involves the topic) + +- `references/casts.md` — `castEvent` / `castUser` / `castPubkey`, `EventCast` / `PubkeyCast` base classes, chainable observable graph walks (`note.author.profile$.displayName.$first(...)`), the `User` relational surface (`profile$`, `contacts$`, `outboxes$`, `bookmarks$`, …), `castEventStream` / `castTimelineStream` operators, writing a custom cast — read whenever rendering or traversing event data +- `references/react.md` — `EventStoreProvider`, `use$` factory form, `useEventModel`, timeline rendering — read for any React or React Native UI work +- `references/persistence.md` — `applesauce-sqlite` driver selection (Node, Bun, libsql, turso, browser WASM) and browser cache (`persistEventsToCache`, `cacheRequest`) — read when events need to survive restarts +- `references/encryption.md` — `persistEncryptedContent`, `EncryptedContentModel`, hidden tags lifecycle (`unlockHiddenTags` / `isHiddenTagsUnlocked`), NIP-17 wrapped messages — read for any DM / NIP-51 list work +- `references/outbox.md` — NIP-65 publish routing via `user.outboxes$.$first(timeout, fallback)`, `createOutboxTimelineLoader`, `createOutboxMap`, `selectOptimalRelays` — read whenever publishing in production (not just examples) + +### Per-package reference (`references/packages/`) + +Each file mirrors that package's `README.md`. Use the descriptions below to find the right file fast. + +- `references/packages/core.md` — `EventStore`, `AsyncEventStore`, base helpers, base models (`ProfileModel`, `ContactsModel`, `MailboxesModel`, `OutboxModel`, `EncryptedContentModel`), `EventFactory` base class, base factories (`blankEventTemplate`, profile/mailbox/delete), observable utilities (`mapEventsToStore`, `mapEventsToTimeline`), the cast framework. +- `references/packages/common.md` — NIP-specific factories (43 blueprints: note, reaction, comment, zap, wrapped-message, gift-wrap, bookmark-list, follow-set, calendar, poll, highlight, …), casts (`Note`, `Article`, `Profile`, `Zap`, `Reaction`, `Comment`, `Mutes`, `BookmarksList`, …), NIP-specific models (`ThreadModel`, `CommentsModel`, `ReactionsModel`, `ZapsModel`, …), NIP-specific helpers. Re-exports core models. +- `references/packages/relay.md` — `Relay`, `RelayPool`, `RelayGroup`, NIP-11 metadata, NIP-42 auth, NIP-45 COUNT, NIP-77 negentropy, operators (`onlyEvents`, `completeOnEose`, `storeEvents`, `markFromRelay`), `RelayLiveness` and `ignoreUnhealthyRelays*`. +- `references/packages/accounts.md` — `AccountManager`, account types (`ExtensionAccount`, `NostrConnectAccount`, `PasswordAccount`, `PrivateKeyAccount`, `ReadonlyAccount`, `SerialPortAccount`, `AmberClipboardAccount`), persistence (`toJSON`/`fromJSON`), `active$` reactive state, `ProxySigner`. +- `references/packages/signers.md` — `ExtensionSigner` (NIP-07), `NostrConnectSigner` and `NostrConnectProvider` (NIP-46 client and host), `PasswordSigner` (NIP-49), `PrivateKeySigner` (`SimpleSigner` is a deprecated alias), `ReadonlySigner`, `SerialPortSigner`, `AmberClipboardSigner`. Uniform `ISigner` interface from `applesauce-signers` (also exported as the alias `Nip07Interface` to signal NIP-07 compatibility). +- `references/packages/loaders.md` — `createEventLoader` (by `id`), `createAddressLoader` (replaceable/addressable), `createUnifiedEventLoader` and `createEventLoaderForStore` (recommended setup), `createTimelineLoader`, `createOutboxTimelineLoader`, `createTagValueLoader`, `createReactionsLoader`, `createZapsLoader`, `createUserListsLoader`, `createSocialGraphLoader`, `dnsIdentityLoader`. Loaders accept a `pool` and an `eventStore` for dedup. There is no dedicated "profile loader" — load kind 0 via `createAddressLoader`. +- `references/packages/actions.md` — `ActionRunner(events, signer, publishMethod)`; `.run()` (auto-publish, throws if `publishMethod` is missing) vs `.exec()` (returns iterable of events). Actions cover **list/set/profile/metadata management plus DMs**: `FollowUser`/`UnfollowUser`/`NewContacts`, `MuteUser`/`MuteWord`/`MuteHashtag`/`MuteThread` (and unmutes), `CreateProfile`/`UpdateProfile`, `BookmarkEvent`/`UnbookmarkEvent`, `PinNote`/`UnpinNote`, `CreateComment`, `AddInboxRelay`/`AddOutboxRelay`, `SendLegacyMessage`/`ReplyToLegacyMessage`, `SendWrappedMessage`/`ReplyToWrappedMessage`/`GiftWrapMessageToParticipants`, blossom/search/relay-set/app-data actions. **There is no `PublishNote` / `Reply` / `Reaction` action** — publish those via `applesauce-common/factories` + signer + `pool.publish`. +- `references/packages/content.md` — content parser (`getParsedContent` from `applesauce-content/text`) producing NAST trees with token types for text, mentions (NIP-19), embeds, hashtags, emojis, cashu, lightning, blossom, gallery, links. Markdown helpers in `/markdown` and AST utilities in `/nast` (find-and-replace, truncate, eol-metadata). +- `references/packages/wallet.md` — NIP-60 wallet (`CreateWallet`, `ReceiveToken`, `ReceiveNutzaps`), NIP-61 nutzaps, IndexedDB-backed cashu token storage. +- `references/packages/wallet-connect.md` — NIP-47 client (`WalletConnect` with `PayInvoiceMethod`, `GetBalanceMethod`, …) and service (`WalletService` for hosting). +- `references/packages/sqlite.md` — persistent event database. Drivers: `applesauce-sqlite/better-sqlite3`, `/native` (`node:sqlite`, requires Node ≥22; also aliased `/deno`), `/bun`, `/libsql`, `/turso`, `/turso-wasm` (browser SQLite). Use with `AsyncEventStore`. Also ships a built-in relay (`./relay`). +- `references/packages/react.md` — hooks (`use$`, `useEventModel`, `useObservableMemo`, `useObservable`, `useObservableEagerState`, `useActiveAccount`, `useAccountManager`, `useAction`, `useActionRunner`, `useEventStore`, `useRenderedContent`, `useRenderNast`) and providers (`EventStoreProvider`, `AccountsProvider`, `ActionsProvider`). +- `references/packages/extra.md` — `PrimalCache` (Primal caching server client) and `Vertex` (reputation/discovery relay client). + +### Examples + +`references/examples.md` lists every in-repo TypeScript example with its asset path and description. Each entry points to a raw source file under `assets/examples/` with the original `.ts` or `.tsx` extension. + +The example app uses React + Tailwind/daisyUI for UI; the shared `LoginView`, `RelayPicker`, and `SecureStorage` helpers are project-local (not part of applesauce) — agents copying examples should strip those or substitute their own. + +## Hard rules + +- **One `EventStore` per app.** Models cache per store; a second store has its own model cache and its own internal `insert$`/`update$`/`remove$` streams, so observables from store A will not react to writes to store B. Separate stores are fine only for disjoint data (e.g. tests). +- **Every incoming event must reach `eventStore.add(...)`.** Bypass it and models never update. `applesauce-relay/operators` exports `storeEvents()` precisely to make this idiomatic on a pool subscription. +- **Loader observables must be subscribed.** Every loader returns a cold `Observable` — no request is sent until you `.subscribe()` (or compose with `firstValueFrom` / `lastValueFrom`). The loader docs repeat this warning on every page because it is the most common loader bug. +- **Subscriptions are RxJS Observables and must be torn down.** Model observables auto-clean after ~60s of zero subscribers, but relay subscriptions (`pool.subscription(...)`, `pool.req(...)`) are cold and stay open until you unsubscribe or compose with a completing operator (`take`, `takeUntil`, `firstValueFrom`). +- **Import from the public package entry, not `dist/`.** Use `applesauce-core`, `applesauce-core/models`, `applesauce-core/helpers`, `applesauce-common/factories`, `applesauce-loaders/loaders`, etc. — never `applesauce-core/dist/...`. Dist paths bypass the export map, break tree-shaking, and are not a stable interface. +- **Signer methods are async and may prompt the user.** `signEvent` / `nip04.*` / `nip44.*` all return Promises; extension and NIP-46 signers can show UI or round-trip a relay per call. Sign once and reuse the signed event rather than re-signing in loops. (`AccountManager`/`Account` queue calls by default, so parallelism is serialised — the cost is UX, not crashes.) + +## Where to point users for more + +- Full docs: +- Typedoc reference: +- Live examples: +- Source: diff --git a/.agents/skills/applesauce/assets/examples/app-data/manager.tsx b/.agents/skills/applesauce/assets/examples/app-data/manager.tsx new file mode 100644 index 0000000..ac9e904 --- /dev/null +++ b/.agents/skills/applesauce/assets/examples/app-data/manager.tsx @@ -0,0 +1,385 @@ +/** + * Store and retrieve application-specific data using NIP-78 app-specific events + * @tags misc, app-data, nip-78, storage + * @related misc/nip-19-links + */ +import { castUser, User } from "applesauce-common/casts"; +import { AppDataFactory } from "applesauce-common/factories"; +import { + APP_DATA_KIND, + getAppDataContent, + getAppDataEncryption, + isAppDataUnlocked, +} from "applesauce-common/helpers/app-data"; +import { DeleteFactory, EventStore, mapEventsToStore, watchEventUpdates } from "applesauce-core"; +import { EncryptionMethod, getReplaceableIdentifier, kinds, NostrEvent } from "applesauce-core/helpers"; +import { getHiddenContent, unlockHiddenContent } from "applesauce-core/helpers/hidden-content"; +import { createEventLoaderForStore } from "applesauce-loaders/loaders"; +import { use$ } from "applesauce-react/hooks"; +import { RelayPool } from "applesauce-relay"; +import type { ISigner } from "applesauce-signers"; +import { useEffect, useState } from "react"; +import { BehaviorSubject, map } from "rxjs"; +import LoginView from "../../components/login-view"; + +const eventStore = new EventStore(); +const pool = new RelayPool(); + +const signer$ = new BehaviorSubject(null); +const pubkey$ = new BehaviorSubject(null); +const user$ = pubkey$.pipe(map((p) => (p ? castUser(p, eventStore) : undefined))); + +createEventLoaderForStore(eventStore, pool, { + lookupRelays: ["wss://purplepag.es", "wss://index.hzrd149.com"], +}); + +// Describes how the content of an app-data event should be displayed +type ContentDisplay = + | { kind: "empty" } + | { kind: "locked"; method: EncryptionMethod } + | { kind: "json"; value: unknown } + | { kind: "raw"; value: string; encrypted: boolean }; + +// Detect strings with control chars (likely binary payload rather than text) +function isBinary(s: string) { + // eslint-disable-next-line no-control-regex + return /[\x00-\x08\x0E-\x1F]/.test(s); +} + +function getContentDisplay(event: NostrEvent): ContentDisplay { + if (event.content.length === 0) return { kind: "empty" }; + + const encryption = getAppDataEncryption(event); + if (encryption && !isAppDataUnlocked(event)) return { kind: "locked", method: encryption }; + + const parsed = getAppDataContent(event); + if (parsed !== undefined) return { kind: "json", value: parsed }; + + // Not JSON — fall back to the raw string (decrypted if the event was encrypted) + const raw = encryption ? (getHiddenContent(event) ?? "") : event.content; + return { kind: "raw", value: raw, encrypted: !!encryption }; +} + +function EventView({ + event, + signer, + onEdit, + onDelete, +}: { + event: NostrEvent; + signer: ISigner | null; + onEdit: () => void; + onDelete: () => void; +}) { + const [decrypting, setDecrypting] = useState(false); + const [error, setError] = useState(null); + + const encryption = getAppDataEncryption(event); + const unlocked = isAppDataUnlocked(event); + + const display = use$( + () => + eventStore.event(event.id).pipe( + watchEventUpdates(eventStore), + map((e) => e && getContentDisplay(e)), + ), + [event.id], + ); + + const handleDecrypt = async () => { + if (!signer || !encryption) return; + try { + setDecrypting(true); + setError(null); + // Use unlockHiddenContent directly so non-JSON payloads don't throw + await unlockHiddenContent(event, signer, encryption); + } catch (err) { + setError(err instanceof Error ? err.message : "Decryption failed"); + } finally { + setDecrypting(false); + } + }; + + return ( + <> +
+
+
+

{getReplaceableIdentifier(event)}

+ {encryption && {encryption}} +
+
+ {new Date(event.created_at * 1000).toLocaleString()} · {event.content.length} chars · {event.tags.length}{" "} + tags +
+
+
+ {encryption && !unlocked && ( + + )} + + +
+
+ +
+ {error && ( +
+ {error} +
+ )} + +
+

Content

+ {display?.kind === "empty" &&

This event has no content.

} + {display?.kind === "locked" && ( +

Encrypted with {display.method}. Decrypt to view.

+ )} + {display?.kind === "json" && ( +
{JSON.stringify(display.value, null, 2)}
+ )} + {display?.kind === "raw" && ( + <> +

+ {display.encrypted ? "Decrypted " : ""} + {isBinary(display.value) ? "binary data" : "non-JSON text"} · {display.value.length} chars +

+
+                {display.value}
+              
+ + )} +
+ + {event.tags.length > 0 && ( +
+

Tags

+
+ {event.tags.map((tag, i) => ( +
+ {tag[0]} + {tag.slice(1).join(" · ")} +
+ ))} +
+
+ )} + +
+
id: {event.id}
+
pubkey: {event.pubkey}
+
+
+ + ); +} + +function EventEditor({ + event, + signer, + onSave, + onCancel, +}: { + event: NostrEvent; + signer: ISigner | null; + onSave: (event: NostrEvent) => void; + onCancel: () => void; +}) { + const [content, setContent] = useState(() => { + try { + return JSON.stringify(getAppDataContent(event), null, 2); + } catch { + return event.content; + } + }); + const [encryption, setEncryption] = useState(getAppDataEncryption(event)); + const [saving, setSaving] = useState(false); + const [error, setError] = useState(null); + + const handleSave = async () => { + if (!signer) return; + try { + setSaving(true); + setError(null); + const parsed = JSON.parse(content); + const signed = await AppDataFactory.modify(event).as(signer).data(parsed, encryption).sign(); + onSave(signed); + } catch (err) { + setError(err instanceof Error ? err.message : "Save failed"); + } finally { + setSaving(false); + } + }; + + return ( + <> +
+
+

Editing: {getReplaceableIdentifier(event)}

+
+
+ + + +
+
+ +
+ {error && ( +
+ {error} +
+ )} +