Files
amethyst/.claude/skills/amy-expert/references/command-template.md
T
Claude 9b9084ffaf docs(cli): split amy docs by responsibility + add amy-expert skill
Separate the single DEVELOPMENT.md into focused docs per audience:

- cli/README.md — trim agent/interop sub-sections; user-facing contract,
  commands, data-dir, troubleshooting only.
- cli/DEVELOPMENT.md — pared down to architecture, how-to-add-a-command,
  output conventions, testing, housekeeping.
- cli/ROADMAP.md (new) — north-star, parity matrix, ordered milestones,
  non-goals. The live checklist for CLI feature parity.
- cli/plans/2026-04-21-cli-distribution.md (new) — packaging strategy
  (Homebrew / winget / Scoop / deb / rpm / AUR / AppImage).
- commons/plans/2026-04-21-event-renderer.md (new) — cross-cutting
  renderer design owned by commons, consumed by cli + desktop + android.

Agentic routing:

- .claude/skills/amy-expert/SKILL.md (new) — routing triggers + the
  five hard rules (thin-layer, JSON contract, non-interactive,
  data-dir-is-world, extract-before-adding).
- references/command-template.md, extraction-recipe.md,
  output-conventions.md — bundled copy-paste references.

Root .claude/CLAUDE.md:

- Adds cli/ to the module list with its sharing rule.
- Registers amy-expert in the skills table.
- Documents per-module plans/ convention; freezes docs/plans/.

https://claude.ai/code/session_01BQ5ZHwa8BAgEQ9zeM4CKhW
2026-04-22 00:23:00 +00:00

3.0 KiB

Command-file template

Copy this shape for every new Amy verb. Resist the urge to deviate — the uniform shape is what makes commands easy to audit and test.

Single-verb command

package com.vitorpamplona.amethyst.cli.commands

import com.vitorpamplona.amethyst.cli.Args
import com.vitorpamplona.amethyst.cli.Context
import com.vitorpamplona.amethyst.cli.DataDir
import com.vitorpamplona.amethyst.cli.Json

object NotePublishCommand {
    suspend fun run(dataDir: DataDir, rest: Array<String>): Int {
        val args = Args(rest)
        val text = args.positional(0, "text")

        val ctx = Context.open(dataDir)
        try {
            ctx.prepare()

            val event = com.vitorpamplona.amethyst.commons.note
                .buildTextNote(ctx.signer, text)
            val ack = ctx.publish(event, ctx.outboxRelays())

            Json.writeLine(mapOf(
                "event_id"      to event.id,
                "kind"          to event.kind,
                "published_to"  to ack.filterValues { it }.keys.map { it.url },
                "rejected_by"   to ack.filterValues { !it }.keys.map { it.url },
            ))
            return 0
        } finally {
            ctx.close()
        }
    }
}

Multi-verb group

When a feature has several verbs (note publish, note show, note react), group them:

object NoteCommands {
    suspend fun dispatch(dataDir: DataDir, tail: Array<String>): Int {
        if (tail.isEmpty()) return Json.error("bad_args", "note <publish|show|react>")
        val rest = tail.drop(1).toTypedArray()
        return when (tail[0]) {
            "publish" -> NotePublishCommand.run(dataDir, rest)
            "show"    -> NoteShowCommand.run(dataDir, rest)
            "react"   -> NoteReactCommand.run(dataDir, rest)
            else      -> Json.error("bad_args", "note ${tail[0]}")
        }
    }
}

Each verb gets its own file. Once a single file crosses ~200 lines, split it — see GroupCommands.kt and its siblings as the reference.

Wire-up checklist

For every new command:

  1. File under cli/commands/.
  2. Branch in Commands.kt:
    suspend fun note(dataDir: DataDir, tail: Array<String>): Int =
        NoteCommands.dispatch(dataDir, tail)
    
  3. Branch in Main.kt's top-level dispatch:
    "note" -> Commands.note(dataDir, tail)
    
  4. Line in printUsage() explaining the verb.
  5. Row in cli/README.md's command table.
  6. Status flip in cli/ROADMAP.md (🆕 / 📦).

What not to do

  • No runBlocking in a command body — main() already does it.
  • No println / print for command output — use Json.writeLine(...). System.err.println(...) is fine for progress logs (they're already disposable).
  • No swallowing errors — let exceptions bubble; main() translates them to {"error":...} + exit code.
  • No holding a connection open across invocations — every run opens a fresh Context and closes it in finally.
  • No blocking reads for user input — take a flag.

Output-shape rules

See output-conventions.md.