v0.0.20 - Add Tier2/Tier3 Nostr tools: nip05 lookup, encode/decode, dm send, relay info, nip44 encrypt/decrypt, nip17 dm, list manage

This commit is contained in:
Your Name
2026-03-01 18:18:46 -04:00
parent a798f2c345
commit fea0fdf5c9
6 changed files with 1697 additions and 188 deletions

View File

@@ -1,247 +1,596 @@
# Implementation Plan: 4 New Nostr Tools
# Implementation Plan: Nostr Tools
## Overview
## Current Tool Inventory (v0.0.19)
Add four new tools to Didactyl's tool system. Each tool follows the same pattern established by existing tools in `src/tools.c`:
1. Register OpenAI function schema in `tools_build_openai_schema_json()`
2. Implement `execute_*()` function
3. Wire dispatch in `tools_execute()`
All four tools reuse existing infrastructure — no new `nostr_handler` APIs needed for the first three, and only a thin new wrapper for the fourth.
| # | Tool | Kind/NIP | Status |
|---|------|----------|--------|
| 1 | `nostr_post` | Any kind | ✅ Shipped |
| 2 | `nostr_post_readme` | 30023 / NIP-23 | ✅ Shipped |
| 3 | `nostr_query` | Filter-based | ✅ Shipped |
| 4 | `nostr_delete` | 5 / NIP-09 | ✅ Shipped |
| 5 | `nostr_react` | 7 / NIP-25 | ✅ Shipped |
| 6 | `nostr_profile_get` | 0 query | ✅ Shipped |
| 7 | `nostr_relay_status` | Pool stats | ✅ Shipped |
| 8 | `shell_exec` | OS | ✅ Shipped |
| 9 | `file_read` | OS | ✅ Shipped |
| 10 | `file_write` | OS | ✅ Shipped |
---
## Tool 1: `nostr_delete` — Event Deletion Request (NIP-09)
## Pattern for All New Tools
### Purpose
Let the agent retract or request deletion of events it previously published. Essential for self-correction.
Every tool follows the same three-step pattern in [`src/tools.c`](../src/tools.c):
### OpenAI Schema
```json
{
"name": "nostr_delete",
"description": "Request deletion of one or more previously published events (NIP-09 kind 5)",
"parameters": {
"type": "object",
"properties": {
"event_ids": {
"type": "array",
"items": { "type": "string" },
"description": "Array of event ID hex strings to request deletion for"
},
"kinds": {
"type": "array",
"items": { "type": "integer" },
"description": "Array of kind numbers corresponding to the events being deleted"
},
"reason": {
"type": "string",
"description": "Optional reason for the deletion request"
}
},
"required": ["event_ids"]
}
}
```
1. **Schema** — Add OpenAI function schema block in [`tools_build_openai_schema_json()`](../src/tools.c:511)
2. **Executor** — Implement `static char* execute_<name>()` function
3. **Dispatch** — Add `strcmp` case in [`tools_execute()`](../src/tools.c:1620)
### Implementation: `execute_nostr_delete()`
```
1. Parse args_json (with same hardened parsing as nostr_post)
2. Extract "event_ids" array (required) — validate each is a 64-char hex string
3. Extract "kinds" array (optional) — validate each is an integer
4. Extract "reason" string (optional) — use as content, default to empty string
5. Build tags array:
- For each event_id: add ["e", event_id]
- For each kind: add ["k", kind_as_string]
6. Call nostr_handler_publish_kind_event(5, reason, tags, &publish_result)
7. Return standard publish result JSON
```
### Files Modified
- `src/tools.c`: Add schema block in `tools_build_openai_schema_json()`, add `execute_nostr_delete()`, add dispatch case in `tools_execute()`
Use the shared [`parse_tool_args_json()`](../src/tools.c:527) helper for hardened argument parsing.
---
## Tool 2: `nostr_react` — Reactions (NIP-25)
## Tier 2 Tools — Medium Value, Moderate Implementation
### Purpose
Let the agent react to events — like, dislike, or emoji react. Gives the agent social presence.
### Tool 11: `nostr_nip05_lookup` — DNS Identity Verification (NIP-05)
### OpenAI Schema
#### Purpose
Verify `user@domain.com` NIP-05 identifiers. Useful for trust decisions and profile enrichment.
#### OpenAI Schema
```json
{
"name": "nostr_react",
"description": "React to a Nostr event with a like, dislike, or emoji (NIP-25 kind 7)",
"name": "nostr_nip05_lookup",
"description": "Verify or look up a NIP-05 DNS identifier and return the associated pubkey and relays",
"parameters": {
"type": "object",
"properties": {
"event_id": {
"identifier": {
"type": "string",
"description": "Hex event ID of the event to react to"
"description": "NIP-05 identifier, e.g. user@domain.com"
},
"event_pubkey": {
"type": "string",
"description": "Hex pubkey of the event author"
},
"event_kind": {
"type": "integer",
"description": "Kind number of the event being reacted to"
},
"reaction": {
"type": "string",
"description": "Reaction content: + for like, - for dislike, or an emoji. Default: +"
}
},
"required": ["event_id", "event_pubkey"]
}
}
```
### Implementation: `execute_nostr_react()`
```
1. Parse args_json
2. Extract "event_id" (required, 64-char hex)
3. Extract "event_pubkey" (required, 64-char hex)
4. Extract "event_kind" (optional integer)
5. Extract "reaction" (optional string, default "+")
6. Build tags array:
- ["e", event_id]
- ["p", event_pubkey]
- If event_kind provided: ["k", kind_as_string]
7. Call nostr_handler_publish_kind_event(7, reaction, tags, &publish_result)
8. Return standard publish result JSON
```
### Files Modified
- `src/tools.c`: Same three insertion points as above
---
## Tool 3: `nostr_profile_get` — Profile Lookup (kind 0)
### Purpose
Let the agent look up any user's profile metadata. Useful for WoT reasoning, greeting users by name, checking identity.
### OpenAI Schema
```json
{
"name": "nostr_profile_get",
"description": "Look up a Nostr user profile (kind 0 metadata) by pubkey",
"parameters": {
"type": "object",
"properties": {
"pubkey": {
"type": "string",
"description": "Hex public key of the user to look up"
"description": "Optional 64-char hex pubkey to verify against the identifier"
}
},
"required": ["pubkey"]
"required": ["identifier"]
}
}
```
### Implementation: `execute_nostr_profile_get()`
#### Implementation: `execute_nostr_nip05_lookup()`
```
1. Parse args_json
2. Extract "pubkey" (required, 64-char hex)
3. Build filter:
{
"kinds": [0],
"authors": [pubkey],
"limit": 1
}
4. Call nostr_handler_query_json(filter, 8000)
5. Parse the returned events JSON
6. If events found:
- Extract the first event's content (which is a JSON string of profile fields)
- Parse that content JSON
- Return { success: true, pubkey: ..., profile: { name, display_name, about, picture, nip05, ... } }
7. If no events: return { success: true, pubkey: ..., profile: null }
2. Extract "identifier" (required string, must contain @)
3. Extract "pubkey" (optional 64-char hex)
4. If pubkey provided:
- Call nostr_nip05_verify(identifier, pubkey, &relays, &relay_count, 10)
- Return { success, verified: true/false, pubkey, relays }
5. If no pubkey:
- Call nostr_nip05_lookup(identifier, pubkey_out, &relays, &relay_count, 10)
- Return { success, pubkey: pubkey_out, relays }
6. Free relay array after serializing
```
### Files Modified
- `src/tools.c`: Same three insertion points
#### Library API Used
- [`nostr_nip05_lookup()`](../nostr_core_lib/nostr_core/nip005.h:13)
- [`nostr_nip05_verify()`](../nostr_core_lib/nostr_core/nip005.h:15)
#### Files Modified
- `src/tools.c`: Schema + executor + dispatch
#### Notes
- Involves HTTP network I/O (fetches `/.well-known/nostr.json` from the domain)
- May block for up to `timeout_seconds` — use 10s default
- Must free the `relays` array returned by the library
---
## Tool 4: `nostr_relay_status` — Relay Health Dashboard
### Tool 12: `nostr_encode` — Bech32 Entity Encoding (NIP-19/NIP-21)
### Purpose
Let the agent introspect its own relay connectivity. Useful for self-diagnostics and admin status reports.
#### Purpose
Encode hex pubkeys, event IDs, or addressable coordinates into shareable `npub`, `note`, `nprofile`, `nevent`, `naddr` URIs.
### OpenAI Schema
#### OpenAI Schema
```json
{
"name": "nostr_relay_status",
"description": "Get connection status and statistics for all connected relays",
"name": "nostr_encode",
"description": "Encode a Nostr entity into a bech32 URI: npub, note, nprofile, nevent, or naddr",
"parameters": {
"type": "object",
"properties": {}
"properties": {
"type": {
"type": "string",
"description": "Entity type: npub, note, nprofile, nevent, or naddr"
},
"hex": {
"type": "string",
"description": "64-char hex value: pubkey for npub/nprofile, event_id for note/nevent"
},
"relays": {
"type": "array",
"items": { "type": "string" },
"description": "Optional relay hints for nprofile, nevent, naddr"
},
"kind": {
"type": "integer",
"description": "Kind number, required for naddr"
},
"identifier": {
"type": "string",
"description": "d-tag identifier, required for naddr"
}
},
"required": ["type", "hex"]
}
}
```
### Implementation
This tool needs access to the relay pool, which is currently a `static` global in `nostr_handler.c`. Two options:
**Option A (preferred):** Add a new function to `nostr_handler.h`:
```c
char* nostr_handler_relay_status_json(void);
```
This function iterates the pool's relays using `nostr_relay_pool_list_relays()` and `nostr_relay_pool_get_relay_stats()`, builds a JSON array, and returns it.
**Option B:** Expose the pool pointer — not recommended, breaks encapsulation.
### `nostr_handler_relay_status_json()` in `nostr_handler.c`:
#### Implementation: `execute_nostr_encode()`
```
1. Call nostr_relay_pool_list_relays(g_pool, &urls, &statuses)
2. For each relay:
a. Get stats via nostr_relay_pool_get_relay_stats(g_pool, url)
b. Build JSON object with:
- url
- status (disconnected/connecting/connected/error)
- ping_latency_ms
- events_received
- events_published
- events_published_ok
- events_published_failed
- connection_uptime_start
3. Return JSON array as string
1. Parse args_json
2. Extract "type" (required: npub|note|nprofile|nevent|naddr)
3. Extract "hex" (required, 64-char hex)
4. Convert hex to 32-byte array via nostr_hex_to_bytes()
5. Switch on type:
- "npub": nostr_build_uri_npub(bytes, output, sizeof output)
- "note": nostr_build_uri_note(bytes, output, sizeof output)
- "nprofile": nostr_build_uri_nprofile(bytes, relays, relay_count, output, sizeof output)
- "nevent": nostr_build_uri_nevent(bytes, relays, relay_count, NULL, -1, 0, output, sizeof output)
- "naddr": nostr_build_uri_naddr(identifier, bytes, kind, relays, relay_count, output, sizeof output)
6. Return { success, type, uri: output }
```
### `execute_nostr_relay_status()` in `tools.c`:
#### Library API Used
- [`nostr_build_uri_npub()`](../nostr_core_lib/nostr_core/nip021.h:66)
- [`nostr_build_uri_note()`](../nostr_core_lib/nostr_core/nip021.h:68)
- [`nostr_build_uri_nprofile()`](../nostr_core_lib/nostr_core/nip021.h:69)
- [`nostr_build_uri_nevent()`](../nostr_core_lib/nostr_core/nip021.h:71)
- [`nostr_build_uri_naddr()`](../nostr_core_lib/nostr_core/nip021.h:74)
```
1. Parse args_json (accept empty/no args)
2. Call nostr_handler_relay_status_json()
3. Wrap in { success: true, relays: [...] }
4. Return
#### Files Modified
- `src/tools.c`: Schema + executor + dispatch
---
### Tool 13: `nostr_decode` — Bech32 Entity Decoding (NIP-19/NIP-21)
#### Purpose
Decode `npub`, `note`, `nprofile`, `nevent`, `naddr` URIs back into hex values and metadata.
#### OpenAI Schema
```json
{
"name": "nostr_decode",
"description": "Decode a bech32 Nostr URI into its components: type, hex, relays, kind, identifier",
"parameters": {
"type": "object",
"properties": {
"uri": {
"type": "string",
"description": "Bech32 URI to decode, e.g. npub1..., note1..., nostr:npub1..."
}
},
"required": ["uri"]
}
}
```
### Files Modified
- `src/nostr_handler.h`: Add `nostr_handler_relay_status_json()` declaration
- `src/nostr_handler.c`: Implement `nostr_handler_relay_status_json()`
- `src/tools.c`: Schema + execute + dispatch
#### Implementation: `execute_nostr_decode()`
```
1. Parse args_json
2. Extract "uri" (required string)
3. Call nostr_parse_uri(uri, &result)
4. Switch on result.type:
- NPUB: convert result.data.pubkey to hex, return { type: "npub", pubkey: hex }
- NOTE: convert result.data.event_id to hex, return { type: "note", event_id: hex }
- NPROFILE: pubkey hex + relays array
- NEVENT: event_id hex + relays + optional author/kind
- NADDR: identifier + pubkey hex + kind + relays
5. Call nostr_uri_result_free(&result)
6. Return JSON
```
#### Library API Used
- [`nostr_parse_uri()`](../nostr_core_lib/nostr_core/nip021.h:63)
- [`nostr_uri_result_free()`](../nostr_core_lib/nostr_core/nip021.h:78)
#### Files Modified
- `src/tools.c`: Schema + executor + dispatch
---
### Tool 14: `nostr_dm_send` — Send DM as Tool Call
#### Purpose
Expose DM sending as a tool so the agent can proactively message people (notify admin, reach out to contacts).
#### OpenAI Schema
```json
{
"name": "nostr_dm_send",
"description": "Send an encrypted direct message to a Nostr user (NIP-04)",
"parameters": {
"type": "object",
"properties": {
"recipient_pubkey": {
"type": "string",
"description": "64-char hex pubkey of the recipient"
},
"message": {
"type": "string",
"description": "Plaintext message to send"
}
},
"required": ["recipient_pubkey", "message"]
}
}
```
#### Implementation: `execute_nostr_dm_send()`
```
1. Parse args_json
2. Extract "recipient_pubkey" (required, 64-char hex)
3. Extract "message" (required, non-empty string)
4. Call nostr_handler_send_dm(recipient_pubkey, message)
5. Return { success: true/false, recipient_pubkey, message_length }
```
#### Library API Used
- [`nostr_handler_send_dm()`](../src/nostr_handler.h:33) — already exists
#### Security Note
- Should be admin-tier only in practice. The tool itself doesn't enforce tier, but the agent's system prompt and skill definitions should restrict usage.
#### Files Modified
- `src/tools.c`: Schema + executor + dispatch
---
### Tool 15: `nostr_relay_info` — Relay Information Document (NIP-11)
#### Purpose
Inspect relay capabilities, supported NIPs, limitations, PoW requirements before publishing.
#### OpenAI Schema
```json
{
"name": "nostr_relay_info",
"description": "Fetch the NIP-11 relay information document for a relay URL",
"parameters": {
"type": "object",
"properties": {
"relay_url": {
"type": "string",
"description": "WebSocket relay URL, e.g. wss://relay.example.com"
}
},
"required": ["relay_url"]
}
}
```
#### Implementation
This requires a new helper in [`nostr_handler.c`](../src/nostr_handler.c) since NIP-11 fetching needs an HTTP GET request with `Accept: application/nostr+json` header.
##### New function: `nostr_handler_relay_info_json()`
```
1. Convert ws:// or wss:// URL to http:// or https://
2. Use libcurl to HTTP GET with header "Accept: application/nostr+json"
3. Return the raw JSON response string
```
##### `execute_nostr_relay_info()` in tools.c:
```
1. Parse args_json
2. Extract "relay_url" (required string, must start with ws:// or wss://)
3. Call nostr_handler_relay_info_json(relay_url)
4. Parse returned JSON
5. Return { success, relay_url, info: { name, description, pubkey, supported_nips, limitations, ... } }
```
#### Files Modified
- `src/nostr_handler.h`: Add `nostr_handler_relay_info_json()` declaration
- `src/nostr_handler.c`: Implement using libcurl (already linked)
- `src/tools.c`: Schema + executor + dispatch
---
## Tier 3 Tools — High Value, More Complex
### Tool 16: `nostr_dm_send_nip17` — Private DMs via Gift Wrap (NIP-17/NIP-59)
#### Purpose
Send modern, metadata-private DMs using the NIP-17 gift wrap protocol instead of legacy NIP-04.
#### OpenAI Schema
```json
{
"name": "nostr_dm_send_nip17",
"description": "Send a private direct message using NIP-17 gift wrap protocol for metadata privacy",
"parameters": {
"type": "object",
"properties": {
"recipient_pubkey": {
"type": "string",
"description": "64-char hex pubkey of the recipient"
},
"message": {
"type": "string",
"description": "Plaintext message to send"
},
"subject": {
"type": "string",
"description": "Optional conversation subject"
}
},
"required": ["recipient_pubkey", "message"]
}
}
```
#### Implementation
This is a multi-step crypto pipeline. The library handles the heavy lifting but we need to orchestrate:
##### New function: `nostr_handler_send_dm_nip17()` in nostr_handler.c
```
1. Query recipient's kind 10050 relay list (DM relays)
- Build filter: { kinds: [10050], authors: [recipient_pubkey], limit: 1 }
- Call nostr_relay_pool_query_sync()
- Extract relay URLs via nostr_nip17_extract_dm_relays()
- Fall back to our own relays if no 10050 found
2. Create kind 14 chat event:
- nostr_nip17_create_chat_event(message, &recipient_pubkey, 1, subject, NULL, NULL, our_pubkey)
3. Gift wrap and send:
- nostr_nip17_send_dm(chat_event, &recipient_pubkey, 1, our_private_key, gift_wraps, max_wraps, 0)
4. Publish each gift wrap to recipient's DM relays
5. Return success/failure + event metadata
```
##### `execute_nostr_dm_send_nip17()` in tools.c:
```
1. Parse args_json
2. Extract recipient_pubkey, message, optional subject
3. Call nostr_handler_send_dm_nip17(recipient_pubkey, message, subject)
4. Return { success, recipient_pubkey, protocol: "nip17" }
```
#### Library API Used
- [`nostr_nip17_create_chat_event()`](../nostr_core_lib/nostr_core/nip017.h:28)
- [`nostr_nip17_send_dm()`](../nostr_core_lib/nostr_core/nip017.h:100)
- [`nostr_nip17_extract_dm_relays()`](../nostr_core_lib/nostr_core/nip017.h)
- [`nostr_nip59_create_gift_wrap()`](../nostr_core_lib/nostr_core/nip059.h:50)
#### Files Modified
- `src/nostr_handler.h`: Add `nostr_handler_send_dm_nip17()` declaration
- `src/nostr_handler.c`: Implement the full NIP-17 send pipeline
- `src/tools.c`: Schema + executor + dispatch
---
### Tool 17: `nostr_encrypt` — NIP-44 Encryption
#### Purpose
Encrypt arbitrary content for a specific recipient using modern NIP-44 encryption.
#### OpenAI Schema
```json
{
"name": "nostr_encrypt",
"description": "Encrypt plaintext for a recipient using NIP-44 ChaCha20+HMAC encryption",
"parameters": {
"type": "object",
"properties": {
"recipient_pubkey": {
"type": "string",
"description": "64-char hex pubkey of the recipient"
},
"plaintext": {
"type": "string",
"description": "Text to encrypt"
}
},
"required": ["recipient_pubkey", "plaintext"]
}
}
```
#### Implementation: `execute_nostr_encrypt()`
```
1. Parse args_json
2. Extract recipient_pubkey (64-char hex), plaintext (non-empty string)
3. Convert recipient_pubkey hex to 32-byte array
4. Allocate output buffer (plaintext length * 2 + 256 for base64 overhead)
5. Call nostr_nip44_encrypt(our_private_key, recipient_pubkey_bytes, plaintext, output, output_size)
6. Return { success, recipient_pubkey, ciphertext: output }
```
#### Library API Used
- [`nostr_nip44_encrypt()`](../nostr_core_lib/nostr_core/nip044.h:28)
#### Security Note
- Admin-tier only. Uses the agent's private key for ECDH shared secret.
#### Files Modified
- `src/tools.c`: Schema + executor + dispatch
---
### Tool 18: `nostr_decrypt` — NIP-44 Decryption
#### Purpose
Decrypt NIP-44 encrypted content from a known sender.
#### OpenAI Schema
```json
{
"name": "nostr_decrypt",
"description": "Decrypt NIP-44 encrypted content from a sender",
"parameters": {
"type": "object",
"properties": {
"sender_pubkey": {
"type": "string",
"description": "64-char hex pubkey of the sender"
},
"ciphertext": {
"type": "string",
"description": "Base64-encoded NIP-44 encrypted payload"
}
},
"required": ["sender_pubkey", "ciphertext"]
}
}
```
#### Implementation: `execute_nostr_decrypt()`
```
1. Parse args_json
2. Extract sender_pubkey (64-char hex), ciphertext (non-empty string)
3. Convert sender_pubkey hex to 32-byte array
4. Allocate output buffer
5. Call nostr_nip44_decrypt(our_private_key, sender_pubkey_bytes, ciphertext, output, output_size)
6. Return { success, sender_pubkey, plaintext: output }
```
#### Library API Used
- [`nostr_nip44_decrypt()`](../nostr_core_lib/nostr_core/nip044.h:62)
#### Security Note
- Admin-tier only.
#### Files Modified
- `src/tools.c`: Schema + executor + dispatch
---
### Tool 19: `nostr_list_manage` — List Management (NIP-51)
#### Purpose
Manage mute lists (kind 10000), bookmarks (kind 10003), follow sets, relay sets. Lets the agent curate its social graph.
#### OpenAI Schema
```json
{
"name": "nostr_list_manage",
"description": "Add or remove items from a Nostr list: mute list, bookmarks, relay sets, etc. (NIP-51)",
"parameters": {
"type": "object",
"properties": {
"list_kind": {
"type": "integer",
"description": "Kind number of the list: 10000=mute, 10001=pins, 10003=bookmarks, 3=follows, 10002=relays"
},
"action": {
"type": "string",
"description": "Action: add or remove"
},
"items": {
"type": "array",
"items": {
"type": "array",
"items": { "type": "string" }
},
"description": "Array of tag tuples to add/remove, e.g. [[p, pubkey], [e, event_id]]"
}
},
"required": ["list_kind", "action", "items"]
}
}
```
#### Implementation
This is a read-modify-write pattern for replaceable events:
##### `execute_nostr_list_manage()`:
```
1. Parse args_json
2. Extract list_kind (integer), action ("add"|"remove"), items (array of tag arrays)
3. Query existing list:
- Build filter: { kinds: [list_kind], authors: [our_pubkey], limit: 1 }
- Call nostr_handler_query_json(filter, 8000)
4. Parse existing event tags (or start with empty array if none found)
5. If action == "add":
- For each item in items: append to tags if not already present
6. If action == "remove":
- For each item in items: remove matching tag from tags
7. Publish updated event:
- Call nostr_handler_publish_kind_event(list_kind, existing_content_or_empty, updated_tags, &result)
8. Return { success, list_kind, action, items_affected, event_id }
```
#### Notes
- Replaceable events (kind 10000-19999) automatically replace previous versions
- Kind 3 (follows) is also replaceable
- Content field may contain NIP-44 encrypted private items — preserve it unchanged
- Must deduplicate tags when adding
#### Files Modified
- `src/tools.c`: Schema + executor + dispatch
---
## Implementation Order
1. **`nostr_delete`** — simplest, pure tag construction + existing publish
2. **`nostr_react`** — same pattern, slightly different tags
3. **`nostr_profile_get`** — uses existing query path, adds profile content parsing
4. **`nostr_relay_status`** — requires one new `nostr_handler` function
```mermaid
graph TD
A[nostr_nip05_lookup] --> B[nostr_encode]
B --> C[nostr_decode]
C --> D[nostr_dm_send]
D --> E[nostr_relay_info]
E --> F[nostr_encrypt]
F --> G[nostr_decrypt]
G --> H[nostr_dm_send_nip17]
H --> I[nostr_list_manage]
style A fill:#FFFFE0
style B fill:#FFFFE0
style C fill:#FFFFE0
style D fill:#FFFFE0
style E fill:#FFFFE0
style F fill:#FFD700
style G fill:#FFD700
style H fill:#FFD700
style I fill:#FFD700
```
All four schemas should be added to `tools_build_openai_schema_json()` in a single pass, and all four dispatch cases added to `tools_execute()` in a single pass, to minimize diff churn.
Yellow = Tier 2 (moderate), Gold = Tier 3 (complex).
### Recommended batching:
**Batch A — Pure computation, no new handler functions:**
1. `nostr_encode` — pure NIP-21 encoding
2. `nostr_decode` — pure NIP-21 decoding
3. `nostr_dm_send` — thin wrapper around existing `nostr_handler_send_dm()`
**Batch B — Network I/O tools:**
4. `nostr_nip05_lookup` — HTTP fetch via library
5. `nostr_relay_info` — HTTP fetch via new handler function + libcurl
**Batch C — Crypto tools:**
6. `nostr_encrypt` — NIP-44 encrypt
7. `nostr_decrypt` — NIP-44 decrypt
**Batch D — Complex orchestration:**
8. `nostr_dm_send_nip17` — multi-step NIP-17/59 pipeline, new handler function
9. `nostr_list_manage` — read-modify-write replaceable events
## Build & Validate
After all four tools are implemented:
After each batch:
1. Run `./build_static.sh` to verify compilation
2. Run `./increment_and_push.sh "Add nostr_delete, nostr_react, nostr_profile_get, nostr_relay_status tools"`
2. Run `./increment_and_push.sh` with descriptive message