Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,6 @@ vendor/

# Local scratch drafts, not meant to be tracked
/.drafts/

# Local-only planning/audit docs, not meant to be tracked
/docs/private/
8 changes: 8 additions & 0 deletions .goreleaser.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ dockers:
- "ghcr.io/ohstr/ncli:latest-amd64"
build_flag_templates:
- "--platform=linux/amd64"
skip_push: auto
- id: ncli-arm64
ids: [ncli]
goos: linux
Expand All @@ -55,16 +56,21 @@ dockers:
- "ghcr.io/ohstr/ncli:latest-arm64"
build_flag_templates:
- "--platform=linux/arm64"
skip_push: auto

# skip_push: auto here too -- otherwise a prerelease build would still try
# to manifest the per-arch tags above, which skip_push: auto never pushed.
docker_manifests:
- name_template: "ghcr.io/ohstr/ncli:{{ .Version }}"
image_templates:
- "ghcr.io/ohstr/ncli:{{ .Version }}-amd64"
- "ghcr.io/ohstr/ncli:{{ .Version }}-arm64"
skip_push: auto
- name_template: "ghcr.io/ohstr/ncli:latest"
image_templates:
- "ghcr.io/ohstr/ncli:latest-amd64"
- "ghcr.io/ohstr/ncli:latest-arm64"
skip_push: auto

brews:
- name: ncli
Expand All @@ -80,6 +86,7 @@ brews:
bin.install "ncli"
test: |
system "#{bin}/ncli", "version"
skip_upload: auto

# No `changelog:` block here, deliberately -- GoReleaser's changelog pipe
# is also the only place that loads a `--release-notes` file into the
Expand All @@ -101,3 +108,4 @@ release:
# place if a Release for the tag already exists (e.g. from a prior
# failed/retried run) instead of using --release-notes below.
mode: replace
prerelease: auto
37 changes: 37 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,42 @@
# Changelog

## [0.5.0-rc.1]

### Added

- `ncli decode` extended to NIP-CASH tokens (`lokicash1...`, any HRP) and
NIP-CW `circlehub1...` connections. A `cashhub1...` Hub connection is
recognized and rejected -- no local decoder for that format. The
pairing secret embedded in either new format is never surfaced, in any
mode. ([#49](https://github.com/ohstr/ncli/pull/49))
- Every command's `--help` output now includes an onboarding `Example:`
field, including pure group commands.
([#51](https://github.com/ohstr/ncli/pull/51))

### Changed

- A local flow's `ensure` policy now defaults to `create` instead of
`exists` when omitted, so a missing local store path is created rather
than failing to load.
([#50](https://github.com/ohstr/ncli/pull/50))
- Trimmed redundant/noisy `Long` text and placeholder examples across
commands -- text already covered by a flag's own description no
longer repeats in the command's `Long`, and the generic `mylabel`
placeholder now reads `satoshi`.
([#51](https://github.com/ohstr/ncli/pull/51))
- Bumped `github.com/ohstr/nmilat` to v0.3.0 -- adds NIP-34/NIP-22
support (consumed by the `decode` extension above), fixes
`TransferFromSources` reusing a stale wallet-bound client on its
second call, and fixes silently-dropped `circle_hub`/`circle_wallet`
fee fields on NWC unmarshal.
([#51](https://github.com/ohstr/ncli/pull/51))

### Fixed

- Two `Example:` commands that didn't actually run as written against a
real `ncli` binary.
([#51](https://github.com/ohstr/ncli/pull/51))

## [0.4.9]

### Fixed
Expand Down
110 changes: 110 additions & 0 deletions CLI_NOISE_AUDIT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
# CLI narration noise audit

Every progress line printed to stderr while a command runs (per AGENTS.md's
stdout=result / stderr=narration convention), checked against what it
actually needs to say. 22 lines across 8 files. Lines not listed here are
kept as-is (final results, errors, genuine tips).

Reconciled 2026-09-17 against current source: every file below was read in
full (not re-grepped) and every row cross-checked line-by-line. Result: all
22 rows below still match source verbatim, zero drift, zero reclassified.
The reconciliation pass did surface two things the first pass missed — a
recurring stdout/stderr plumbing bug (see its own section below) and ~38
files that had never actually been checked (folded into "Clean" below).

## cli/reindex/command.go — worst offender (8 lines)

Every reindex phase announces its own start, and the search path reports
its final count twice.

| Line | Before | After |
|---|---|---|
| 40 | `"connecting to search backend..."` | removed |
| 50 | `"starting search reindexing..."` | removed |
| 102 | `"progress..."` (field `indexed`) | `"indexed"` |
| 114 | `"final batch indexed"` (field `indexed`) | removed — duplicates line 124's `total` |
| 122 | `"waiting for verification worker to finish remaining jobs..."` | `"waiting on verification worker"` |
| 128 | `"fetching events..."` | removed |
| 149 | `"starting zap reindexing..."` | removed |
| 157 | `"progress..."` (field `zaps_indexed`) | `"indexed"` |

## cli/relay/service.go — server lifecycle (5 lines)

| Line | Before | After |
|---|---|---|
| 46 | `"server config check"` | removed — fold `pubkey`/`port` fields into line 247 |
| 247 | `"listening..."` (no fields) | `"listening"` with `pubkey`/`port` fields |
| 266 | `"stopping server gracefully"` | removed — outcome already logged by line 280 or 285 |
| 288 | `"stopping verification workers..."` | removed — paired with "verification workers stopped" |
| 292 | `"stopping events store..."` | removed — paired with "events store stopped" |

## cli/relay/admin.go + command.go — duplicated across both (2 lines)

| Line | Before | After |
|---|---|---|
| admin.go:172 | `"using config file"` (field `config`) | `"config"` |
| command.go:286 | `"using config file"` (field `config`) | `"config"` |

## client/client.go — per-target query noise (4 lines)

| Line | Before | After |
|---|---|---|
| 149 | `"querying %s"` (local path, in mergeEventsFromTargets) | removed |
| 156 | `"querying %s"` (remote host, in mergeEventsFromTargets) | removed |
| 225 | `"querying %s"` (local path, in Find) | removed |
| 232 | `"querying %s"` (remote host, in Find) | removed |

## client/ping.go — start-of-run announcement (1 line)

| Line | Before | After |
|---|---|---|
| 123 | `"Checking connectivity for %d %s"` | removed — the real result is the "%d of %d reachable" summary at line 167 |

## cli/ncli/id.go — vault save (3 lines)

| Line | Before | After |
|---|---|---|
| 204 | `"unlocking vault..."` | removed — redundant with the "Vault password:" prompt right after (confirmed live in `cli/keyresolve/resolve.go`) |
| 206 | `"creating vault identity..."` | removed — redundant with the "Set a vault password:" prompt right after (same) |
| 223 | `"saving identity..."` | removed — superseded by "identity saved to vault (label: %s)" right after |

## cli/ncli/miner.go — progress tick (1 line)

| Line | Before | After |
|---|---|---|
| 177 | `"mining... %s hashes tried, %s elapsed, %s across %d worker(s)"` | `"%s hashes, %s, %s, %d workers"` |

## cli/delegate/command.go — redundant header (1 line)

| Line | Before | After |
|---|---|---|
| 141 | `"Delegation token generated."` | removed — restates the fields printed right below it |

## Stdout/stderr plumbing gaps (separate issue — not wording)

Three commands report their actual text-mode *result* — not narration —
through `log.Info`/`log.Error`, which AGENTS.md routes to stderr. Nothing
wrong with the wording; the fix is which stream it goes to.

| File | Lines | Text | Command affected |
|---|---|---|---|
| `client/ping.go` | ~167, ~214–222 | `"%d of %d %s reachable"`, per-relay `"connectivity OK"`/`"connectivity check failed"` | `ping`'s entire text-mode result lives on stderr |
| `cli/ncli/prefs.go` | 60, 62, 93, 95, 146 | `"added"`, `"already configured"`, `"removed"`, `"not configured"`, `"cleared"` | `prefs relays add`/`remove`/`clear` |
| `cli/relay/context_run.go` | 146, 189 | `"relay context created"`, `"new identity saved to vault"` | `relay --context <new-name>`'s auto-create path |

## Clean (audited in full, zero noise)

Every file below was read end to end, not sampled by grep.

- **cli/blossom** (all 9): `command.go`, `download.go`, `list.go`, `mirror.go`, `report.go`, `rm.go`, `servers.go`, `shared.go`, `upload.go` — no progress narration anywhere, including upload/download/mirror's transfer loops.
- **cli/bunker** (all 16 non-TUI files): `client.go`, `clipboard.go`, `command.go`, `daemon.go`, `eventlog.go`, `grantspec.go`, `handler.go`, `identity.go`, `ipc_client.go`, `ipc_server.go`, `policy.go`, `queue.go`, `spawn.go`, `spawn_unix.go`, `spawn_windows.go`, `uri.go`. `daemon.go`'s custom `d.log()` method never reaches CLI stdout/stderr — it only feeds an in-memory TUI log panel or a rotating `daemon.log` file on disk.
- **cli/relay**: `context.go`, `context_run.go` (noise-wise; see plumbing gap above), `service_membership.go`.
- **cli/common** (all 10): `errors.go`, `appdir.go`, `args.go`, `auth.go`, `config.go`, `logging.go`, `logging_unix.go`, `logging_windows.go`, `prompt.go`, `version.go`.
- **cli/keyresolve/resolve.go**, **cli/reindex/state.go**.
- **cli/ncli** (11): `apply.go`, `decode.go`, `dump.go`, `filters.go`, `find.go`, `id_sign.go`, `ping.go`, `publish.go`, `query.go`, `root.go`, `version.go`. (`prefs.go` has the plumbing gap above instead.)
- **client/** (12): `decode.go`, `event_export.go`, `identity.go`, `inspect.go`, `inspect_store.go`, `miner.go`, `prefs.go`, `publish.go`, `recovery.go`, `spec.go`, `stream.go`, `vault.go`.

Out of scope, not "clean" — confirmed to have no headless code path at all,
so there's no console narration to audit: `cli/bunker/board.go` (live TUI
board) and `client/neg_sync.go` (`Client.init()` rejects a `SyncSpec`
without a TUI attached).
3 changes: 2 additions & 1 deletion cli/blossom/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ per (item, server) pair, and exiting non-zero if any pair failed.
"download" tries the configured servers in order, stopping at the first
that answers; "list" queries one server by default, or every server with
--all.`,
RunE: common.RequireSubcommand,
Example: ` ncli blossom upload ./photo.jpg --identity satoshi`,
RunE: common.RequireSubcommand,
}

cmd.PersistentFlags().String("identity", "", "Identity to sign with -- vault label, nsec, npub, hex, nprofile, or nip-05")
Expand Down
2 changes: 2 additions & 0 deletions cli/blossom/download.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ server URL ending in a hash -- tries the configured servers in order

Writes to --output, or "<hash>.<ext>" in the current directory if
omitted, or streams to stdout with "-o -" (suppressing the summary line).`,
Example: ` ncli blossom download <hash>
ncli blossom download <hash> -o -`,
Args: common.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
ctx, cancel := signal.NotifyContext(cmd.Context(), syscall.SIGINT, syscall.SIGTERM)
Expand Down
2 changes: 2 additions & 0 deletions cli/blossom/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ deduped by hash.
identifier may be a vault label, nsec, npub, hex pubkey, nprofile, or
nip-05 address, resolved to a hex pubkey; defaults to --identity's
resolved pubkey when omitted.`,
Example: ` ncli blossom list --identity satoshi
ncli blossom list --identity satoshi --all`,
Args: common.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
ctx, cancel := signal.NotifyContext(cmd.Context(), syscall.SIGINT, syscall.SIGTERM)
Expand Down
1 change: 1 addition & 0 deletions cli/blossom/mirror.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ func newMirrorCommand() *cobra.Command {
(--server, or the configured default list) -- each server fetches
source-url itself; no bytes pass through ncli. Reports a result per
server.`,
Example: ` ncli blossom mirror https://example.com/file.jpg --identity satoshi`,
Args: func(cmd *cobra.Command, args []string) error {
if len(args) != 1 {
return common.UsageError(cmd, fmt.Errorf("exactly one source URL is required"))
Expand Down
1 change: 1 addition & 0 deletions cli/blossom/report.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ func newReportCommand() *cobra.Command {
Long: `Sign and submit a kind:1984 report event to a server's PUT /report --
authenticated by its own signature, not a BUD-11 token. Targets one
server: --server, or the first configured default.`,
Example: ` ncli blossom report <hash> --identity satoshi`,
Args: func(cmd *cobra.Command, args []string) error {
if len(args) != 1 {
return common.UsageError(cmd, fmt.Errorf("exactly one hash is required"))
Expand Down
1 change: 1 addition & 0 deletions cli/blossom/rm.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ func newRmCommand() *cobra.Command {
Long: `Sign a hash-scoped BUD-11 authorization and DELETE the blob from every
target server (--server, or the configured default list), reporting a
result per server. Requires --yes in a non-interactive session.`,
Example: ` ncli blossom rm <hash> --identity satoshi --yes`,
Args: func(cmd *cobra.Command, args []string) error {
if len(args) != 1 {
return common.UsageError(cmd, fmt.Errorf("exactly one hash is required"))
Expand Down
29 changes: 17 additions & 12 deletions cli/blossom/servers.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,11 @@ import (

func newServersCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "servers",
Short: "Manage the default Blossom server list",
Long: `Manage the server list "ncli blossom" commands fall back to when not given explicit --server flags.`,
RunE: common.RequireSubcommand,
Use: "servers",
Short: "Manage the default Blossom server list",
Long: `Manage the server list "ncli blossom" commands fall back to when not given explicit --server flags.`,
Example: ` ncli blossom servers list`,
RunE: common.RequireSubcommand,
}

cmd.AddCommand(newServersAddCommand())
Expand Down Expand Up @@ -81,8 +82,9 @@ func newServersAddCommand() *cobra.Command {
var publish bool

cmd := &cobra.Command{
Use: "add <server-url>",
Short: "Add a server to the default list",
Use: "add <server-url>",
Short: "Add a server to the default list",
Example: ` ncli blossom servers add https://blossom.example.com`,
Args: func(cmd *cobra.Command, args []string) error {
if len(args) != 1 {
return common.UsageError(cmd, fmt.Errorf("exactly one server url is required"))
Expand Down Expand Up @@ -150,8 +152,9 @@ func newServersRemoveCommand() *cobra.Command {
var publish bool

cmd := &cobra.Command{
Use: "remove <server-url>",
Short: "Remove a server from the default list",
Use: "remove <server-url>",
Short: "Remove a server from the default list",
Example: ` ncli blossom servers remove https://blossom.example.com`,
Args: func(cmd *cobra.Command, args []string) error {
if len(args) != 1 {
return common.UsageError(cmd, fmt.Errorf("exactly one server url is required"))
Expand Down Expand Up @@ -213,9 +216,10 @@ func newServersRemoveCommand() *cobra.Command {

func newServersListCommand() *cobra.Command {
return &cobra.Command{
Use: "list",
Short: "List the default servers",
Args: common.NoArgs,
Use: "list",
Short: "List the default servers",
Example: ` ncli blossom servers list`,
Args: common.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
jsonMode, _ := cmd.Flags().GetBool("json")

Expand Down Expand Up @@ -266,7 +270,8 @@ declares.

Unlike "servers add/remove/list", which manage your own default list,
this looks up someone else's published servers.`,
Args: common.ExactArgs(1),
Example: ` ncli blossom servers discover npub1...`,
Args: common.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
ctx, cancel := signal.NotifyContext(cmd.Context(), syscall.SIGINT, syscall.SIGTERM)
defer cancel()
Expand Down
3 changes: 3 additions & 0 deletions cli/blossom/upload.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ func newUploadCommand() *cobra.Command {

Pass --optimize to request server-side transcoding/optimization (BUD-05's
PUT /media) instead of a byte-for-byte store.`,
Example: ` ncli blossom upload ./photo.jpg --identity satoshi
ncli blossom upload ./photo.jpg --identity satoshi --optimize
ncli blossom upload ./photo.jpg --identity satoshi --server https://blossom.example.com`,
Args: func(cmd *cobra.Command, args []string) error {
if len(args) == 0 {
return common.UsageError(cmd, fmt.Errorf("at least one file is required"))
Expand Down
Loading
Loading