Files
amethyst/.claude/CLAUDE.md
T
Claude 8209f4416a docs: fix stale claims in Claude skills against current code
Audit pass that verified every concrete claim in .claude/ against the
repository:

- account-state: Account.kt no longer exposes followListFlow-style
  StateFlows; document the state-object pattern (kind3FollowList,
  muteList, bookmarkState, ... each exposing .flow) and rewrite the
  catalog reference from the real Account.kt
- feed-patterns: filter bases (FeedFilter, AdditiveFeedFilter,
  ChangesFlowFilter, FeedContentState) moved to commons/ui/feeds;
  ui/dal keeps AdditiveComplexFeedFilter/FilterByListParams plus
  back-compat typealiases; fix recipe example signatures
- relay-client: add nip17Dm/, eoseManagers and subscriptions entries
  to the layout tree
- gradle-expert: 4-module claim -> 10 modules; refresh compose/kotlin/
  BOM versions; rewrite dependency graph with verified edges for cli,
  geode, quic, nestsClient, quic-interop, benchmark
- desktop-expert: drop drifted Main.kt line numbers; sidebar is the
  custom MainSidebar in DeckSidebar.kt, not a NavigationRail in
  SinglePaneLayout.kt
- android-expert: compileSdk/targetSdk 36 -> 37, versionName via
  generateVersionName()
- kotlin-expert: remove reference to nonexistent commit 258c4e011
- CLAUDE.md: add missing geode/benchmark/quic-interop modules
- desktop-run: packageRpm + correct binaries output path; extract.md:
  drop duplicated find clause
- session-start.sh: /home/user/Amber fallback was a copy-paste from
  another repo; fall back to CLAUDE_PROJECT_DIR

https://claude.ai/code/session_01EC7LdXjatFTh1CJSP4qKRn
2026-06-10 22:01:29 +00:00

12 KiB

Amethyst

Project Overview

Amethyst is a Nostr Client for Android that was made for Android-only and has been slowly switching over to a Kotlin Multiplatform project. The main modules are: quartz, commons, amethyst, desktopApp, cli, plus the audio-rooms transport stack quic + nestsClient. Quartz should contain implementations of Nostr specifications and utilities to help implement them. Commons stores shared code between Amethyst Android (amethyst) and Amethyst Desktop (desktopApp). The Desktop App is designed to be mouse first and so uses a completely different screen and navigation architecture while sharing the back end components with the android counterpart. cli ships amy, a non-interactive JVM command-line client that drives the same quartz + commons code — used by humans, agents, and interop tests. quic is a from-scratch pure-Kotlin QUIC v1 + HTTP/3 + WebTransport client (no JNI, no BouncyCastle), built because no Android-compatible Java QUIC library exists. geode is a standalone JVM Nostr relay (Ktor) built on quartz's relay-server code; smaller modules are benchmark (Android macrobenchmarks) and quic-interop (QUIC interop runner, lives at quic/interop). nestsClient runs the audio-room protocol on top of :quic for the NIP-53 audio-rooms feature. It implements both IETF draft-ietf-moq-transport-17 (under moq/) and moq-lite Lite-03 (kixelated's variant, under moq/lite/); the production listener AND speaker paths both run on moq-lite to interop with the nostrnests reference relay. The IETF code is kept as a reference + unit-test implementation for any future IETF target; see nestsClient/plans/2026-04-26-moq-lite-gap.md.

Canonical NIP specs live at https://github.com/nostr-protocol/nips — use /nip <number> to pull a specific one (it fetches the spec file directly).

Verify, Don't Guess

Don't assert a diagnosis you haven't reproduced. This repo gives you cheap verification tools: ./gradlew test, per-module test suites, and the amy CLI (built partly to drive quartz/commons for interop checks). If a claim is checkable in under a minute, check it before stating it — write the failing case first, watch it fail, then explain.

Architecture

amethyst/
├── quartz/         # Nostr KMP library (protocol only, no UI)
│   └── src/
│       ├── commonMain/    # Shared Nostr protocol, data models
│       ├── androidMain/   # Android-specific (crypto, storage)
│       ├── jvmMain/       # Desktop JVM-specific
│       └── iosMain/       # iOS-specific
├── commons/        # Shared UI components (convert to KMP)
│   └── src/
│       ├── commonMain/    # Shared composables, icons, state
│       ├── androidMain/   # Android-specific UI utilities
│       └── jvmMain/       # Desktop-specific UI utilities
├── quic/           # Pure-Kotlin QUIC v1 + HTTP/3 + WebTransport (audio-rooms transport)
│   └── src/
│       ├── commonMain/    # Protocol, frame/packet codecs, TLS state machine
│       ├── jvmAndroid/    # JCA-backed AEAD + UDP socket actuals
│       └── commonTest/    # RFC vector + adversarial tests
├── nestsClient/    # Audio-room client (production runs on moq-lite; IETF MoQ kept as reference)
│   └── src/
│       ├── commonMain/    # MoQ session, NestsListener, audio glue
│       └── jvmAndroid/    # Opus encode/decode, AudioRecord/AudioTrack
├── geode/          # Standalone JVM Nostr relay (Ktor) on quartz's relay-server code
├── desktopApp/     # Desktop JVM application (layouts, navigation)
├── amethyst/       # Android app (layouts, navigation)
└── cli/            # Amy — non-interactive CLI (JVM only, no Compose)

Sharing Philosophy:

  • quartz/ = Nostr business logic, protocol, data (no UI)
  • commons/ = Shared code for every front end (Android, Desktop, iOS, and the headless cli): domain models, state holders, ViewModels, the relay client, shared services, and the Compose UI that ≥1 GUI front end renders. The package taxonomy, the CLI-safe / UI boundary, and a "where does my code go?" guide are documented in commons/ARCHITECTURE.md — read it before adding a new package or dropping code into commons.
  • quic/ = Transport library (QUIC + HTTP/3 + WebTransport); reusable for any KMP project that needs MoQ. Has no Android-framework dependencies.
  • nestsClient/ = MoQ + audio-rooms client; takes :quic as transport, Quartz for crypto, MediaCodec / AudioRecord / AudioTrack for audio.
  • amethyst/ & desktopApp/ = Platform-native layouts and navigation
  • cli/ = Thin assembly layer over quartz/ + commons/ (no new logic allowed)

Plans per module: design docs for new subsystems live in the owning module's plans/YYYY-MM-DD-<slug>.md (e.g. cli/plans/, commons/plans/). The global docs/plans/ folder is frozen — don't add new plans there.

Tech Stack

Exact versions live in gradle/libs.versions.toml (the source of truth — check there rather than trusting a number copied here).

Layer Technology
Core Quartz (Nostr KMP)
UI Compose Multiplatform
Async kotlinx.coroutines + Flow
Network OkHttp (JVM)
Serialization Jackson
DI Manual / Koin
Build Gradle + Kotlin Multiplatform

Skills

The full list of available skills (with descriptions and triggers) is injected into every session, so it isn't duplicated here. Two kinds exist and are meant to be used together:

  • Codebase-oriented skills (nostr-expert, compose-expert, feed-patterns, account-state, amy-expert, …) answer "where is X in Amethyst, what pattern do we use here."
  • Technique-oriented skills (vendored from chrisbanes/skills, e.g. compose-slot-api-pattern, kotlin-flow-state-event-modeling) answer "what is the correct Compose/Kotlin design." They complement, not replace, the codebase skills: compose-expert tells you where shared composables live; compose-slot-api-pattern tells you how to shape their public API.

Feature Workflow

CRITICAL: Check existing implementations first — most logic already exists. Before writing code, survey all modules (use Grep/Explore) for managers, caches, state systems, filters, ViewModels, and composables that already do the job. Your job is usually to reuse (quartz protocol/business logic), extract (Android UI/ViewModels → commons), and add platform-specific layouts/nav — not to duplicate existing managers, caches, or state.

Summarize the survey in your plan: for each component, note whether it's reused as-is, extracted from amethyst/ to commons/, genuinely new (platform-specific only), or a duplicate of an existing pattern to avoid.

Share vs keep platform-native:

  • Sharequartz/commonMain/ (business logic, data models, protocol) and commons/commonMain/ (major UI components, ViewModels under viewmodels/, icons). ViewModels are platform-agnostic state + logic (StateFlow/SharedFlow), so they belong in commons.
  • Keep native → screen composables/scaffolding (Desktop Window vs Android Activity), navigation (sidebar vs bottom nav), platform interactions (gestures, keyboard shortcuts), system integrations (notifications, file pickers).

When extracting a composable: move it to commons/commonMain/ (see /compose-expert), add expect/actual for any platform behavior (see /kotlin-multiplatform), then point both Android and Desktop at the shared version. quartz/ is protocol-only — no composables.

Build Commands

# Run desktop app
./gradlew :desktopApp:run

# Run Android app
./gradlew :amethyst:installDebug

# Build Quartz for all targets
./gradlew :quartz:build

# Run tests
./gradlew test

# Format code
./gradlew spotlessApply

Dependency Licensing

MANDATORY whenever you introduce a new third-party dependency — in any module (quartz, commons, amethyst, desktopApp, cli, quic, nestsClient, …), whether you add it to gradle/libs.versions.toml or to a module's build.gradle.kts: determine its license before wiring it in. Amethyst ships under the MIT license, so a copyleft dependency linked into a distributed artifact (APK, desktop binary) can force that artifact's combined-work terms onto the whole project.

Verify against the dependency's actual LICENSE/COPYING file or its published POM — not from memory. Then classify and act:

  • Permissive (MIT, Apache-2.0, BSD, ISC, MPL-2.0, zlib, …) → OK, proceed.
  • LGPL, or GPL/EPL with a linking / Classpath exceptionWARN. Acceptable to link (the exception keeps our own code MIT), but call it out in your summary so the human knows. Confirm the exception actually exists in the LICENSE text — don't assume it does.
  • Stricter than LGPL — GPL/AGPL without a linking exception, SSPL, proprietary/commercial-only, or anything where the linking-exception check is "no" → STRONGLY WARN and STOP. Do not add it silently. Surface it prominently and require an explicit call-out in the PR description so a maintainer makes the decision. Prefer a permissive alternative, a clean-room implementation, or dropping the feature.

For any GPL-family hit the decisive question is always "is there a linking (LGPL/Classpath) exception?" — that is what separates a WARN from a STOP. (Example: TarsosDSP, GPLv3 with no exception, was removed from amethyst and replaced with an in-house pitch shifter for exactly this reason.)

Quartz KMP Structure

Quartz uses expect/actual for platform-specific implementations (e.g. crypto backed by secp256k1-kmp-jni-android on Android and secp256k1-kmp-jni-jvm on JVM). See /kotlin-multiplatform for the expect/actual and source-set patterns.

Icons

The Material Symbols font bundled at commons/src/commonMain/composeResources/font/material_symbols_outlined.ttf is a subset that only contains the glyphs referenced from commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons/symbols/MaterialSymbols.kt.

MANDATORY: Whenever you add a new icon — i.e. introduce a MaterialSymbol("\uXXXX") codepoint that wasn't already referenced anywhere in MaterialSymbols.kt — you MUST regenerate the subset font by running:

./tools/material-symbols-subset/subset.sh

Commit the regenerated material_symbols_outlined.ttf alongside your MaterialSymbols.kt change. Without this step the new icon renders as tofu (□) at runtime because the glyph is not in the bundled font.

Reusing a codepoint already present in MaterialSymbols.kt does NOT require regenerating. See tools/material-symbols-subset/README.md for details and prerequisites (pip install fonttools brotli).

Code Formatting

After completing any task that modifies Kotlin files, always run:

./gradlew spotlessApply

Do this before considering the task complete.

Kotlin Style

  • Never write fully-qualified class names inline in function bodies. Add an import for the class and reference it by its simple name. Write Event (with import com.vitorpamplona.quartz...Event), not com.vitorpamplona.quartz...Event in the middle of code.
  • The only acceptable inline fully-qualified names are: a genuine name collision (prefer import ... as Alias instead), or where the language requires it. Comments, KDoc, and string literals are exempt.

Navigation Shell

  • Desktop: Sidebar + main content area
  • Android: Bottom navigation

Git Workflow

  • Commits: Conventional commits (feat:, fix:, etc.)
  • Never use --no-verify