mirror of
https://github.com/fiatjaf/nak.git
synced 2025-12-09 17:18:50 +00:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6daf0f4e5c | ||
|
|
bfeaf0710f | ||
|
|
e45b54ea62 |
@@ -15,6 +15,7 @@ import (
|
|||||||
"github.com/nbd-wtf/go-nostr/nip19"
|
"github.com/nbd-wtf/go-nostr/nip19"
|
||||||
"github.com/nbd-wtf/go-nostr/nip46"
|
"github.com/nbd-wtf/go-nostr/nip46"
|
||||||
"github.com/nbd-wtf/go-nostr/nip49"
|
"github.com/nbd-wtf/go-nostr/nip49"
|
||||||
|
"golang.org/x/term"
|
||||||
)
|
)
|
||||||
|
|
||||||
var defaultKeyFlags = []cli.Flag{
|
var defaultKeyFlags = []cli.Flag{
|
||||||
@@ -84,9 +85,6 @@ func gatherSecretKeyOrBunkerFromArguments(ctx context.Context, c *cli.Command) (
|
|||||||
}
|
}
|
||||||
|
|
||||||
if c.Bool("prompt-sec") {
|
if c.Bool("prompt-sec") {
|
||||||
if isPiped() {
|
|
||||||
return "", nil, fmt.Errorf("can't prompt for a secret key when processing data from a pipe, try again without --prompt-sec")
|
|
||||||
}
|
|
||||||
sec, err = askPassword("type your secret key as ncryptsec, nsec or hex: ", nil)
|
sec, err = askPassword("type your secret key as ncryptsec, nsec or hex: ", nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", nil, fmt.Errorf("failed to get secret key: %w", err)
|
return "", nil, fmt.Errorf("failed to get secret key: %w", err)
|
||||||
@@ -133,29 +131,59 @@ func promptDecrypt(ncryptsec string) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func askPassword(msg string, shouldAskAgain func(answer string) bool) (string, error) {
|
func askPassword(msg string, shouldAskAgain func(answer string) bool) (string, error) {
|
||||||
config := &readline.Config{
|
if isPiped() {
|
||||||
Stdout: color.Error,
|
// Use TTY method when stdin is piped
|
||||||
Prompt: color.YellowString(msg),
|
tty, err := os.Open("/dev/tty")
|
||||||
InterruptPrompt: "^C",
|
if err != nil {
|
||||||
DisableAutoSaveHistory: true,
|
return "", fmt.Errorf("can't prompt for a secret key when processing data from a pipe on this system (failed to open /dev/tty: %w), try again without --prompt-sec or provide the key via --sec or NOSTR_SECRET_KEY environment variable", err)
|
||||||
EnableMask: true,
|
}
|
||||||
MaskRune: '*',
|
defer tty.Close()
|
||||||
}
|
|
||||||
|
|
||||||
rl, err := readline.NewEx(config)
|
for {
|
||||||
if err != nil {
|
// Print the prompt to stderr so it's visible to the user
|
||||||
return "", err
|
fmt.Fprintf(color.Error, color.YellowString(msg))
|
||||||
}
|
|
||||||
|
|
||||||
for {
|
// Read password from TTY with masking
|
||||||
answer, err := rl.Readline()
|
password, err := term.ReadPassword(int(tty.Fd()))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print newline after password input
|
||||||
|
fmt.Fprintln(color.Error)
|
||||||
|
|
||||||
|
answer := strings.TrimSpace(string(password))
|
||||||
|
if shouldAskAgain != nil && shouldAskAgain(answer) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return answer, nil
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Use normal readline method when stdin is not piped
|
||||||
|
config := &readline.Config{
|
||||||
|
Stdout: color.Error,
|
||||||
|
Prompt: color.YellowString(msg),
|
||||||
|
InterruptPrompt: "^C",
|
||||||
|
DisableAutoSaveHistory: true,
|
||||||
|
EnableMask: true,
|
||||||
|
MaskRune: '*',
|
||||||
|
}
|
||||||
|
|
||||||
|
rl, err := readline.NewEx(config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
answer = strings.TrimSpace(answer)
|
|
||||||
if shouldAskAgain != nil && shouldAskAgain(answer) {
|
for {
|
||||||
continue
|
answer, err := rl.Readline()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
answer = strings.TrimSpace(answer)
|
||||||
|
if shouldAskAgain != nil && shouldAskAgain(answer) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return answer, err
|
||||||
}
|
}
|
||||||
return answer, err
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
115
mcp.go
115
mcp.go
@@ -28,25 +28,13 @@ var mcpServer = &cli.Command{
|
|||||||
|
|
||||||
s.AddTool(mcp.NewTool("publish_note",
|
s.AddTool(mcp.NewTool("publish_note",
|
||||||
mcp.WithDescription("Publish a short note event to Nostr with the given text content"),
|
mcp.WithDescription("Publish a short note event to Nostr with the given text content"),
|
||||||
mcp.WithString("relay",
|
mcp.WithString("content", mcp.Description("Arbitrary string to be published"), mcp.Required()),
|
||||||
mcp.Description("Relay to publish the note to"),
|
mcp.WithString("relay", mcp.Description("Relay to publish the note to")),
|
||||||
),
|
mcp.WithString("mention", mcp.Description("Nostr user's public key to be mentioned")),
|
||||||
mcp.WithString("content",
|
), func(ctx context.Context, r mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
mcp.Required(),
|
content := required[string](r, "content")
|
||||||
mcp.Description("Arbitrary string to be published"),
|
mention, _ := optional[string](r, "mention")
|
||||||
),
|
relay, _ := optional[string](r, "relay")
|
||||||
mcp.WithString("mention",
|
|
||||||
mcp.Required(),
|
|
||||||
mcp.Description("Nostr user's public key to be mentioned"),
|
|
||||||
),
|
|
||||||
), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
||||||
content, _ := request.Params.Arguments["content"].(string)
|
|
||||||
mention, _ := request.Params.Arguments["mention"].(string)
|
|
||||||
relayI, ok := request.Params.Arguments["relay"]
|
|
||||||
var relay string
|
|
||||||
if ok {
|
|
||||||
relay, _ = relayI.(string)
|
|
||||||
}
|
|
||||||
|
|
||||||
if mention != "" && !nostr.IsValidPublicKey(mention) {
|
if mention != "" && !nostr.IsValidPublicKey(mention) {
|
||||||
return mcp.NewToolResultError("the given mention isn't a valid public key, it must be 32 bytes hex, like the ones returned by search_profile"), nil
|
return mcp.NewToolResultError("the given mention isn't a valid public key, it must be 32 bytes hex, like the ones returned by search_profile"), nil
|
||||||
@@ -81,7 +69,9 @@ var mcpServer = &cli.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
// extra relay specified
|
// extra relay specified
|
||||||
relays = append(relays, relay)
|
if relay != "" {
|
||||||
|
relays = append(relays, relay)
|
||||||
|
}
|
||||||
|
|
||||||
result := strings.Builder{}
|
result := strings.Builder{}
|
||||||
result.WriteString(
|
result.WriteString(
|
||||||
@@ -111,12 +101,9 @@ var mcpServer = &cli.Command{
|
|||||||
|
|
||||||
s.AddTool(mcp.NewTool("resolve_nostr_uri",
|
s.AddTool(mcp.NewTool("resolve_nostr_uri",
|
||||||
mcp.WithDescription("Resolve URIs prefixed with nostr:, including nostr:nevent1..., nostr:npub1..., nostr:nprofile1... and nostr:naddr1..."),
|
mcp.WithDescription("Resolve URIs prefixed with nostr:, including nostr:nevent1..., nostr:npub1..., nostr:nprofile1... and nostr:naddr1..."),
|
||||||
mcp.WithString("uri",
|
mcp.WithString("uri", mcp.Description("URI to be resolved"), mcp.Required()),
|
||||||
mcp.Required(),
|
), func(ctx context.Context, r mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
mcp.Description("URI to be resolved"),
|
uri := required[string](r, "uri")
|
||||||
),
|
|
||||||
), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
||||||
uri, _ := request.Params.Arguments["uri"].(string)
|
|
||||||
if strings.HasPrefix(uri, "nostr:") {
|
if strings.HasPrefix(uri, "nostr:") {
|
||||||
uri = uri[6:]
|
uri = uri[6:]
|
||||||
}
|
}
|
||||||
@@ -159,12 +146,9 @@ var mcpServer = &cli.Command{
|
|||||||
|
|
||||||
s.AddTool(mcp.NewTool("search_profile",
|
s.AddTool(mcp.NewTool("search_profile",
|
||||||
mcp.WithDescription("Search for the public key of a Nostr user given their name"),
|
mcp.WithDescription("Search for the public key of a Nostr user given their name"),
|
||||||
mcp.WithString("name",
|
mcp.WithString("name", mcp.Description("Name to be searched"), mcp.Required()),
|
||||||
mcp.Required(),
|
), func(ctx context.Context, r mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
mcp.Description("Name to be searched"),
|
name := required[string](r, "name")
|
||||||
),
|
|
||||||
), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
||||||
name, _ := request.Params.Arguments["name"].(string)
|
|
||||||
re := sys.Pool.QuerySingle(ctx, []string{"relay.nostr.band", "nostr.wine"}, nostr.Filter{Search: name, Kinds: []int{0}})
|
re := sys.Pool.QuerySingle(ctx, []string{"relay.nostr.band", "nostr.wine"}, nostr.Filter{Search: name, Kinds: []int{0}})
|
||||||
if re == nil {
|
if re == nil {
|
||||||
return mcp.NewToolResultError("couldn't find anyone with that name"), nil
|
return mcp.NewToolResultError("couldn't find anyone with that name"), nil
|
||||||
@@ -175,42 +159,24 @@ var mcpServer = &cli.Command{
|
|||||||
|
|
||||||
s.AddTool(mcp.NewTool("get_outbox_relay_for_pubkey",
|
s.AddTool(mcp.NewTool("get_outbox_relay_for_pubkey",
|
||||||
mcp.WithDescription("Get the best relay from where to read notes from a specific Nostr user"),
|
mcp.WithDescription("Get the best relay from where to read notes from a specific Nostr user"),
|
||||||
mcp.WithString("pubkey",
|
mcp.WithString("pubkey", mcp.Description("Public key of Nostr user we want to know the relay from where to read"), mcp.Required()),
|
||||||
mcp.Required(),
|
), func(ctx context.Context, r mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
mcp.Description("Public key of Nostr user we want to know the relay from where to read"),
|
pubkey := required[string](r, "pubkey")
|
||||||
),
|
|
||||||
), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
||||||
pubkey, _ := request.Params.Arguments["pubkey"].(string)
|
|
||||||
res := sys.FetchOutboxRelays(ctx, pubkey, 1)
|
res := sys.FetchOutboxRelays(ctx, pubkey, 1)
|
||||||
return mcp.NewToolResultText(res[0]), nil
|
return mcp.NewToolResultText(res[0]), nil
|
||||||
})
|
})
|
||||||
|
|
||||||
s.AddTool(mcp.NewTool("read_events_from_relay",
|
s.AddTool(mcp.NewTool("read_events_from_relay",
|
||||||
mcp.WithDescription("Makes a REQ query to one relay using the specified parameters, this can be used to fetch notes from a profile"),
|
mcp.WithDescription("Makes a REQ query to one relay using the specified parameters, this can be used to fetch notes from a profile"),
|
||||||
mcp.WithNumber("kind",
|
mcp.WithString("relay", mcp.Description("relay URL to send the query to"), mcp.Required()),
|
||||||
mcp.Required(),
|
mcp.WithNumber("kind", mcp.Description("event kind number to include in the 'kinds' field"), mcp.Required()),
|
||||||
mcp.Description("event kind number to include in the 'kinds' field"),
|
mcp.WithNumber("limit", mcp.Description("maximum number of events to query"), mcp.Required()),
|
||||||
),
|
mcp.WithString("pubkey", mcp.Description("pubkey to include in the 'authors' field")),
|
||||||
mcp.WithString("pubkey",
|
), func(ctx context.Context, r mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
mcp.Description("pubkey to include in the 'authors' field"),
|
relay := required[string](r, "relay")
|
||||||
),
|
kind := int(required[float64](r, "kind"))
|
||||||
mcp.WithNumber("limit",
|
limit := int(required[float64](r, "limit"))
|
||||||
mcp.Required(),
|
pubkey, _ := optional[string](r, "pubkey")
|
||||||
mcp.Description("maximum number of events to query"),
|
|
||||||
),
|
|
||||||
mcp.WithString("relay",
|
|
||||||
mcp.Required(),
|
|
||||||
mcp.Description("relay URL to send the query to"),
|
|
||||||
),
|
|
||||||
), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
||||||
relay, _ := request.Params.Arguments["relay"].(string)
|
|
||||||
limit, _ := request.Params.Arguments["limit"].(int)
|
|
||||||
kind, _ := request.Params.Arguments["kind"].(int)
|
|
||||||
pubkeyI, ok := request.Params.Arguments["pubkey"]
|
|
||||||
var pubkey string
|
|
||||||
if ok {
|
|
||||||
pubkey, _ = pubkeyI.(string)
|
|
||||||
}
|
|
||||||
|
|
||||||
if pubkey != "" && !nostr.IsValidPublicKey(pubkey) {
|
if pubkey != "" && !nostr.IsValidPublicKey(pubkey) {
|
||||||
return mcp.NewToolResultError("the given pubkey isn't a valid public key, it must be 32 bytes hex, like the ones returned by search_profile"), nil
|
return mcp.NewToolResultError("the given pubkey isn't a valid public key, it must be 32 bytes hex, like the ones returned by search_profile"), nil
|
||||||
@@ -242,3 +208,28 @@ var mcpServer = &cli.Command{
|
|||||||
return server.ServeStdio(s)
|
return server.ServeStdio(s)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func required[T comparable](r mcp.CallToolRequest, p string) T {
|
||||||
|
var zero T
|
||||||
|
if _, ok := r.Params.Arguments[p]; !ok {
|
||||||
|
return zero
|
||||||
|
}
|
||||||
|
if _, ok := r.Params.Arguments[p].(T); !ok {
|
||||||
|
return zero
|
||||||
|
}
|
||||||
|
if r.Params.Arguments[p].(T) == zero {
|
||||||
|
return zero
|
||||||
|
}
|
||||||
|
return r.Params.Arguments[p].(T)
|
||||||
|
}
|
||||||
|
|
||||||
|
func optional[T any](r mcp.CallToolRequest, p string) (T, bool) {
|
||||||
|
var zero T
|
||||||
|
if _, ok := r.Params.Arguments[p]; !ok {
|
||||||
|
return zero, false
|
||||||
|
}
|
||||||
|
if _, ok := r.Params.Arguments[p].(T); !ok {
|
||||||
|
return zero, false
|
||||||
|
}
|
||||||
|
return r.Params.Arguments[p].(T), true
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,14 +1,7 @@
|
|||||||
nak:
|
repository: https://github.com/fiatjaf/nak
|
||||||
cli:
|
assets:
|
||||||
name: nak
|
- nak-v\d+\.\d+\.\d+-darwin-arm64
|
||||||
summary: a command line tool for doing all things nostr
|
- nak-v\d+\.\d+\.\d+-linux-amd64
|
||||||
repository: https://github.com/fiatjaf/nak
|
- nak-v\d+\.\d+\.\d+-linux-arm64
|
||||||
artifacts:
|
remote_metadata:
|
||||||
nak-v%v-darwin-arm64:
|
- github
|
||||||
platforms: [darwin-arm64]
|
|
||||||
nak-v%v-darwin-amd64:
|
|
||||||
platforms: [darwin-x86_64]
|
|
||||||
nak-v%v-linux-arm64:
|
|
||||||
platforms: [linux-aarch64]
|
|
||||||
nak-v%v-linux-amd64:
|
|
||||||
platforms: [linux-x86_64]
|
|
||||||
|
|||||||
Reference in New Issue
Block a user