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 CLI command can call it. Enforces the thin-assembly-layer rule (no Nostr protocol or business logic inside `cli/`), the dual-output contract (text by default, single-line JSON object on stdout under `--json`, exit codes 0/1/2/124), and the extract-from-Android recipe. Complements `nostr-expert` (protocol in Quartz), `kotlin-multiplatform` (expect/actual for extraction), and `feed-patterns` / `account-state` / `relay-client` (where the business logic should end up). NOT for general Nostr or Kotlin work โ those have their own skills.
Standardmรครig ist der Prompt ausgewรคhlt, der zuerst die Quelle prรผft. Sie kรถnnen zu einem direkten Befehl wechseln oder eine lokale Kopie herunterladen.
Quelldateien prรผfen
Lesen Sie SKILL.md und alle von SkillsMP angezeigten Begleitdateien, bevor Sie sich fรผr eine Installation entscheiden.
Mit Codex oder Claude installieren Kopieren Sie diesen Prompt, fรผgen Sie ihn in Codex, Claude oder einen anderen Assistant ein und lassen Sie die Skill-Seite prรผfen und installieren.
Ein direkter Befehl รผberspringt den Prรผf-Prompt. Prรผfen Sie die Quelle, bevor Sie ihn ausfรผhren.
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 CLI command can call it. Enforces the thin-assembly-layer rule (no Nostr protocol or business logic inside `cli/`), the dual-output contract (text by default, single-line JSON object on stdout under `--json`, exit codes 0/1/2/124), and the extract-from-Android recipe. Complements `nostr-expert` (protocol in Quartz), `kotlin-multiplatform` (expect/actual for extraction), and `feed-patterns` / `account-state` / `relay-client` (where the business logic should end up). NOT for general Nostr or Kotlin work โ those have their own skills.
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.
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/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:
Identify the class in amethyst/src/main/java/โฆ/.
List its Android-only dependencies (Context, SharedPreferences,
WorkManager, Log, Bitmap, Uri, โฆ).
For each, choose: inline, platform-abstract via expect/actual, or
take-as-constructor-arg.
Move the file to commons/commonMain/โฆ.
Update the Android caller to use the new location. Add a JVM test.
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:
New file in cli/commands/ with the object pattern. Sub-verb
dispatch functions use the shared route(...) helper in
Router.kt rather than a hand-rolled when (tail[0]).
Add a branch in Main.kt's dispatch (top-level verbs call the
command object directly, e.g. "relay" -> RelayCommands.dispatch(โฆ);
marmot sub-verbs go through marmotDispatch's route map).
Extend printUsage() in Main.kt.
Add the row to cli/README.md's command table.
Update cli/ROADMAP.md โ move the row from ๐ / ๐ฆ to โ .
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/.
CashuKeysetCounterStore; the NIP-60/61 wallet, shared with Android.
commons/relayManagement/Nip86Retriever โ NIP-86 HTTP client, shared with
the Android relay-management screen.
commons/defaults/ โ default relays, kinds
Consult commons/plans/ for cross-cutting design work in flight.
A few amy verbs lean on modules beyond quartz/commons: amy serve
depends on :geode (the standalone relay) โ the one allowed extra module
dependency. :amethyst / :desktopApp remain forbidden (Rule 5).
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.