From f19ec983f459409b36862621b758cfe9f9884942 Mon Sep 17 00:00:00 2001 From: sepehr-safari Date: Thu, 24 Sep 2026 11:57:55 +0300 Subject: [PATCH 1/2] docs: a skill, AGENTS.md and a plugin, so agents can use deed skills/deed/SKILL.md teaches an agent deed's commands, recipes that were each run against the binary, the output and exit-code contract, and two rules: publishing is public and permanent, so ask first, and keep a secret key in NOSTR_SECRET_KEY. It installs with npx skills add zig-nostr/deed, and .claude-plugin makes it a Claude Code plugin. AGENTS.md is for changing deed rather than using it. The README says how to add them. Closes #28. --- .claude-plugin/marketplace.json | 15 ++++++ .claude-plugin/plugin.json | 12 +++++ AGENTS.md | 43 +++++++++++++++++ README.md | 14 +++++- skills/deed/SKILL.md | 83 +++++++++++++++++++++++++++++++++ 5 files changed, 166 insertions(+), 1 deletion(-) create mode 100644 .claude-plugin/marketplace.json create mode 100644 .claude-plugin/plugin.json create mode 100644 AGENTS.md create mode 100644 skills/deed/SKILL.md diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json new file mode 100644 index 0000000..e2709c4 --- /dev/null +++ b/.claude-plugin/marketplace.json @@ -0,0 +1,15 @@ +{ + "name": "deed", + "description": "deed, the nostr command line: keys, events, NIP-19, NIP-44, relays and a local store, from a shell.", + "owner": { + "name": "Sepehr Safari", + "url": "https://github.com/sepehr-safari" + }, + "plugins": [ + { + "name": "deed", + "source": "./", + "description": "Teaches the agent to use deed for nostr work: generating keys, signing and verifying events, NIP-19 codes, NIP-44, querying relays, publishing, and a local store it can query offline." + } + ] +} diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json new file mode 100644 index 0000000..a91f281 --- /dev/null +++ b/.claude-plugin/plugin.json @@ -0,0 +1,12 @@ +{ + "name": "deed", + "description": "Teaches the agent to use deed for nostr work: generating keys, signing and verifying events, NIP-19 codes, NIP-44, querying relays, publishing, and a local store it can query offline.", + "author": { + "name": "Sepehr Safari", + "url": "https://github.com/sepehr-safari" + }, + "homepage": "https://zignostr.com/deed", + "repository": "https://github.com/zig-nostr/deed", + "license": "MIT", + "keywords": ["nostr", "cli", "zig", "nip-19", "nip-44", "relay", "signing"] +} diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..dbf8a0a --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,43 @@ +# AGENTS.md + +A guide to this repository for anyone changing it, people and coding agents alike. To *use* deed rather than change it, read [`skills/deed/SKILL.md`](skills/deed/SKILL.md). + +## What this is + +`deed` is a command line for nostr, written in Zig on top of the [`nostr`](https://github.com/zig-nostr/nostr) library. It makes keys, builds and signs events, reads and writes NIP-19 codes, encrypts and decrypts with NIP-44, checks signatures, asks relays for events, publishes to them, and keeps what it fetches in a local LMDB store. + +## Build and test + +```sh +zig build # binary at zig-out/bin/deed +zig build test # unit tests, including real relays on loopback +zig fmt --check src build.zig # CI fails on unformatted code +zig build -Doptimize=ReleaseSafe -Dstrip=true # the release build +python3 bench/run.py zig-out/bin/deed # benchmarks, see BENCHMARKS.md +``` + +Use the Zig version in `.zigversion`. The `nostr` dependency is pinned by URL and hash in `build.zig.zon`; move it with `zig fetch --save=nostr ` and check the diff is only the url and hash lines. + +## Layout + +``` +src/ + main.zig # dispatch, the stdout/stderr writers, exit codes + cli.zig # exit code constants, the stdin record reader + cmd_*.zig # one file per verb + relayset.zig # the shared relay query behind req and fetch + dial.zig # dialling every relay at once under one deadline + testrelay.zig # a websocket relay on loopback, for tests only +bench/ # the benchmark script and its relay +scripts/ # the one-line installer (pure ASCII, CI checks it) +skills/deed/ # the skill for agents that operate deed +``` + +## Conventions + +- Every verb takes its inputs as arguments or, given none, as newline-delimited records on stdin, and writes one result per line on stdout. Diagnostics go to stderr. Keep it that way: it is what makes the verbs compose. +- Exit codes are an interface: 0 success, 1 ran and failed, 2 not understood, 141 the reader went away. Do not add new ones casually. +- Anything a relay sends is untrusted: events are verified and matched against the filter before they are printed or stored, and relay text is escaped before it reaches a terminal. +- Tests that dial use `testrelay.DialAllocator`, not `std.testing.allocator`: on macOS a stack-capturing allocator can swallow a cancel and hang the test. +- `zig build test` caches passing runs. To check for flakiness, run the test binary directly (`ls -t .zig-cache/o/*/test | head -1`) several times. +- Conventional Commits. Every PR links its tracking issue. diff --git a/README.md b/README.md index 0d64de3..e8d0ffa 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ macOS and Linux, Intel and ARM. It works out which build this machine wants, che The script is short and worth reading before you pipe anything into bash. If you would rather do it yourself: ```sh -VERSION=0.3.1 +VERSION=0.3.2 PLATFORM=macos-aarch64 # or macos-x86_64, linux-x86_64, linux-aarch64 BASE=https://github.com/zig-nostr/deed/releases/download/v$VERSION @@ -105,6 +105,18 @@ Scripts branch on these, so they are part of the interface and not free to drift | `2` | the command was not understood: unknown verb, unknown flag, missing argument. Nothing was attempted | | `141` | the reader on the other end of the pipe went away, as in `deed decode … \| head -1` | +## Using deed from an agent + +deed is built to be driven by scripts, and that makes it a good tool for AI agents doing nostr work: one result per line on stdout, diagnostics on stderr, exit codes that mean one thing each, and `deed help ` for the exact usage of anything. + +The skill in [`skills/deed`](skills/deed/SKILL.md) teaches an agent the commands, the recipes, and the two things to get right: publishing is public and permanent, so it asks first, and a secret key stays in `NOSTR_SECRET_KEY` rather than on a command line. Add it to any agent that supports skills: + +```sh +npx skills add zig-nostr/deed +``` + +In Claude Code it also installs as a plugin: `/plugin marketplace add zig-nostr/deed`, then `/plugin install deed@deed`. + ## How fast it is A one-shot command runs in about 2.3 ms and under 2 MB of memory. deed signs and verifies about 31,000 events a second each, stores 100,000 events from a relay at about 17,000 a second, and answers a lookup from that store in about 3 ms, start to finish. The binaries are 2.3 to 2.7 MB. [BENCHMARKS.md](BENCHMARKS.md) has the full set and how to reproduce every number. diff --git a/skills/deed/SKILL.md b/skills/deed/SKILL.md new file mode 100644 index 0000000..fda1fe8 --- /dev/null +++ b/skills/deed/SKILL.md @@ -0,0 +1,83 @@ +--- +name: deed +description: Use deed, the nostr command line, to work with nostr from a shell. Use when generating nostr keys, deriving an npub, building and signing events, checking signatures, encoding or decoding NIP-19 codes (npub, nsec, note, nprofile, nevent, naddr), encrypting or decrypting NIP-44 messages, querying relays with REQ filters, fetching the events a code names, publishing events to relays, or keeping events in a local store to query offline. Also use when writing scripts or pipelines that process nostr events as JSON lines, or when testing a nostr client or relay. +--- + +# deed + +A fast command line for nostr. One binary, macOS and Linux. Every command takes its input as arguments or as newline-delimited records on stdin, and writes one result per line on stdout, so commands pipe into each other and into `jq`. + +## Install + +```sh +curl -fsSL https://raw.githubusercontent.com/zig-nostr/deed/main/scripts/install.sh | bash +``` + +Installs into `~/.local/bin` after checking the download's SHA-256. `deed version` confirms it. `deed help ` prints the exact usage of any command; check it before guessing a flag. + +## Before you act + +- **Publishing is public and permanent.** `deed publish` sends events to relays, where anyone can read them and they cannot be reliably deleted. Confirm with the user before publishing anything to a public relay, and never publish with a key that is not theirs to use. +- **Keep secret keys out of commands and output.** Put the key in `NOSTR_SECRET_KEY`, which `event`, `encrypt` and `decrypt` read when `--sec` is absent. A key passed as `--sec` lands in shell history and the process table. Never print an nsec back to the user unless they asked for it. +- `deed key public` takes a **secret** key. A 64-character hex string is always read as a secret key; passing a public key derives a different, wrong npub without an error. + +## Output and exit codes + +- stdout: results only, one per line (events as JSON, codes as text or JSON). +- stderr: every diagnostic, each line starting with `deed`. +- Exit codes: `0` success, `1` the command ran and something failed (a bad signature, an event no relay accepted, an unreadable record), `2` the command was not understood, `141` the reader on the other end of the pipe went away. A bad record fails that record alone; the rest of the stream still runs. + +## Recipes + +```sh +# a key, kept out of history +export NOSTR_SECRET_KEY=$(deed key generate) +deed key public "$NOSTR_SECRET_KEY" # npub1... + +# sign and check +deed event -c "hello" | deed verify # prints nothing and exits 0 when valid +deed event -k 1 -c "gm" -t t=nostr # a kind-1 note with a t tag + +# sign many: drafts as JSON lines, one event out per line (the `-` is required) +cat drafts.jsonl | deed event - > signed.jsonl + +# publish, keeping a record of what went out (ASK THE USER FIRST) +deed publish wss://relay.example < signed.jsonl > published.jsonl +# stdout: each event a relay accepted; stderr: every relay's answer with the event id + +# ask relays, and keep what comes back +deed req -k 1 -l 20 -a npub1... --store ~/.deed/db wss://relay.example +deed req -k 1 -l 20 -a npub1... --store ~/.deed/db --local # again, no network + +# see the filter before it is sent: no relay means print, don't send +deed req -k 0 -a npub1... + +# get what a code points at (a bare npub fetches the profile) +deed fetch nevent1... +deed fetch npub1... wss://relay.example # a code with no relay hints needs a relay named + +# NIP-19 +deed decode nprofile1... # {"pubkey":"...","relays":[...]} +deed encode nevent --relay wss://relay.example --kind 1 + +# NIP-44 +deed encrypt --to npub1... "meet at six" +deed decrypt --from npub1... +``` + +Filters for `req`: `-k` kind, `-a` author, `-i` id, `-e` / `-p` / `-t` tag values (all repeatable), `-l` limit, `-s` / `-u` since and until in unix seconds, `--stream` to keep reading after stored events, `--timeout ` (default 30000). + +## Things that trip people up + +- `deed event -` needs the `-` to read drafts from stdin; without it, it signs one empty-content event. +- `deed verify` is silent on success. Check the exit code, not the output. +- Events from relays are verified and matched against the filter before they are printed, so a relay cannot slip in forged or unrelated events. Anything dropped is reported on stderr. +- A relay that does not accept the connection within five seconds (or `--timeout`, if shorter) is named on stderr and left out; the run carries on with the others. +- deed does not yet find an author's relays on its own: name the relays to ask. +- There is no Windows build yet. + +## More + +- Source and full reference: https://github.com/zig-nostr/deed +- Benchmarks: https://github.com/zig-nostr/deed/blob/main/BENCHMARKS.md +- The Zig library underneath: https://github.com/zig-nostr/nostr From ba31605ea7c1250a225ec72cd11b3d8790ab6993 Mon Sep 17 00:00:00 2001 From: sepehr-safari Date: Thu, 24 Sep 2026 11:57:55 +0300 Subject: [PATCH 2/2] chore(release): 0.3.2 The agent skill and plugin, and req and fetch bounded against a relay that pings and has stopped reading. --- .github/RELEASE_NOTES.md | 6 ++++++ build.zig.zon | 2 +- src/main.zig | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/RELEASE_NOTES.md b/.github/RELEASE_NOTES.md index d72b941..276b304 100644 --- a/.github/RELEASE_NOTES.md +++ b/.github/RELEASE_NOTES.md @@ -2,6 +2,12 @@ Every artifact below is published with a `.sha256` beside it, so the download can be checked against a digest that was written by the same job that built it. +### What's new in v0.3.2 + +**deed works well with AI agents.** A skill in `skills/deed` teaches an agent the commands, working recipes, and the two rules that matter: publishing is public and permanent, so it asks first, and a secret key stays in `NOSTR_SECRET_KEY`. Add it with `npx skills add zig-nostr/deed`, or in Claude Code as a plugin. `AGENTS.md` covers changing deed itself. + +**A relay that sends pings and has stopped reading no longer holds `req` or `fetch`.** Answering its pings filled a socket it never read, and the reply waited forever, past `--timeout`. The nostr library now bounds that reply by the read's deadline, and deed moves to that version. + ### What's new in v0.3.1 Faster, smaller, and measured. diff --git a/build.zig.zon b/build.zig.zon index 9087b76..75f0b92 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -1,6 +1,6 @@ .{ .name = .deed, - .version = "0.3.1", + .version = "0.3.2", .fingerprint = 0x89498c2094a1a4a3, .minimum_zig_version = "0.16.0", .dependencies = .{ diff --git a/src/main.zig b/src/main.zig index 11375a3..648edef 100644 --- a/src/main.zig +++ b/src/main.zig @@ -15,7 +15,7 @@ const cmd_publish = @import("cmd_publish.zig"); const cmd_req = @import("cmd_req.zig"); const cmd_verify = @import("cmd_verify.zig"); -pub const version = "0.3.1"; +pub const version = "0.3.2"; const usage = \\deed: the nostr command line