diff --git a/.agents/skills/applesauce/SKILL.md b/.agents/skills/applesauce/SKILL.md deleted file mode 100644 index 2a7391e..0000000 --- a/.agents/skills/applesauce/SKILL.md +++ /dev/null @@ -1,101 +0,0 @@ ---- -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 deleted file mode 100644 index ac9e904..0000000 --- a/.agents/skills/applesauce/assets/examples/app-data/manager.tsx +++ /dev/null @@ -1,385 +0,0 @@ -/** - * 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} -
- )} -