Compare commits

...

12 Commits

Author SHA1 Message Date
Anthony Accioly dc8e3d1479
docs(readme): add bunker subcommands documentation
- Document new bunker subcommands, including `help`, `info`, `qr`, and `connect`.
- Update examples for displaying bunker QR codes and usage instructions.
2025-08-22 00:51:18 +01:00
Anthony Accioly c31b45e996
feat(bunker): delay bunker info print after direct connect response
- Introduce a 3-second delay before calling `printBunkerInfo`
2025-08-22 00:51:18 +01:00
Anthony Accioly 790e39e76c
feat(bunker): add bunker info print after direct connect response
- Call `printBunkerInfo` to display bunker details after sending connect
 response
2025-08-22 00:51:18 +01:00
Anthony Accioly acfb2ecc3e
feat(bunker): publish bunker connect responses concurrently
- Introduce goroutines to handle concurrent publishing to target relays.
- Use `sync/atomic` for thread-safe success counter tracking
- Add `WaitGroup` to ensure all goroutines complete before proceeding.
2025-08-22 00:51:18 +01:00
Anthony Accioly 761f980e27
refactor(bunker): manage subscriptions dynamically for relay updates
- Refactor subscription management to allow dynamic relay updates.
- Introduce cancelable context and lock mechanism to safely recreate subscriptions.
- Automatically update subscriptions when relay list changes.
2025-08-22 00:51:17 +01:00
Anthony Accioly d7b83260b5
feat(bunker): add NostrConnect `connect` command support to bunker
- Implement `connect` command to enable processing `nostrconnect://` URIs.
- Update configuration dynamically with new relays and authorized keys.
2025-08-22 00:51:17 +01:00
Anthony Accioly 989f5e2194
refactor(bunker): remove unused NostrConnect `connect` command 2025-08-22 00:51:17 +01:00
Anthony Accioly 706d37be28
feat(bunker): introduce command interface with QR code and help commands
- Add interactive command interface for the bunker.
- Implement `help`, `info`, `qr`, and `exit` commands for user convenience.
- Display QR code on demand using `qr` command.
- Ensure proper command handling with locking mechanism and safe shutdown using `exit`.
2025-08-22 00:51:17 +01:00
fiatjaf b316646821 new release with updated dependencies. 2025-08-18 21:01:52 -03:00
fiatjaf d3975679e4 add labels to subscriptions for easier debugging. 2025-08-14 13:28:15 -03:00
fiatjaf 23e27da077 use isatty for detecting stuff for the fancy output (it doesn't work). 2025-08-14 13:28:15 -03:00
fiatjaf 1a221a133c cleanup and fix readme. 2025-08-14 13:28:15 -03:00
9 changed files with 417 additions and 124 deletions

View File

@ -3,6 +3,8 @@
install with `go install github.com/fiatjaf/nak@latest` or
[download a binary](https://github.com/fiatjaf/nak/releases).
or get the source with `git clone https://github.com/fiatjaf/nak` then install with `go install` or run with docker using `docker build -t nak . && docker run nak event`.
## what can you do with it?
take a look at the help text that comes in it to learn all possibilities, but here are some:
@ -128,17 +130,13 @@ type the password to decrypt your secret key: **********
985d66d2644dfa7676e26046914470d66ebc7fa783a3f57f139fde32d0d631d7
```
### sign an event using [Amber](https://github.com/greenart7c3/Amber) (or other bunker provider)
### sign an event using a bunker provider (amber, promenade etc)
```shell
~> export NOSTR_CLIENT_KEY="$(nak key generate)"
~> nak event --sec 'bunker://a9e0f110f636f3191644110c19a33448daf09d7cda9708a769e91b7e91340208?relay=wss%3A%2F%2Frelay.damus.io&relay=wss%3A%2F%2Frelay.nsecbunker.com&relay=wss%3A%2F%2Fnos.lol&secret=TWfGbjQCLxUf' -c 'hello from bunker'
```
> [!IMPORTANT]
> Remember to set a `NOSTR_CLIENT_KEY` permanently on your shell, otherwise you'll only be able to use the bunker once. For `bash`:
> ```shell
> echo 'export NOSTR_CLIENT_KEY="$(nak key generate)"' >> ~/.bashrc
> ```
(in most cases it's better to set `NOSTR_CLIENT_KEY` permanently on your shell, as that identity will be recorded by the bunker provider.)
### sign an event using a NIP-49 encrypted key
```shell
@ -174,22 +172,41 @@ listening at [wss://relay.damus.io wss://nos.lol wss://relay.nsecbunker.com]:
bunker: bunker://f59911b561c37c90b01e9e5c2557307380835c83399756f4d62d8167227e420a?relay=wss%3A%2F%2Frelay.damus.io&relay=wss%3A%2F%2Fnos.lol&relay=wss%3A%2F%2Frelay.nsecbunker.com&secret=XuuiMbcLwuwL
```
you can also display a QR code for the bunker URI by adding the `--qrcode` flag:
#### Bunker subcommands
Bunker has a few subcommands that you can use to manage it, type `help` to see them all:
```shell
~> ./nak bunker relay.nsec.app
wss://relay.nsec.app... ok.
listening at [wss://relay.nsec.app]:
pubkey: f59911b561c37c90b01e9e5c2557307380835c83399756f4d62d8167227e420a
npub: npub17kv3rdtpcd7fpvq7newz24eswwqgxhyr8xt4daxk9kqkwgn7gg9q4gy8vf
to restart: nak bunker relay.nsec.app
bunker: bunker://f59911b561c37c90b01e9e5c2557307380835c83399756f4d62d8167227e420a?relay=wss%3A%2F%2Frelay.nsec.app&secret=cAMoUOddVMla
--------------- Bunker Command Interface ---------------
Type 'help' for available commands or 'exit' to quit.
--------------------------------------------------------
help
Available Commands:
help, h, ? - Show this help message
info, i - Display current bunker information
qr - Generate and display QR code for the bunker URI
connect, c <nostrconnect://uri> - Connect to a remote client using nostrconnect:// URI
exit, quit, q - Shutdown the bunker
```
You can also display a QR code for the bunker URI by adding the `--qrcode` flag:
```shell
~> nak bunker --qrcode --sec ncryptsec1... relay.damus.io
```
### start a bunker that persists its metadata to disc
### start a bunker that persists its metadata (secret key, relays, authorized client pubkeys) to disc
```shell
~> nak bunker --persist --sec ncryptsec1... relay.nsec.app nos.lol
```
> [!CAUTION]
> when you start a bunker with `--persist` or `--profile`, it will store `--sec` credentials and authorized keys in
> `~/.config/nak/bunker`. if you don't want your private key to be stored in plain text, you can
> [encrypt it with NIP-49](#encrypt-key-with-nip-49) it beforehand.
```shell
then later just
@ -250,7 +267,7 @@ type the password to decrypt your secret key: ********
~> nak req -i 412f2d3e73acc312942c055ac2a695dc60bf58ff97e06689a8a79e97796c4cdb relay.westernbtc.com | jq -r .content > ~/.jq
```
### watch a NIP-53 livestream (zap.stream etc)
### watch a NIP-53 livestream (zap.stream, amethyst, shosho etc)
```shell
~> # this requires the jq utils from the step above
~> mpv $(nak fetch naddr1qqjxvvm9xscnsdtx95cxvcfk956rsvtx943rje3k95mx2dp389jnwwrp8ymxgqg4waehxw309aex2mrp0yhxgctdw4eju6t09upzpn6956apxcad0mfp8grcuugdysg44eepex68h50t73zcathmfs49qvzqqqrkvu7ed38k | jq -r 'tag_value("streaming")')
@ -307,20 +324,12 @@ echo "#surely you're joking, mr npub1l2vyh47mk2p0qlsku7hg0vn29faehy9hy34ygaclpn6
# and there is a --confirm flag that gives you a chance to confirm before actually publishing the result to relays.
```
### record and publish an audio note of 10s (yakbak etc) signed from a bunker
### record and publish an audio note (yakbak, nostur etc) signed from a bunker
```shell
ffmpeg -f alsa -i default -f webm -t 00:00:03 pipe:1 | nak blossom --server blossom.primal.net upload | jq -rc '{content: .url}' | nak event -k 1222 --sec 'bunker://urlgoeshere' pyramid.fiatjaf.com nostr.wine
```
### from a file with events get only those that have kind 1111 and were created by a given pubkey
```shell
~> cat all.jsonl | nak filter -k 1111 > filtered.jsonl
```
### run nak in Docker
If you want to run nak inside a container (i.e. to run nak as a server, or to avoid installing the Go toolchain) you can run it with Docker:
```shell
docker build -t nak .
docker run nak event
~> cat all.jsonl | nak filter -k 1111 -a 117673e191b10fe1aedf1736ee74de4cffd4c132ca701960b70a5abad5870faa > filtered.jsonl
```

433
bunker.go
View File

@ -1,9 +1,11 @@
package main
import (
"bufio"
"bytes"
"context"
"encoding/hex"
"fiatjaf.com/nostr/nip44"
"fmt"
"net/url"
"os"
@ -11,6 +13,7 @@ import (
"slices"
"strings"
"sync"
"sync/atomic"
"time"
"fiatjaf.com/nostr"
@ -229,6 +232,67 @@ var bunker = &cli.Command{
// static information
pubkey := sec.Public()
npub := nip19.EncodeNpub(pubkey)
signer := nip46.NewStaticKeySigner(sec)
printLock := sync.Mutex{}
exitChan := make(chan bool, 1)
// subscription management
var events chan nostr.RelayEvent
var cancelSubscription context.CancelFunc
subscriptionMutex := sync.Mutex{}
// Function to create/recreate subscription
updateSubscription := func() {
subscriptionMutex.Lock()
defer subscriptionMutex.Unlock()
// Cancel existing subscription if it exists
if cancelSubscription != nil {
cancelSubscription()
}
// Create new context for the subscription
subCtx, cancel := context.WithCancel(ctx)
cancelSubscription = cancel
// Create new subscription with current relay list
events = sys.Pool.SubscribeMany(subCtx, relayURLs, nostr.Filter{
Kinds: []nostr.Kind{nostr.KindNostrConnect},
Tags: nostr.TagMap{"p": []string{pubkey.Hex()}},
Since: nostr.Now(),
LimitZero: true,
}, nostr.SubscriptionOptions{
Label: "nak-bunker",
})
}
// Initial subscription to relays
updateSubscription()
handlerWg := sync.WaitGroup{}
// == SUBCOMMANDS ==
// printHelp displays available commands for the bunker interface
printHelp := func() {
log("%s\n", color.CyanString("Available Commands:"))
log(" %s - Show this help message\n", color.GreenString("help, h, ?"))
log(" %s - Display current bunker information\n", color.GreenString("info, i"))
log(" %s - Generate and display QR code for the bunker URI\n", color.GreenString("qr"))
log(" %s - Connect to a remote client using nostrconnect:// URI\n", color.GreenString("connect, c <nostrconnect://uri>"))
log(" %s - Shutdown the bunker\n", color.GreenString("exit, quit, q"))
log("\n")
}
// Create a function to print QR code on demand
printQR := func() {
qs.Set("secret", newSecret)
bunkerURI := fmt.Sprintf("bunker://%s?%s", pubkey.Hex(), qs.Encode())
printLock.Lock()
log("\nQR Code for bunker URI:\n")
qrterminal.Generate(bunkerURI, qrterminal.L, os.Stdout)
log("\n\n")
printLock.Unlock()
}
// this function will be called every now and then
printBunkerInfo := func() {
@ -301,27 +365,224 @@ var bunker = &cli.Command{
// print QR code if requested
if c.Bool("qrcode") {
log("QR Code for bunker URI:\n")
qrterminal.Generate(bunkerURI, qrterminal.L, os.Stdout)
log("\n\n")
printQR()
}
}
// handleConnect processes nostrconnect:// URIs for direct connection flow
handleConnect := func(connectURI string) {
if !strings.HasPrefix(connectURI, "nostrconnect://") {
log("Error: URI must start with nostrconnect://\n")
return
}
// Parse the nostrconnect URI
u, err := url.Parse(connectURI)
if err != nil {
log("Error: Invalid nostrconnect URI: %v\n", err)
return
}
// Extract client pubkey from the URI
clientPubkeyHex := u.Host
if clientPubkeyHex == "" {
log("Error: Missing client pubkey in URI\n")
return
}
clientPubkey, err := nostr.PubKeyFromHex(clientPubkeyHex)
if err != nil {
log("Error: Invalid client pubkey: %v\n", err)
return
}
// Extract relays from query parameters
queryParams := u.Query()
newRelayURLs := queryParams["relay"]
if len(newRelayURLs) == 0 {
log("Error: No relays specified in URI\n")
return
}
// Extract secret from query parameters (optional)
var secret string
secrets := queryParams["secret"]
if len(secrets) > 0 {
secret = secrets[0]
} else {
// log error and return if no secret is provided
log("Error: No secret provided in URI\n")
return
}
log("Parsed nostrconnect URI:\n")
log(" Client pubkey: %s\n", color.CyanString(clientPubkey.Hex()))
log(" Relays: %v\n", newRelayURLs)
log(" Secret: %s\n", color.YellowString(secret))
// Normalize relay URLs
for i, relayURL := range newRelayURLs {
newRelayURLs[i] = nostr.NormalizeURL(relayURL)
}
// Update relays in config (add new ones)
relaysAdded := false
for _, relayURL := range newRelayURLs {
if !slices.Contains(config.Relays, relayURL) {
config.Relays = append(config.Relays, relayURL)
log("Added new relay: %s\n", color.MagentaString(relayURL))
relaysAdded = true
}
}
// Add client key to authorized keys
keyAdded := false
if !slices.Contains(config.AuthorizedKeys, clientPubkey) {
config.AuthorizedKeys = append(config.AuthorizedKeys, clientPubkey)
log("Added client key to authorized keys: %s\n", color.GreenString(clientPubkey.Hex()))
keyAdded = true
}
// Persist config if needed and changes were made
if persist != nil && (relaysAdded || keyAdded) {
persist()
log(color.GreenString("Configuration saved\n"))
}
// Connect to new relays if any were added
if relaysAdded {
newRelays := connectToAllRelays(ctx, c, newRelayURLs, nil, nostr.PoolOptions{})
log("Connected to %d new relay(s)\n", len(newRelays))
// Update the relay URLs list with successfully connected relays
for _, relay := range newRelays {
if !slices.Contains(relayURLs, relay.URL) {
relayURLs = append(relayURLs, relay.URL)
}
}
log("Updated subscription to listen on %d relay(s)\n", len(relayURLs))
// Cancel and recreate subscription with updated relay list
updateSubscription()
log("Subscription updated to include new relays\n")
}
// Prepare the response payload
responsePayload := nip46.Response{
ID: secret,
Result: "ack",
}
// Marshal the response payload to JSON
responseJSON, err := json.MarshalIndent(responsePayload, "", " ")
if err != nil {
log("Error: Failed to marshal response payload: %v\n", err)
return
}
log("Sending connect response with:\n%s\n", string(responseJSON))
// Encrypt the response using NIP-44
conversationKey, err := nip44.GenerateConversationKey(clientPubkey, sec)
if err != nil {
log("Error: Failed to generate conversation key: %v\n", err)
return
}
encryptedContent, err := nip44.Encrypt(string(responseJSON), conversationKey)
if err != nil {
log("Error: Failed to encrypt response content: %v\n", err)
return
}
// Create the kind 24133 event
eventResponse := nostr.Event{
Kind: nostr.KindNostrConnect,
PubKey: pubkey,
Content: encryptedContent,
Tags: nostr.Tags{{"p", clientPubkey.Hex()}},
CreatedAt: nostr.Now(),
}
// Sign the event with the signer
if err := eventResponse.Sign(sec); err != nil {
log("Error: Failed to sign connect response: %v\n", err)
return
}
targetRelays := newRelayURLs
if len(targetRelays) == 0 {
targetRelays = relayURLs
}
log("Sending connect response...\n")
successCount := atomic.Uint32{}
handlerWg.Add(len(targetRelays))
for _, relayURL := range targetRelays {
go func(relayURL string) {
if relay, _ := sys.Pool.EnsureRelay(relayURL); relay != nil {
err := relay.Publish(ctx, eventResponse)
printLock.Lock()
if err != nil {
log("Failed to publish to %s: %v\n", relayURL, err)
} else {
log("Published connect response to %s\n", color.GreenString(relayURL))
successCount.Add(1)
}
printLock.Unlock()
handlerWg.Done()
}
}(relayURL)
}
handlerWg.Wait()
if successCount.Load() == 0 {
log("Error: Failed to publish connect response to any relay\n")
} else {
log(color.GreenString("\nConnect response sent successfully to %d relay(s)!\n"), successCount.Load())
}
// print bunker info again after this
go func() {
time.Sleep(3 * time.Second)
printBunkerInfo()
}()
}
// handleBunkerCommand processes user commands in the bunker interface
handleBunkerCommand := func(command string) {
parts := strings.Fields(command)
if len(parts) == 0 {
return
}
switch strings.ToLower(parts[0]) {
case "help", "h", "?":
printHelp()
case "info", "i":
printBunkerInfo()
case "qr":
printQR()
case "connect", "c":
if len(parts) < 2 {
log("Usage: connect <nostrconnect://uri>\n")
return
}
handleConnect(parts[1])
case "exit", "quit", "q":
log("Exit command received.\n")
exitChan <- true
case "":
// Ignore empty commands
default:
log("Unknown command: %s. Type 'help' for available commands.\n", command)
}
}
// == END OF SUBCOMMANDS ==
// Print initial bunker information
printBunkerInfo()
// subscribe to relays
events := sys.Pool.SubscribeMany(ctx, relayURLs, nostr.Filter{
Kinds: []nostr.Kind{nostr.KindNostrConnect},
Tags: nostr.TagMap{"p": []string{pubkey.Hex()}},
Since: nostr.Now(),
LimitZero: true,
}, nostr.SubscriptionOptions{
Label: "nak-bunker",
})
signer := nip46.NewStaticKeySigner(sec)
handlerWg := sync.WaitGroup{}
printLock := sync.Mutex{}
// just a gimmick
var cancelPreviousBunkerInfoPrint context.CancelFunc
_, cancel := context.WithCancel(ctx)
@ -348,77 +609,81 @@ var bunker = &cli.Command{
return slices.Contains(config.AuthorizedKeys, from) || slices.Contains(authorizedSecrets, secret)
}
for ie := range events {
cancelPreviousBunkerInfoPrint() // this prevents us from printing a million bunker info blocks
// Start command input handler in a separate goroutine
go func() {
scanner := bufio.NewScanner(os.Stdin)
for scanner.Scan() {
command := strings.TrimSpace(scanner.Text())
handleBunkerCommand(command)
}
if err := scanner.Err(); err != nil {
log("error reading command: %v\n", err)
}
}()
// handle the NIP-46 request event
req, resp, eventResponse, err := signer.HandleRequest(ctx, ie.Event)
if err != nil {
log("< failed to handle request from %s: %s\n", ie.Event.PubKey, err.Error())
// Print initial command help
log("%s\nType 'help' for available commands or 'exit' to quit.\n%s\n",
color.CyanString("--------------- Bunker Command Interface ---------------"),
color.CyanString("--------------------------------------------------------"))
for {
// Check if exit was requested first
select {
case <-exitChan:
log("Shutting down bunker...\n")
return nil
case ie := <-events:
cancelPreviousBunkerInfoPrint() // this prevents us from printing a million bunker info blocks
// handle the NIP-46 request event
req, resp, eventResponse, err := signer.HandleRequest(ctx, ie.Event)
if err != nil {
log("< failed to handle request from %s: %s\n", ie.Event.PubKey, err.Error())
continue
}
jreq, _ := json.MarshalIndent(req, "", " ")
log("- got request from '%s': %s\n", color.New(color.Bold, color.FgBlue).Sprint(ie.Event.PubKey.Hex()), string(jreq))
jresp, _ := json.MarshalIndent(resp, "", " ")
log("~ responding with %s\n", string(jresp))
handlerWg.Add(len(relayURLs))
for _, relayURL := range relayURLs {
go func(relayURL string) {
if relay, _ := sys.Pool.EnsureRelay(relayURL); relay != nil {
err := relay.Publish(ctx, eventResponse)
printLock.Lock()
if err == nil {
log("* sent response through %s\n", relay.URL)
} else {
log("* failed to send response: %s\n", err)
}
printLock.Unlock()
handlerWg.Done()
}
}(relayURL)
}
handlerWg.Wait()
// just after handling one request we trigger this
go func() {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
cancelPreviousBunkerInfoPrint = cancel
// the idea is that we will print the bunker URL again so it is easier to copy-paste by users
// but we will only do if the bunker is inactive for more than 5 minutes
select {
case <-ctx.Done():
case <-time.After(time.Minute * 5):
log("\n")
printBunkerInfo()
}
}()
case <-time.After(100 * time.Millisecond):
// Continue to check for exit signal even when no events
continue
}
jreq, _ := json.MarshalIndent(req, "", " ")
log("- got request from '%s': %s\n", color.New(color.Bold, color.FgBlue).Sprint(ie.Event.PubKey.Hex()), string(jreq))
jresp, _ := json.MarshalIndent(resp, "", " ")
log("~ responding with %s\n", string(jresp))
handlerWg.Add(len(relayURLs))
for _, relayURL := range relayURLs {
go func(relayURL string) {
if relay, _ := sys.Pool.EnsureRelay(relayURL); relay != nil {
err := relay.Publish(ctx, eventResponse)
printLock.Lock()
if err == nil {
log("* sent response through %s\n", relay.URL)
} else {
log("* failed to send response: %s\n", err)
}
printLock.Unlock()
handlerWg.Done()
}
}(relayURL)
}
handlerWg.Wait()
// just after handling one request we trigger this
go func() {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
cancelPreviousBunkerInfoPrint = cancel
// the idea is that we will print the bunker URL again so it is easier to copy-paste by users
// but we will only do if the bunker is inactive for more than 5 minutes
select {
case <-ctx.Done():
case <-time.After(time.Minute * 5):
log("\n")
printBunkerInfo()
}
}()
}
return nil
},
Commands: []*cli.Command{
{
Name: "connect",
Usage: "use the client-initiated NostrConnect flow of NIP46",
ArgsUsage: "<nostrconnect-uri>",
Action: func(ctx context.Context, c *cli.Command) error {
if c.Args().Len() != 1 {
return fmt.Errorf("must be called with a nostrconnect://... uri")
}
uri, err := url.Parse(c.Args().First())
if err != nil || uri.Scheme != "nostrconnect" {
return fmt.Errorf("invalid uri")
}
// TODO
return fmt.Errorf("this is not implemented yet")
},
},
},
}

View File

@ -141,7 +141,9 @@ var count = &cli.Command{
}
for _, relayUrl := range relayUrls {
relay, _ := sys.Pool.EnsureRelay(relayUrl)
count, hllRegisters, err := relay.Count(ctx, filter, nostr.SubscriptionOptions{})
count, hllRegisters, err := relay.Count(ctx, filter, nostr.SubscriptionOptions{
Label: "nak-count",
})
fmt.Fprintf(os.Stderr, "%s%s: ", strings.Repeat(" ", biggerUrlSize-len(relayUrl)), relayUrl)
if err != nil {

View File

@ -106,7 +106,9 @@ var fetch = &cli.Command{
continue
}
for ie := range sys.Pool.FetchMany(ctx, relays, filter, nostr.SubscriptionOptions{}) {
for ie := range sys.Pool.FetchMany(ctx, relays, filter, nostr.SubscriptionOptions{
Label: "nak-fetch",
}) {
stdout(ie.Event)
}
}

10
go.mod
View File

@ -4,7 +4,7 @@ go 1.24.1
require (
fiatjaf.com/lib v0.3.1
fiatjaf.com/nostr v0.0.0-20250715161459-840e2846ed15
fiatjaf.com/nostr v0.0.0-20250818235102-c8d5aa703fab
github.com/bep/debounce v1.2.1
github.com/btcsuite/btcd/btcec/v2 v2.3.5
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e
@ -16,11 +16,12 @@ require (
github.com/mailru/easyjson v0.9.0
github.com/mark3labs/mcp-go v0.8.3
github.com/markusmobius/go-dateparser v1.2.3
github.com/mattn/go-isatty v0.0.20
github.com/mattn/go-tty v0.0.7
github.com/mdp/qrterminal/v3 v3.2.1
github.com/stretchr/testify v1.10.0
github.com/urfave/cli/v3 v3.0.0-beta1
golang.org/x/exp v0.0.0-20250717185816-542afb5b7346
golang.org/x/exp v0.0.0-20250813145105-42675adae3e6
golang.org/x/term v0.32.0
)
@ -42,7 +43,7 @@ require (
github.com/dgraph-io/ristretto/v2 v2.1.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/elliotchance/pie/v2 v2.7.0 // indirect
github.com/elnosh/gonuts v0.3.1-0.20250123162555-7c0381a585e3 // indirect
github.com/elnosh/gonuts v0.4.2 // indirect
github.com/fasthttp/websocket v1.5.12 // indirect
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
@ -56,7 +57,6 @@ require (
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/magefile/mage v1.14.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pkg/errors v0.9.1 // indirect
@ -76,7 +76,7 @@ require (
golang.org/x/crypto v0.36.0 // indirect
golang.org/x/net v0.37.0 // indirect
golang.org/x/sync v0.16.0 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/sys v0.34.0 // indirect
golang.org/x/text v0.23.0 // indirect
google.golang.org/protobuf v1.36.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect

16
go.sum
View File

@ -1,8 +1,8 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
fiatjaf.com/lib v0.3.1 h1:/oFQwNtFRfV+ukmOCxfBEAuayoLwXp4wu2/fz5iHpwA=
fiatjaf.com/lib v0.3.1/go.mod h1:Ycqq3+mJ9jAWu7XjbQI1cVr+OFgnHn79dQR5oTII47g=
fiatjaf.com/nostr v0.0.0-20250715161459-840e2846ed15 h1:XQq9DyW9j14wRKCU0cNyBUDCjJO6HAm+rK9abLLJKes=
fiatjaf.com/nostr v0.0.0-20250715161459-840e2846ed15/go.mod h1:lJ9x/Ehcq/7x2mf6iMlC4AOjPUh3WbfLMY+3PyaPRNs=
fiatjaf.com/nostr v0.0.0-20250818235102-c8d5aa703fab h1:zMp+G9Et5Z7ku/WUflZpmQzDIAB/Ah00Ms3cMtX9Pw4=
fiatjaf.com/nostr v0.0.0-20250818235102-c8d5aa703fab/go.mod h1:j7AfnEAevFuLcpH4Y1RYM27sYJfshL3An6ZSAQNlUlY=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/FastFilter/xorfilter v0.2.1 h1:lbdeLG9BdpquK64ZsleBS8B4xO/QW1IM0gMzF7KaBKc=
github.com/FastFilter/xorfilter v0.2.1/go.mod h1:aumvdkhscz6YBZF9ZA/6O4fIoNod4YR50kIVGGZ7l9I=
@ -80,8 +80,8 @@ github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+m
github.com/dvyukov/go-fuzz v0.0.0-20200318091601-be3528f3a813/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw=
github.com/elliotchance/pie/v2 v2.7.0 h1:FqoIKg4uj0G/CrLGuMS9ejnFKa92lxE1dEgBD3pShXg=
github.com/elliotchance/pie/v2 v2.7.0/go.mod h1:18t0dgGFH006g4eVdDtWfgFZPQEgl10IoEO8YWEq3Og=
github.com/elnosh/gonuts v0.3.1-0.20250123162555-7c0381a585e3 h1:k7evIqJ2BtFn191DgY/b03N2bMYA/iQwzr4f/uHYn20=
github.com/elnosh/gonuts v0.3.1-0.20250123162555-7c0381a585e3/go.mod h1:vgZomh4YQk7R3w4ltZc0sHwCmndfHkuX6V4sga/8oNs=
github.com/elnosh/gonuts v0.4.2 h1:/WubPAWGxTE+okJ0WPvmtEzTzpi04RGxiTHAF1FYU+M=
github.com/elnosh/gonuts v0.4.2/go.mod h1:vgZomh4YQk7R3w4ltZc0sHwCmndfHkuX6V4sga/8oNs=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
@ -212,6 +212,8 @@ github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JT
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tyler-smith/go-bip39 v1.1.0 h1:5eUemwrMargf3BSLRRCalXT93Ns6pQJIjYQN2nyfOP8=
github.com/tyler-smith/go-bip39 v1.1.0/go.mod h1:gUYDtqQw1JS3ZJ8UWVcGTGqqr6YIN3CWg+kkNaLt55U=
github.com/urfave/cli/v3 v3.0.0-beta1 h1:6DTaaUarcM0wX7qj5Hcvs+5Dm3dyUTBbEwIWAjcw9Zg=
github.com/urfave/cli/v3 v3.0.0-beta1/go.mod h1:FnIeEMYu+ko8zP1F9Ypr3xkZMIDqW3DR92yUtY39q1Y=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
@ -236,6 +238,8 @@ golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZv
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20250717185816-542afb5b7346 h1:vuCObX8mQzik1tfEcYxWZBuVsmQtD1IjxCyPKM18Bh4=
golang.org/x/exp v0.0.0-20250717185816-542afb5b7346/go.mod h1:A+z0yzpGtvnG90cToK5n2tu8UJVP2XUATh+r+sfOOOc=
golang.org/x/exp v0.0.0-20250813145105-42675adae3e6 h1:SbTAbRFnd5kjQXbczszQ0hdk3ctwYf3qBNH9jIsGclE=
golang.org/x/exp v0.0.0-20250813145105-42675adae3e6/go.mod h1:4QTo5u+SEIbbKW1RacMZq1YEfOBqeXa19JeshGi+zc4=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
@ -269,8 +273,8 @@ golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=

View File

@ -24,6 +24,7 @@ import (
"github.com/chzyer/readline"
"github.com/fatih/color"
jsoniter "github.com/json-iterator/go"
"github.com/mattn/go-isatty"
"github.com/mattn/go-tty"
"github.com/urfave/cli/v3"
"golang.org/x/term"
@ -315,6 +316,10 @@ func supportsDynamicMultilineMagic() bool {
return false
}
if !isatty.IsTerminal(os.Stdout.Fd()) {
return false
}
width, _, err := term.GetSize(int(os.Stderr.Fd()))
if err != nil {
return false

8
mcp.go
View File

@ -165,7 +165,9 @@ var mcpServer = &cli.Command{
res := strings.Builder{}
res.WriteString("Search results: ")
l := 0
for result := range sys.Pool.FetchMany(ctx, []string{"relay.nostr.band", "nostr.wine"}, filter, nostr.SubscriptionOptions{}) {
for result := range sys.Pool.FetchMany(ctx, []string{"relay.nostr.band", "nostr.wine"}, filter, nostr.SubscriptionOptions{
Label: "nak-mcp-search",
}) {
l++
pm, _ := sdk.ParseMetadata(result.Event)
res.WriteString(fmt.Sprintf("\n\nResult %d\nUser name: \"%s\"\nPublic key: \"%s\"\nDescription: \"%s\"\n",
@ -219,7 +221,9 @@ var mcpServer = &cli.Command{
}
}
events := sys.Pool.FetchMany(ctx, []string{relay}, filter, nostr.SubscriptionOptions{})
events := sys.Pool.FetchMany(ctx, []string{relay}, filter, nostr.SubscriptionOptions{
Label: "nak-mcp-profile-events",
})
result := strings.Builder{}
for ie := range events {

4
req.go
View File

@ -154,7 +154,9 @@ example:
fn = sys.Pool.SubscribeMany
}
for ie := range fn(ctx, relayUrls, filter, nostr.SubscriptionOptions{}) {
for ie := range fn(ctx, relayUrls, filter, nostr.SubscriptionOptions{
Label: "nak-req",
}) {
stdout(ie.Event)
}
}