Skip to content
amy-expert logo

Amy CLI Expert

amy-expert

Patterns for extending `amy`, the Amethyst CLI in `cli/`. Use when adding an `amy <verb>` command, touching files under `cli/src/main/kotlin/…/cli/`, wiring a new subcommand into `Main.kt`, writing an interop test script that drives Amy, or extracting logic out of `amethyst/` into `commons/` so a...

vitorpamplona/amethyst0installs1.6Kstars

SKILL.md

Full skill instructions

Amy CLI Expert

Practical patterns for touching the cli/ module without breaking its public contract.

When to use this skill

  • Adding a new amy <verb> subcommand.
  • Editing anything under cli/​src/​main/​kotlin/…/​cli/.
  • Writing a shell script or test harness that drives Amy.
  • Extracting code out of amethyst/ so the CLI can call it (this is the single most common reason an Amy feature request stalls).
  • Deciding whether a piece of logic belongs in cli/ vs commons/ vs quartz/ (answer: almost never cli/).

Not for: general Nostr protocol work (nostr-expert), general Kotlin (kotlin-expert), Compose UI (compose-expert), Android-only flows (android-expert), gradle/​build (gradle-expert).

The rules that matter

Amy has a small number of hard rules. Any change that breaks them is a breaking change to the CLI's public API, and breaks the interop- test harnesses that depend on it.

Rule 1 — cli/ is a thin assembly layer

No new Nostr protocol, filter assembly, state machines, or encryption lives in cli/. Ever. If you need logic that doesn't exist yet:

  • Protocol piece (event kind, tags, signing)? Add it to quartz/.
  • Business logic (state, defaults, ordering, filter assembly)? Add it to commons/ — extract from amethyst/ first if needed (see Rule 5).

A commands/​*.kt file longer than ~200 lines is a code smell. Either the command is doing too many things, or the logic has leaked in from where it should have lived.

Rule 2 — text by default, --json is the machine contract

amy ships a dual-output contract:

  • Default stdout is human-readable text. A YAML-ish render of the result map. No shape promise — the renderer can change between releases.
  • --json switches stdout to one JSON object, one line. Stable snake_case keys; this shape is the public API.
  • stderr is for humans. Progress logs, warnings, per-relay ACK traces. Errors go here too — error: <code>: <detail> by default, JSON {"error":"…","detail":"…"} under --json.
  • Exit codes: 0 success · 1 runtime · 2 bad args · 124 await timeout.
  • Adding a --json key is safe; renaming or removing one is a breaking change and needs the commit message to say so.

Commands emit results via Output.emit(mapOf(...)) and errors via Output.error("code", "detail"). The Output object (in cli/​src/​main/​kotlin/…/​cli/​Output.kt) handles the text-vs-JSON branching automatically. Never println(...) user-facing output directly — System.err.println(...) is fine for progress logs only.

See references/​output-conventions.md.

Rule 3 — Non-interactive, ever

No readLine(), no TTY prompts, no hidden interactive behaviour. Passwords, names, keys, anything — all flags. Any network wait is an explicit await verb with --timeout.

Rule 4 — ~/​.amy/ is the whole world

State is reloaded from ~/​.amy/ on every invocation. No singletons, no in-process caches that survive across runs. This is what lets 100 parallel interop scenarios share a harness safely.

The layout:

  • ~/​.amy/​shared/​events-store/ — one file-backed Nostr event store per machine, shared across every account.
  • ~/​.amy/<account>/ — per-account dir: identity.json, state.json, aliases.json, marmot/.
  • ~/​.amy/​current — marker file written by amy use NAME to pin the active account.

Account selection is via the global --account NAME flag (required when more than one account exists; auto-picked when exactly one does). --account cannot collide with subcommand flags, so commands like marmot group create --name "Group" or profile edit --name "Alice" keep their own --name parameter.

Tests isolate by overriding $HOME for the amy subprocess (HOME=$(mktemp -d) amy --account alice init). amy reads $HOME directly (not user.home, which JDK 21 derives from getpwuid and ignores $HOME), so the same convention git/gpg/npm/ssh follow Just Works.

If you need new persisted state, add it to Config.kt, stores/​FileStores.kt, or a new helper (e.g. Aliases.kt) with a named JSON schema. Don't smuggle state into ~/​.amy/ outside the documented files.

Rule 5 — Extract before adding

If the command you're about to add needs logic from amethyst/, land the extraction first, in its own commit:

  1. Identify the class in amethyst/​src/​main/​java/…/.
  2. List its Android-only dependencies (Context, SharedPreferences, WorkManager, Log, Bitmap, Uri, …).
  3. For each, choose: inline, platform-abstract via expect/​actual, or take-as-constructor-arg.
  4. Move the file to commons/​commonMain/….
  5. Update the Android caller to use the new location. Add a JVM test.
  6. Then add the cli/​commands/… file.

Full checklist: references/​extraction-recipe.md.

Standard command shape

Every new command follows the same shape — parse args, open Context, prepare, call into commons/​quartz, publish or drain, emit one result via Output.emit. The template is in references/​command-template.md; copy it rather than re-deriving it.

Wire-up checklist:

  1. New file in cli/​commands/ with the object pattern.
  2. Add a branch in Commands.kt.
  3. Add a branch in Main.kt's dispatch (or under marmotDispatch / a new group dispatcher).
  4. Extend printUsage() in Main.kt.
  5. Add the row to cli/​README.md's command table.
  6. Update cli/​ROADMAP.md — move the row from 🆕 / 📦 to ✅.
  7. If the verb changes observable wire behaviour (a new event kind, a new relay-routing rule, a new JSON discriminator), add a case in the appropriate harness under cli/​tests/ — cli/​tests/​marmot/ for MLS flows, cli/​tests/​dm/ for NIP-17, cli/​tests/​cache/ for event-store behaviour, or a new sibling suite if it's none.

If you change --json output shape: note it in the commit message, bump the example in cli/​README.md, update any interop fixtures under cli/​tests/.

Where things live

cli/
├── README.md              # user-facing tour: install, examples, command tables
├── DEVELOPMENT.md         # public contract, architecture, design rules,
│                          #   event-store, relay-routing, full on-disk layout
├── ROADMAP.md             # parity matrix + ordered milestones
├── plans/                 # dated design docs (use for new subsystems)
├── tests/                 # end-to-end shell harnesses against a local relay
│   ├── lib.sh             # shared logging + result tracking
│   ├── headless/          # shared amy wrappers + assertions
│   ├── marmot/            # MLS group-messaging interop (vs whitenoise-rs)
│   ├── dm/                # NIP-17 DM interop (two amy clients)
│   └── cache/             # FsEventStore behaviour vs the cache helpers
└── src/​main/​kotlin/…/​cli/
    ├── Main.kt            # argv dispatch, global flags
    ├── Args.kt            # flag parser
    ├── Output.kt          # text/​json mode emitter + colour
    ├── Aliases.kt         # per-account aliases.json read/​write
    ├── Config.kt          # Identity, RunState, DataDir (~/​.amy layout)
    ├── Context.kt         # per-run wiring — the backbone
    ├── SecureFileIO.kt    # 0600/​0700 atomic writes, perm tighten
    ├── stores/            # file-backed MLS / KP / message stores
    ├── secrets/           # SecretStore backends (keychain / ncryptsec / plaintext)
    └── commands/          # one file (or group) per top-level verb
        ├── UseCommand.kt          # `amy use NAME`
        ├── InitCommands.kt        # init, whoami
        ├── CreateCommand.kt + LoginCommand.kt
        ├── RelayCommands.kt
        ├── ProfileCommands.kt
        ├── NotesCommands.kt + PostCommand.kt + FeedCommand.kt
        ├── DmCommands.kt
        ├── KeyPackageCommands.kt
        ├── GroupCommands.kt + GroupCreateCommand.kt + GroupReadCommands.kt
        │   GroupAddMemberCommand.kt + GroupMembershipCommands.kt
        │   GroupMetadataCommands.kt
        ├── MessageCommands.kt
        ├── MarmotResetCommand.kt
        ├── AwaitCommands.kt
        └── StoreCommands.kt

Shared logic consumed by Amy lives in commons/:

  • commons/​account/ — account bootstrap
  • commons/​marmot/ — MLS / group state
  • commons/​defaults/ — default relays, kinds
  • Consult commons/​plans/ for cross-cutting design work in flight.

Common mistakes to refuse

  • Adding protocol logic to cli/. Push back, offer to extract.
  • Silently changing a --json key. Flag as breaking.
  • Using println or print for command output. Use Output.emit(...) / Output.error(...). Plain System.err.println is fine for progress logs but never for user-consumable output.
  • runBlocking inside a command — the top-level main already does that. Commands are suspend fun.
  • Depending on :amethyst or :desktopApp. Never. If you need something from there, Rule 5.
  • Re-inventing identifier parsing. Use Context.requireUserHex or resolveUserHexOrNull in quartz/​nip05DnsIdentifiers/.
  • Re-inventing publish-and-confirm. Use Context.publish.
  • Re-inventing one-shot subscription. Use Context.drain.
  • Reading user.home directly. Use DataDir.DEFAULT_ROOT, which reads $HOME (the convention git/gpg/npm follow); JDK 21's user.home is derived from getpwuid and ignores $HOME, which silently breaks the test-isolation pattern.
  • Adding a global flag that collides with subcommand flags. --name is reserved for subcommand use (group/​profile names). Account selection is --account.

Plans & design docs

Cross-cutting design work goes in dated plan docs, in the module that owns the code being created — not in docs/​plans/, which is frozen.

  • cli/​plans/ — Amy-specific subsystems.
  • commons/​plans/ — shared code Amy consumes (e.g. 2026-04-21-event-renderer.md).

Cross-references

More skills from vitorpamplona

feed-patterns logo
vitorpamplona/amethyst

feed-patterns

Feed composition and data-access layer patterns in Amethyst. Use when adding or modifying a feed (home, profile, hashtag, bookmarks, notifications, DMs, communities), working with `FeedFilter` / `AdditiveComplexFeedFilter` / `ChangesFlowFilter` / `FilterByListParams` in `amethyst/.../ui/dal/`, or...

1.6K 0
View
quartz-integration logo
vitorpamplona/amethyst

quartz-integration

Integration guide for using the Quartz Nostr KMP library in external projects. Use when: (1) adding Quartz as a Gradle dependency, (2) setting up NostrClient with WebSocket, (3) creating/signing/sending events, (4) building relay subscriptions with Filter, (5) handling keys with KeyPair/NostrSign...

1.6K 0
View
auth-signers logo
vitorpamplona/amethyst

auth-signers

Signer abstraction patterns in Amethyst. Use when working with event signing, choosing between a local keypair (`NostrSignerInternal`), a remote NIP-46 bunker signer (`NostrSignerRemote`), or a NIP-55 Android external-app signer (`NostrSignerExternal`). Covers the abstract `NostrSigner` base clas...

1.6K 0
View
gradle-expert logo
vitorpamplona/amethyst

gradle-expert

Build optimization, dependency resolution, and multi-module KMP troubleshooting for AmethystMultiplatform. Use when working with: (1) Gradle build files (build.gradle.kts, settings.gradle), (2) Version catalog (libs.versions.toml), (3) Build errors and dependency conflicts, (4) Module dependencie...

1.6K 0
View
account-state logo
vitorpamplona/amethyst

account-state

Account state and in-memory event store patterns in Amethyst. Use when working with `Account.kt` (per-user StateFlow properties — follow list, relays, settings, mutes, bookmarks), `LocalCache` (the object-level event store backed by `LargeCache`), `User`/`Note` model classes, or any ViewModel tha...

1.6K 0
View
android-expert logo
vitorpamplona/amethyst

android-expert

Android platform patterns for the `amethyst/` module. Use when working with (1) Android navigation (Navigation Compose, type-safe routes, bottom nav), (2) runtime permissions (camera, notifications, biometrics), (3) platform APIs (Intent, Context, Activity, ContentResolver), (4) Material3 theming...

1.6K 0
View
relay-client logo
vitorpamplona/amethyst

relay-client

Subscription and filter-assembly patterns for the Amethyst relay client layer in `commons/.../relayClient/`. Use when working with compose-scoped subscriptions (`ComposeSubscriptionManager`, `Subscribable`), filter assemblers (`MetadataFilterAssembler`, `ReactionsFilterAssembler`, `FeedMetadataCo...

1.6K 0
View
Amethyst logo
vitorpamplona/amethyst

Amethyst

Build customized Amethyst Nostr clients for Android. Fork, rebrand, customize, and distribute your own version.

1.6K 0
View
kotlin-coroutines logo
vitorpamplona/amethyst

kotlin-coroutines

Advanced Kotlin coroutines patterns for AmethystMultiplatform. Use when working with: (1) Structured concurrency (supervisorScope, coroutineScope), (2) Advanced Flow operators (flatMapLatest, combine, merge, shareIn, stateIn), (3) Channels and callbackFlow, (4) Dispatcher management and context s...

1.6K 0
View
kotlin-expert logo
vitorpamplona/amethyst

kotlin-expert

Advanced Kotlin patterns for AmethystMultiplatform. Flow state management (StateFlow/SharedFlow), sealed hierarchies (classes vs interfaces), immutability (@Immutable, data classes), DSL builders (type-safe fluent APIs), inline functions (reified generics, performance). Use when working with: (1)...

1.6K 0
View
find-missing-translations logo
vitorpamplona/amethyst

find-missing-translations

Use when comparing Android strings.xml locale files to find untranslated string resources, missing translation keys, or preparing translation work for a specific language

1.6K 0
View
find-non-lambda-logs logo
vitorpamplona/amethyst

find-non-lambda-logs

Use when auditing or migrating Log calls — flags both interpolated Log.d/i/w/e that should use the lambda overload (allocation hygiene) and catch-block Log.w/e that interpolate ${e.message} but drop the throwable (lost stack traces)

1.6K 0
View

Popular AI tools

Kaiber logo
Video

Kaiber

Generate, edit, and beat-sync AI video with leading models in one workspace.

Paid
View
Vimcal logo
Productivity

Vimcal

The world's fastest calendar for remote work

Free
View

Transform Your Design with AI Designer by ImgCreator.ai

Freemium
View
Akool AI logo
Content & writing

Akool AI

Revolutionizing Video Production with AI-Powered Creativity

Paid
View

Extend an image past the frame and let AI fill the new aspect ratio.

Freemium
View
StarByFace logo
Security

StarByFace

Discover your celebrity doppelgänger with StarByFace!

Free
View
C

ChainClarity explains 700+ crypto whitepapers in plain English, with layered summaries, comparisons, research tools, alerts, and a $4.99 Pro plan.

Freemium
View
Opus Clip logo
Coding & apps

Opus Clip

Opus.ai: Revolutionize Your Web Experience

Free
View