20 KiB
Nostr Terminal (nt) C99 Port - Planning Document
Overview
Recreating the Node.js-based nt (Nostr Terminal) in C99, leveraging nostr_core_lib for core Nostr functionality. The original implementation is in resources/NostrTerminal_5/nt.mjs.
Core Design Constraint: No Persistent Relay Pool
Every operation follows: connect → act → disconnect.
- No background sockets between commands
- No reconnection state machine
- Each menu action opens the relays it needs, performs the work, and closes them
- Live feeds are an explicit mode — entering it opens sockets; leaving it closes them
- Local SQLite is the only thing that persists between operations
This shapes every design choice below: there is no "pool object" to keep alive, no event loop running idle, no shared connection state across menus.
Architecture Overview
Core Dependencies
| Component | Purpose | Source |
|---|---|---|
| nostr_core_lib | Nostr protocol, crypto, relay pool, NIPs | resources/nostr_core_lib/ |
| cJSON | JSON parsing | Bundled in nostr_core_lib/cjson/ |
| OpenSSL | TLS for WebSockets | System library |
| SQLite3 | Local event/relay caching | System library |
| ncurses | Terminal UI | System library |
| libcurl | HTTP requests (AI APIs, NIP-11) | System library |
Global State (from original)
OBJ_KEYS- User keypair (nsec/npub hex/bech32)KIND_0- User metadataKIND_3- Follows listKIND_10002- Relay list metadataKIND_10096- Blossom media server listKIND_17375- Cashu wallet metadataKIND_PREFS(30078) - User preferences (AI model, mints)OBJ_PROOFS- Cashu proofs by mint- SQLite DB at
~/.nostr/nostr.db
Priority Legend
- 🔴 P0 - Critical: Must-have for MVP (basic Nostr client functionality)
- 🟠 P1 - High: Important features that complete the core experience
- 🟡 P2 - Medium: Nice-to-have, enhances usability
- 🟢 P3 - Low: Optional/experimental features
Status Legend
- ✅ Available in
nostr_core_lib - 🟦 Partially available (needs glue code)
- ❌ Missing from
nostr_core_lib(needs new implementation or external lib)
Menu Item Breakdown
🔴 P0 - [L]og in
Function: LogIn()
Description: Accept seed phrase, nsec, nsecHex, npub, bunker URL, or create new account. Derives keys via BIP39/BIP32 (path m/44'/1237'/account'/change/index).
| Sub-feature | nostr_core_lib Status | Notes |
|---|---|---|
| Generate mnemonic | ✅ nip006.h |
See mnemonic_generation.c |
| Validate mnemonic | ✅ nip006.h |
|
| Derive keys from seed | ✅ nip006.h |
See mnemonic_derivation.c |
| nsec/npub bech32 encoding | ✅ nip019.h |
|
| NIP-46 bunker login | ✅ nip046.h |
See nip46_remote_signer.c |
| Load user info from relays | ✅ Via relay pool | Need to fetch KIND 0, 3, 10002, 10096, 17375, 30078 |
🔴 P0 - [P]rofile
Function: Profile()
Description: Display and edit user metadata (KIND 0). Fields: name, displayname, about, picture, banner, website, lud16, nip05.
| Sub-feature | nostr_core_lib Status | Notes |
|---|---|---|
| Read KIND 0 | ✅ Via relay pool | |
| Publish KIND 0 | ✅ nip001.h |
|
| NIP-05 verification | ✅ nip005.h |
🔴 P0 - [R]elays
Function: EditRelays()
Description: Manage relay list (KIND 10002). Add/delete/modify relays, set read/write flags, update from nostr.watch, run kind-1 acceptance tests.
| Sub-feature | nostr_core_lib Status | Notes |
|---|---|---|
| Connect to relays | ✅ core_relay_pool.c |
See relay_pool.c |
| Publish KIND 10002 | ✅ | |
| Fetch NIP-11 info | ✅ nip011.h |
|
| Test relay (publish kind 1) | 🟦 | Need wrapper: timed publish + response capture |
| Fetch from nostr.watch API | ❌ | Need libcurl HTTP GET |
| Store relays in SQLite | ❌ | Custom SQLite layer needed |
🔴 P0 - [T]weet
Function: Tweet()
Description: Quick post a KIND 1 note. Optional vipe editor launch.
| Sub-feature | nostr_core_lib Status | Notes |
|---|---|---|
| Sign + publish KIND 1 | ✅ nip001.h |
|
| Launch external editor | ❌ | Need fork()/exec() wrapper for vipe |
🔴 P0 - [F]ollows
Function: Follows()
Description: Display/edit follows list (KIND 3). Add/delete follows, load follow profile info, view profiles.
| Sub-feature | nostr_core_lib Status | Notes |
|---|---|---|
| Read/Write KIND 3 | ✅ | |
| Fetch KIND 0 for follows | ✅ Via relay pool | |
| npub <-> hex conversion | ✅ nip019.h |
🟠 P1 - Li[v]e Feeds
Function: LiveFeed()
Description: Real-time event subscriptions. Sub-menu: Follows feed, all from/about follows, notifications, mentions, firehose.
| Sub-feature | nostr_core_lib Status | Notes |
|---|---|---|
| Subscribe to events | ✅ core_relay_pool.c |
|
| Multi-filter subscriptions | ✅ | |
| Live event callbacks | ✅ | |
| Cache events to SQLite | ❌ | Custom layer |
🟠 P1 - [N]otifications
Function: Notifications()
Description: Show recent events tagging the user (#p). Reactions (kind 7) and replies (kind 1) over last 3 days.
| Sub-feature | nostr_core_lib Status | Notes |
|---|---|---|
Query events by #p tag |
✅ Via relay pool filters | |
| Sort/group by date | ❌ | Application logic |
🟠 P1 - [W]rite
Function: Write()
Description: Launch vipe editor, then choose: save to diary (kind 30024), post new (kind 30023/30024), append to existing post, tweet (kind 1), or discard.
| Sub-feature | nostr_core_lib Status | Notes |
|---|---|---|
| Long-form notes (30023/30024) | ✅ Via generic publish | |
| NIP-04 encryption | ✅ nip004.h |
|
| Editor integration | ❌ | fork()/exec() for vipe |
🟠 P1 - P[o]sts
Function: EditNotes()
Description: Browse/edit user's notes by kind. Sub-menu: private blog (30024), public blog (30023), encrypted data (30078), all posts.
| Sub-feature | nostr_core_lib Status | Notes |
|---|---|---|
| Query addressable events (30000-39999) | ✅ | |
| NIP-04 encrypt/decrypt content + tags | ✅ nip004.h |
|
| Delete event (kind 5) | ✅ Via generic publish |
🟠 P1 - Direct [M]essage
Function: nip04Messaage()
Description: Send/receive encrypted DMs. Currently uses NIP-04, should use NIP-17 for new messages.
| Sub-feature | nostr_core_lib Status | Notes |
|---|---|---|
| NIP-04 DMs (legacy) | ✅ nip004.h |
|
| NIP-17 sealed DMs | ✅ nip017.h |
See send_nip17_dm.c |
| NIP-44 encryption | ✅ nip044.h |
|
| NIP-59 gift wrap | ✅ nip059.h |
🟡 P2 - To[d]o
Function: Todo()
Description: Personal todo list stored as encrypted KIND 30078 event with d=todo. Add, complete, reorder, delete items.
| Sub-feature | nostr_core_lib Status | Notes |
|---|---|---|
| Read/write KIND 30078 | ✅ | |
| NIP-04 encrypt JSON content | ✅ nip004.h |
🟡 P2 - D[i]ary
Function: Diary()
Description: Daily encrypted diary entry. KIND 30024 with d=YYYYMMDD, t=DD. Launches vipe.
| Sub-feature | nostr_core_lib Status | Notes |
|---|---|---|
| Same as Posts/Write | ✅ |
🟡 P2 - [E]cash (Cashu Wallet)
Function: CWallet()
Description: Chaumian eCash wallet via Cashu. Add/remove mints, request mint quotes, send/receive eCash tokens, manage proofs (KIND 7375), wallet metadata (KIND 17375).
| Sub-feature | nostr_core_lib Status | Notes |
|---|---|---|
| Cashu mint client | ✅ cashu_mint.h |
See cashu_wallet.c |
| NIP-60 wallet (kind 17375) | ✅ nip060.h |
|
| NIP-61 nutzaps | ✅ nip061.h |
|
| NIP-44 encrypt proofs | ✅ nip044.h |
|
| QR code generation | ❌ | Need libqrencode or port |
| Cashu token encode/decode (V4) | 🟦 | Verify in cashu_mint.c |
🟡 P2 - [A]I
Function: AI()
Description: Chat with AI models. Choose source (Venice.ai or local Ollama), list models, send prompts.
| Sub-feature | nostr_core_lib Status | Notes |
|---|---|---|
| HTTP POST to Venice/Ollama | ❌ | Need libcurl wrapper |
| Store prefs (KIND 30078, d=prefs) | ✅ | |
| NIP-04 encrypt prefs | ✅ nip004.h |
🟢 P3 - [C]ircus (AI Bot Troop)
Function: Circus()
Description: Generate N AI personas (using BIP32 derivation change=1), publish their KIND 0/3, have them tweet via AI prompts. Posts to local circus relay.
| Sub-feature | nostr_core_lib Status | Notes |
|---|---|---|
| Multi-account derivation | ✅ nip006.h |
|
| Publish to specific relay set | ✅ | |
| AI prompt fetch | ❌ | Same as AI menu |
🟢 P3 - Bl[o]ssom (Media Servers)
Function: Blossom()
Description: Manage Blossom media server list (KIND 10096). Add/delete/modify servers.
| Sub-feature | nostr_core_lib Status | Notes |
|---|---|---|
| Blossom client | ✅ blossom_client.h |
|
| Publish KIND 10096 | ✅ |
🟢 P3 - [DB] (Database Maintenance)
Function: DB()
Description: Bulk operations. Sub-menu: fetch new relays from nostr.watch, get NIP-11 info for all relays, run kind-1 tests, fetch info on all known people, fetch follows' kind-1 notes, fetch replies to follows.
| Sub-feature | nostr_core_lib Status | Notes |
|---|---|---|
| All bulk relay/event fetching | ✅ Via relay pool | |
| SQLite bulk inserts | ❌ | Custom layer |
🟢 P3 - [B]rowser
Function: LaunchBrowser()
Description: Launches brave-browser --app=https://localhost:PORT/nostr to view companion web UI.
| Sub-feature | nostr_core_lib Status | Notes |
|---|---|---|
| Launch external process | ❌ | fork()/exec() |
| HTTPS server | ❌ | Optional - skip unless web UI is desired |
🟢 P3 - [K]inds (Debug View)
Function: Inline in MainMenu() case "k"
Description: Dump all loaded KIND_* globals to screen for debugging.
| Sub-feature | nostr_core_lib Status | Notes |
|---|---|---|
| Print state | N/A | Trivial |
🔴 P0 - [Q]uit
Trivial - just exit cleanly.
Cross-Cutting Infrastructure (Required for ALL menus)
🔴 P0 - Terminal UI Layer
Status: ❌ Not in nostr_core_lib
Description: Replicate getLine(), getKey(), ClearScreen(), cl() from utilities.mjs. Custom rendering of ^_X^: highlighted hotkeys.
Library: ncurses or raw VT100 escape codes.
🔴 P0 - SQLite Layer
Status: ❌ Not in nostr_core_lib
Description: Replicate schema from BuildNostrSQLiteDB():
regular_events,replacable_events,addressable_events,ephemeral_eventsrelays(with NIP-11 fields, test results)relay_posts(publish history)local(key-value state)people(cached pubkeys/names)
🔴 P0 - HTTP Client
Status: 🟦 Partial - nostr_http.h exists
Description: Verify it supports arbitrary GET/POST with custom headers. May need libcurl for Venice/Ollama JSON APIs.
🔴 P0 - Per-Action Network Helpers (no global event loop)
Status: 🟦 nostr_core_lib provides the building blocks Description: Because of the connect → act → disconnect rule, we do not run a global event loop multiplexing background sockets. Instead we provide small synchronous helpers that each:
- Open the required relay sockets
- Send the request (EVENT/REQ)
- Pump messages until done (OK/EOSE/timeout)
- Close the sockets and return
Helpers to build:
nt_publish(event, relays, timeout)— connect → EVENT → wait OK → closent_query_sync(filter, relays, timeout, &events)— connect → REQ → collect until EOSE → CLOSE → close socketnt_get_first(filter, relays, timeout, &event)— same, returns first matchnt_live_feed(filters, relays, on_event_cb)— the one exception: blocking call used only inside the explicit "Live Feeds" menu; returns (and closes sockets) when the user hits a key
The TUI is purely synchronous — getLine() blocks on stdin between actions; no concurrent socket pumping while the user is typing.
🟡 P2 - External Process Spawning
Status: ❌ Not in nostr_core_lib
Description: Wrapper around fork()/exec()/pipe() for launching vipe editor and capturing stdout.
🟢 P3 - QR Code Rendering
Status: ❌ Not in nostr_core_lib
Description: For Cashu wallet (mint quotes, send tokens). Use libqrencode or port a small QR library to render to terminal.
🟢 P3 - HTTPS/WebSocket Server
Status: ❌ Not in nostr_core_lib
Description: For companion web UI (/nostr endpoint, WSS broadcast). Skip for MVP.
Missing from nostr_core_lib (Roadmap Items)
Based on nostr_core_lib/todo.md, the following are not yet complete in nostr_core_lib but needed:
-
NIP-01 Event Validation - Currently in progress (per
todo.md):nostr_validate_event_structure()- 🚧 declared, not implementednostr_verify_event_signature()- 🚧 declared, not implementednostr_validate_event()- 🚧 declared, not implemented- Impact: We need to validate incoming events from relays. Could work around by using Schnorr verify directly, but a complete API is preferred.
-
Higher-level relay query helpers - The original
pool.querySync()andpool.get()patterns aren't directly mirrored. We may need to build:nt_query_sync(relays, filter, timeout, &events)nt_get_first(relays, filter, timeout, &event)
-
Event deduplication for replaceable/addressable kinds - The original picks the latest by
created_at. Likely needs application-side logic. -
NIP-42 AUTH integration in pool - Per
plans/nip42_relay_pool_plan.md, this is planned but unverified.
Implementation Roadmap
Phase 1: Foundation (P0 Infrastructure)
- Set up CMake build with linkage to nostr_core_lib (static lib) — see
CMakeLists.txt - Implement TUI layer with ncurses — see
include/tui.handsrc/tui.c(raw VT100/termios, not ncurses, but functionally equivalent) - Implement SQLite layer (
db.c/h) with partial schema — seeinclude/db.handsrc/db.c(events / relays / people / local / relay_posts present; full original schema split into regular/replacable/addressable/ephemeral still pending) - Replace event-loop concept with synchronous per-action network helpers — see
include/net.h(nt_publish, nt_query_sync, nt_get_first all implemented) - Implement HTTP wrapper (libcurl) —
nt_fetch_nip11()is currently a stub returning -1; needed for NIP-11, nostr.watch, Venice/Ollama
Phase 2: Core Nostr Client (P0 Menus)
- [L]ogin (mnemonic) — see
src/menu_login.c(existing-account path: seed phrase / nsec / nsecHex / npub; new-account path; loads KIND 0/3/10002/10096/17375/30078) - [L]ogin bunker (NIP-46) sub-path — not yet wired
- [P]rofile (view + modify + publish KIND 0) — see
src/menu_profile.c - [R]elays (list / add / delete / modify / publish KIND 10002) — see
src/menu_relays.c(NIP-11 fetch is stubbed; nostr.watch sync and kind-1 acceptance test pending) - [T]weet (sign + publish KIND 1) — see
src/menu_tweet.c(novipeintegration yet) - [F]ollows (list / add / delete / publish KIND 3) — see
src/menu_follows.c(Profile sub-view from follows list pending) - [Q]uit
- [K]inds debug dump — implemented in
src/main.c
Phase 3: Reading & Interaction (P1 Menus)
- Li[v]e Feeds — needs a long-running
nt_live_feed()helper added tosrc/net.c - [N]otifications — query events with
#ptag, group/sort - [W]rite (with vipe integration) — needs external editor wrapper
- P[o]sts (browse / edit user notes by kind, NIP-04 decrypt for 30078)
- Direct [M]essage (NIP-17 send/receive, NIP-04 receive for legacy)
Phase 4: Advanced Features (P2 Menus)
- To[d]o (encrypted KIND 30078, d=todo)
- D[i]ary (KIND 30024 daily entries via vipe)
- [E]cash wallet (NIP-60/61, Cashu mint client, QR rendering)
- [A]I integration (Venice/Ollama via libcurl, prefs in KIND 30078)
Phase 5: Optional/Experimental (P3 Menus)
- [C]ircus (multi-account derivation + AI-driven posts)
- Bl[o]ssom (KIND 10096 manage media servers)
- [DB] maintenance (bulk relay + people + notes refresh)
- [B]rowser launcher
- HTTPS/WSS companion server
Current Status Snapshot (auto-summary)
Done — MVP usable: Build, TUI, SQLite scaffold, synchronous net helpers, login (seed-based), profile, relays (without NIP-11/test), tweet, follows, kinds dump, quit.
Immediate gaps blocking the rest of P0:
- HTTP client —
nt_fetch_nip11()is a stub. Without it, the relay menu can't show NIP-11 info, can't query nostr.watch, and the AI menu can't talk to Venice/Ollama. This is the single biggest unblocker. - Relay acceptance test — small wrapper around
nt_publishto publish a throwaway kind-1 to a single relay with a tight timeout; needed for theRelay menu's test feature. - NIP-46 bunker login — alternate auth path inside
src/menu_login.c.
Next P1 tickets in recommended order:
nt_live_feed(filters, relays, on_event_cb)insrc/net.c— blocking subscription that exits on any keypress; foundation for both Live Feeds and Notifications menus.- External editor wrapper (
fork/exec/pipetovipe, return captured stdout) — unlocksWrite,Diary, and richerTweet. - [N]otifications menu — straight
nt_query_syncover#pfilter on read relays, then group/sort. - P[o]sts menu — query addressable kinds (30023/30024/30078); NIP-04 decrypt for 30078.
- Li[v]e Feeds menu — built on
nt_live_feed; sub-options for follows/firehose/mentions. - Direct [M]essage — NIP-17 send via existing
nip017.h, plus inbox query.