initial commit

This commit is contained in:
pablof7z
2025-03-29 09:34:43 +00:00
commit 6d0e5a8e79
38 changed files with 2934 additions and 0 deletions

34
.gitignore vendored Normal file
View File

@@ -0,0 +1,34 @@
# dependencies (bun install)
node_modules
# output
out
dist
*.tgz
# code coverage
coverage
*.lcov
# logs
logs
_.log
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# caches
.eslintcache
.cache
*.tsbuildinfo
# IntelliJ based IDEs
.idea
# Finder (MacOS) folder config
.DS_Store

81
README.md Normal file
View File

@@ -0,0 +1,81 @@
# MCP-NOSTR: Nostr Publisher for Model Context Protocol
A bridge between the Model Context Protocol (MCP) and the Nostr network, enabling AI language models to publish content to Nostr.
## Features
- Implements the Model Context Protocol for interacting with AI language models
- Provides CLI commands for managing Nostr identities, profiles, and content
- Publishes AI-generated content to the Nostr network
- Supports Web of Trust (WoT) for verified connections
- Manages user profiles and follows
## Installation
```bash
bunx mcp-code mcp
```
### From source
```bash
# Clone the repository
git clone https://github.com/pablof7z/mcp-nostr.git
cd mcp-nostr
# Install dependencies
bun install
# Build the executable
bun run build
```
## Usage
### As an MCP Server
Run without arguments to start the MCP server mode, which listens for MCP protocol messages on stdin and responds on stdout:
```bash
./mcp-code
```
### CLI Commands
The tool also provides various command-line utilities for managing Nostr profiles and content:
```bash
# See available commands
./mcp-code --help
```
## Configuration
Configuration is stored in `~/.mcp-nostr.json`:
## Development
```bash
# Run linting
bun run lint
# Format code
bun run format
# Check and fix issues
bun run check
```
## Dependencies
- [@modelcontextprotocol/sdk](https://github.com/model-context-protocol/sdk) - SDK for the Model Context Protocol
- [@nostr-dev-kit/ndk](https://github.com/nostr-dev-kit/ndk) - Nostr Development Kit
- [@nostr-dev-kit/ndk-wallet](https://github.com/nostr-dev-kit/ndk-wallet) - Wallet integration for NOSTR
- [commander](https://github.com/tj/commander.js) - Command-line interface framework
- [yaml](https://github.com/eemeli/yaml) - YAML parsing and serialization
## License
MIT

33
biome.json Normal file
View File

@@ -0,0 +1,33 @@
{
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
"organizeImports": {
"enabled": true
},
"linter": {
"enabled": true,
"rules": {
"recommended": true,
"style": {
"noNonNullAssertion": "off"
},
"correctness": {
"noUnusedVariables": "error"
},
"suspicious": {
"noExplicitAny": "warn"
}
}
},
"formatter": {
"enabled": true,
"indentStyle": "space",
"indentWidth": 4
},
"javascript": {
"formatter": {
"quoteStyle": "double",
"trailingCommas": "es5",
"semicolons": "always"
}
}
}

423
bun.lock Normal file
View File

@@ -0,0 +1,423 @@
{
"lockfileVersion": 1,
"workspaces": {
"": {
"name": "mcp-ndk",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.7.0",
"@nostr-dev-kit/ndk": "^2.13.0-rc2",
"@nostr-dev-kit/ndk-wallet": "0.5.0",
"commander": "^13.1.0",
"inquirer": "^12.5.0",
"yaml": "^2.7.0",
},
"devDependencies": {
"@biomejs/biome": "^1.9.4",
"@types/bun": "latest",
"@types/inquirer": "^9.0.7",
},
"peerDependencies": {
"typescript": "^5",
},
},
},
"packages": {
"@biomejs/biome": ["@biomejs/biome@1.9.4", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "1.9.4", "@biomejs/cli-darwin-x64": "1.9.4", "@biomejs/cli-linux-arm64": "1.9.4", "@biomejs/cli-linux-arm64-musl": "1.9.4", "@biomejs/cli-linux-x64": "1.9.4", "@biomejs/cli-linux-x64-musl": "1.9.4", "@biomejs/cli-win32-arm64": "1.9.4", "@biomejs/cli-win32-x64": "1.9.4" }, "bin": { "biome": "bin/biome" } }, "sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog=="],
"@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@1.9.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw=="],
"@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@1.9.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg=="],
"@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@1.9.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g=="],
"@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@1.9.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA=="],
"@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@1.9.4", "", { "os": "linux", "cpu": "x64" }, "sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg=="],
"@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@1.9.4", "", { "os": "linux", "cpu": "x64" }, "sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg=="],
"@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@1.9.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg=="],
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@1.9.4", "", { "os": "win32", "cpu": "x64" }, "sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA=="],
"@cashu/cashu-ts": ["@cashu/cashu-ts@2.4.1", "", { "dependencies": { "@cashu/crypto": "^0.3.4", "@noble/curves": "^1.3.0", "@noble/hashes": "^1.3.3", "buffer": "^6.0.3" } }, "sha512-9lDHP5GtWvC/mIDPRg5KdRQAsqoYYm93efPyfgDtRd9eW1BhWrzLuS0sN1WVkL9noAXCZoWjjpX8TElMXhpFhA=="],
"@cashu/crypto": ["@cashu/crypto@0.3.4", "", { "dependencies": { "@noble/curves": "^1.6.0", "@noble/hashes": "^1.5.0", "@scure/bip32": "^1.5.0", "@scure/bip39": "^1.4.0", "buffer": "^6.0.3" } }, "sha512-mfv1Pj4iL1PXzUj9NKIJbmncCLMqYfnEDqh/OPxAX0nNBt6BOnVJJLjLWFlQeYxlnEfWABSNkrqPje1t5zcyhA=="],
"@inquirer/checkbox": ["@inquirer/checkbox@4.1.4", "", { "dependencies": { "@inquirer/core": "^10.1.9", "@inquirer/figures": "^1.0.11", "@inquirer/type": "^3.0.5", "ansi-escapes": "^4.3.2", "yoctocolors-cjs": "^2.1.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-d30576EZdApjAMceijXA5jDzRQHT/MygbC+J8I7EqA6f/FRpYxlRtRJbHF8gHeWYeSdOuTEJqonn7QLB1ELezA=="],
"@inquirer/confirm": ["@inquirer/confirm@5.1.8", "", { "dependencies": { "@inquirer/core": "^10.1.9", "@inquirer/type": "^3.0.5" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-dNLWCYZvXDjO3rnQfk2iuJNL4Ivwz/T2+C3+WnNfJKsNGSuOs3wAo2F6e0p946gtSAk31nZMfW+MRmYaplPKsg=="],
"@inquirer/core": ["@inquirer/core@10.1.9", "", { "dependencies": { "@inquirer/figures": "^1.0.11", "@inquirer/type": "^3.0.5", "ansi-escapes": "^4.3.2", "cli-width": "^4.1.0", "mute-stream": "^2.0.0", "signal-exit": "^4.1.0", "wrap-ansi": "^6.2.0", "yoctocolors-cjs": "^2.1.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-sXhVB8n20NYkUBfDYgizGHlpRVaCRjtuzNZA6xpALIUbkgfd2Hjz+DfEN6+h1BRnuxw0/P4jCIMjMsEOAMwAJw=="],
"@inquirer/editor": ["@inquirer/editor@4.2.9", "", { "dependencies": { "@inquirer/core": "^10.1.9", "@inquirer/type": "^3.0.5", "external-editor": "^3.1.0" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-8HjOppAxO7O4wV1ETUlJFg6NDjp/W2NP5FB9ZPAcinAlNT4ZIWOLe2pUVwmmPRSV0NMdI5r/+lflN55AwZOKSw=="],
"@inquirer/expand": ["@inquirer/expand@4.0.11", "", { "dependencies": { "@inquirer/core": "^10.1.9", "@inquirer/type": "^3.0.5", "yoctocolors-cjs": "^2.1.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-OZSUW4hFMW2TYvX/Sv+NnOZgO8CHT2TU1roUCUIF2T+wfw60XFRRp9MRUPCT06cRnKL+aemt2YmTWwt7rOrNEA=="],
"@inquirer/figures": ["@inquirer/figures@1.0.11", "", {}, "sha512-eOg92lvrn/aRUqbxRyvpEWnrvRuTYRifixHkYVpJiygTgVSBIHDqLh0SrMQXkafvULg3ck11V7xvR+zcgvpHFw=="],
"@inquirer/input": ["@inquirer/input@4.1.8", "", { "dependencies": { "@inquirer/core": "^10.1.9", "@inquirer/type": "^3.0.5" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-WXJI16oOZ3/LiENCAxe8joniNp8MQxF6Wi5V+EBbVA0ZIOpFcL4I9e7f7cXse0HJeIPCWO8Lcgnk98juItCi7Q=="],
"@inquirer/number": ["@inquirer/number@3.0.11", "", { "dependencies": { "@inquirer/core": "^10.1.9", "@inquirer/type": "^3.0.5" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-pQK68CsKOgwvU2eA53AG/4npRTH2pvs/pZ2bFvzpBhrznh8Mcwt19c+nMO7LHRr3Vreu1KPhNBF3vQAKrjIulw=="],
"@inquirer/password": ["@inquirer/password@4.0.11", "", { "dependencies": { "@inquirer/core": "^10.1.9", "@inquirer/type": "^3.0.5", "ansi-escapes": "^4.3.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-dH6zLdv+HEv1nBs96Case6eppkRggMe8LoOTl30+Gq5Wf27AO/vHFgStTVz4aoevLdNXqwE23++IXGw4eiOXTg=="],
"@inquirer/prompts": ["@inquirer/prompts@7.4.0", "", { "dependencies": { "@inquirer/checkbox": "^4.1.4", "@inquirer/confirm": "^5.1.8", "@inquirer/editor": "^4.2.9", "@inquirer/expand": "^4.0.11", "@inquirer/input": "^4.1.8", "@inquirer/number": "^3.0.11", "@inquirer/password": "^4.0.11", "@inquirer/rawlist": "^4.0.11", "@inquirer/search": "^3.0.11", "@inquirer/select": "^4.1.0" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-EZiJidQOT4O5PYtqnu1JbF0clv36oW2CviR66c7ma4LsupmmQlUwmdReGKRp456OWPWMz3PdrPiYg3aCk3op2w=="],
"@inquirer/rawlist": ["@inquirer/rawlist@4.0.11", "", { "dependencies": { "@inquirer/core": "^10.1.9", "@inquirer/type": "^3.0.5", "yoctocolors-cjs": "^2.1.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-uAYtTx0IF/PqUAvsRrF3xvnxJV516wmR6YVONOmCWJbbt87HcDHLfL9wmBQFbNJRv5kCjdYKrZcavDkH3sVJPg=="],
"@inquirer/search": ["@inquirer/search@3.0.11", "", { "dependencies": { "@inquirer/core": "^10.1.9", "@inquirer/figures": "^1.0.11", "@inquirer/type": "^3.0.5", "yoctocolors-cjs": "^2.1.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-9CWQT0ikYcg6Ls3TOa7jljsD7PgjcsYEM0bYE+Gkz+uoW9u8eaJCRHJKkucpRE5+xKtaaDbrND+nPDoxzjYyew=="],
"@inquirer/select": ["@inquirer/select@4.1.0", "", { "dependencies": { "@inquirer/core": "^10.1.9", "@inquirer/figures": "^1.0.11", "@inquirer/type": "^3.0.5", "ansi-escapes": "^4.3.2", "yoctocolors-cjs": "^2.1.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-z0a2fmgTSRN+YBuiK1ROfJ2Nvrpij5lVN3gPDkQGhavdvIVGHGW29LwYZfM/j42Ai2hUghTI/uoBuTbrJk42bA=="],
"@inquirer/type": ["@inquirer/type@3.0.5", "", { "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-ZJpeIYYueOz/i/ONzrfof8g89kNdO2hjGuvULROo3O8rlB2CRtSseE5KeirnyE4t/thAn/EwvS/vuQeJCn+NZg=="],
"@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.7.0", "", { "dependencies": { "content-type": "^1.0.5", "cors": "^2.8.5", "eventsource": "^3.0.2", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "pkce-challenge": "^4.1.0", "raw-body": "^3.0.0", "zod": "^3.23.8", "zod-to-json-schema": "^3.24.1" } }, "sha512-IYPe/FLpvF3IZrd/f5p5ffmWhMc3aEMuM2wGJASDqC2Ge7qatVCdbfPx3n/5xFeb19xN0j/911M2AaFuircsWA=="],
"@noble/ciphers": ["@noble/ciphers@0.5.3", "", {}, "sha512-B0+6IIHiqEs3BPMT0hcRmHvEj2QHOLu+uwt+tqDDeVd0oyVzh7BPrDcPjRnV1PV/5LaknXJJQvOuRGR0zQJz+w=="],
"@noble/curves": ["@noble/curves@1.8.1", "", { "dependencies": { "@noble/hashes": "1.7.1" } }, "sha512-warwspo+UYUPep0Q+vtdVB4Ugn8GGQj8iyB3gnRWsztmUHTI3S1nhdiWNsPUGL0vud7JlRRk1XEu7Lq1KGTnMQ=="],
"@noble/hashes": ["@noble/hashes@1.7.1", "", {}, "sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ=="],
"@noble/secp256k1": ["@noble/secp256k1@2.2.3", "", {}, "sha512-l7r5oEQym9Us7EAigzg30/PQAvynhMt2uoYtT3t26eGDVm9Yii5mZ5jWSWmZ/oSIR2Et0xfc6DXrG0bZ787V3w=="],
"@nostr-dev-kit/ndk": ["@nostr-dev-kit/ndk@2.13.0-rc2", "", { "dependencies": { "@noble/curves": "^1.6.0", "@noble/hashes": "^1.5.0", "@noble/secp256k1": "^2.1.0", "@scure/base": "^1.1.9", "debug": "^4.3.6", "light-bolt11-decoder": "^3.2.0", "tseep": "^1.2.2", "typescript-lru-cache": "^2.0.0", "utf8-buffer": "^1.0.0", "websocket-polyfill": "^0.0.3" }, "peerDependencies": { "nostr-tools": "^2.7.1" } }, "sha512-oTj19rqcft6VCxvlkosIuCl8aQNrQzU0NJ/yi0XRbtlVReAyJhZ7iT6rZixO9aL4pAG9ZCBp+ej3GRcU3OIEeQ=="],
"@nostr-dev-kit/ndk-wallet": ["@nostr-dev-kit/ndk-wallet@0.5.0", "", { "dependencies": { "@nostr-dev-kit/ndk": "2.13.0-rc2", "debug": "^4.3.4", "light-bolt11-decoder": "^3.0.0", "tseep": "^1.1.1", "typescript": "^5.4.4", "webln": "^0.3.2" }, "peerDependencies": { "@cashu/cashu-ts": "*", "@cashu/crypto": "*" } }, "sha512-iHMh5kRKbEEw42fmE89KIH+4cnsRQzpW4SrbtpLCbdGFeu8iUOE2C1bBAWWW3HdtcjuRxa83iYXI+eo/Wake2w=="],
"@scure/base": ["@scure/base@1.2.4", "", {}, "sha512-5Yy9czTO47mqz+/J8GM6GIId4umdCk1wc1q8rKERQulIoc8VP9pzDcghv10Tl2E7R96ZUx/PhND3ESYUQX8NuQ=="],
"@scure/bip32": ["@scure/bip32@1.3.1", "", { "dependencies": { "@noble/curves": "~1.1.0", "@noble/hashes": "~1.3.1", "@scure/base": "~1.1.0" } }, "sha512-osvveYtyzdEVbt3OfwwXFr4P2iVBL5u1Q3q4ONBfDY/UpOuXmOlbgwc1xECEboY8wIays8Yt6onaWMUdUbfl0A=="],
"@scure/bip39": ["@scure/bip39@1.2.1", "", { "dependencies": { "@noble/hashes": "~1.3.0", "@scure/base": "~1.1.0" } }, "sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg=="],
"@types/bun": ["@types/bun@1.2.5", "", { "dependencies": { "bun-types": "1.2.5" } }, "sha512-w2OZTzrZTVtbnJew1pdFmgV99H0/L+Pvw+z1P67HaR18MHOzYnTYOi6qzErhK8HyT+DB782ADVPPE92Xu2/Opg=="],
"@types/chrome": ["@types/chrome@0.0.74", "", { "dependencies": { "@types/filesystem": "*" } }, "sha512-hzosS5CkQcIKCgxcsV2AzbJ36KNxG/Db2YEN/erEu7Boprg+KpMDLBQqKFmSo+JkQMGqRcicUyqCowJpuT+C6A=="],
"@types/filesystem": ["@types/filesystem@0.0.36", "", { "dependencies": { "@types/filewriter": "*" } }, "sha512-vPDXOZuannb9FZdxgHnqSwAG/jvdGM8Wq+6N4D/d80z+D4HWH+bItqsZaVRQykAn6WEVeEkLm2oQigyHtgb0RA=="],
"@types/filewriter": ["@types/filewriter@0.0.33", "", {}, "sha512-xFU8ZXTw4gd358lb2jw25nxY9QAgqn2+bKKjKOYfNCzN4DKCFetK7sPtrlpg66Ywe3vWY9FNxprZawAh9wfJ3g=="],
"@types/inquirer": ["@types/inquirer@9.0.7", "", { "dependencies": { "@types/through": "*", "rxjs": "^7.2.0" } }, "sha512-Q0zyBupO6NxGRZut/JdmqYKOnN95Eg5V8Csg3PGKkP+FnvsUZx1jAyK7fztIszxxMuoBA6E3KXWvdZVXIpx60g=="],
"@types/node": ["@types/node@22.13.11", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-iEUCUJoU0i3VnrCmgoWCXttklWcvoCIx4jzcP22fioIVSdTmjgoEvmAO/QPw6TcS9k5FrNgn4w7q5lGOd1CT5g=="],
"@types/through": ["@types/through@0.0.33", "", { "dependencies": { "@types/node": "*" } }, "sha512-HsJ+z3QuETzP3cswwtzt2vEIiHBk/dCcHGhbmG5X3ecnwFD/lPrMpliGXxSCg03L9AhrdwA4Oz/qfspkDW+xGQ=="],
"@types/ws": ["@types/ws@8.5.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-bd/YFLW+URhBzMXurx7lWByOu+xzU9+kb3RboOteXYDfW+tr+JZa99OyNmPINEGB/ahzKrEuc8rcv4gnpJmxTw=="],
"accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
"ansi-escapes": ["ansi-escapes@4.3.2", "", { "dependencies": { "type-fest": "^0.21.3" } }, "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ=="],
"ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
"body-parser": ["body-parser@2.1.0", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.0", "http-errors": "^2.0.0", "iconv-lite": "^0.5.2", "on-finished": "^2.4.1", "qs": "^6.14.0", "raw-body": "^3.0.0", "type-is": "^2.0.0" } }, "sha512-/hPxh61E+ll0Ujp24Ilm64cykicul1ypfwjVttduAiEdtnJFvLePSrIPk+HMImtNv5270wOGCb1Tns2rybMkoQ=="],
"buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="],
"bufferutil": ["bufferutil@4.0.9", "", { "dependencies": { "node-gyp-build": "^4.3.0" } }, "sha512-WDtdLmJvAuNNPzByAYpRo2rF1Mmradw6gvWsQKf63476DDXmomT9zUiGypLcG4ibIM67vhAj8jJRdbmEws2Aqw=="],
"bun-types": ["bun-types@1.2.5", "", { "dependencies": { "@types/node": "*", "@types/ws": "~8.5.10" } }, "sha512-3oO6LVGGRRKI4kHINx5PIdIgnLRb7l/SprhzqXapmoYkFl5m4j6EvALvbDVuuBFaamB46Ap6HCUxIXNLCGy+tg=="],
"bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
"call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="],
"chardet": ["chardet@0.7.0", "", {}, "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA=="],
"cli-width": ["cli-width@4.1.0", "", {}, "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ=="],
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
"commander": ["commander@13.1.0", "", {}, "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw=="],
"content-disposition": ["content-disposition@1.0.0", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg=="],
"content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="],
"cookie": ["cookie@0.7.1", "", {}, "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w=="],
"cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="],
"cors": ["cors@2.8.5", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g=="],
"d": ["d@1.0.2", "", { "dependencies": { "es5-ext": "^0.10.64", "type": "^2.7.2" } }, "sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw=="],
"debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="],
"depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
"destroy": ["destroy@1.2.0", "", {}, "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg=="],
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
"emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
"encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
"es5-ext": ["es5-ext@0.10.64", "", { "dependencies": { "es6-iterator": "^2.0.3", "es6-symbol": "^3.1.3", "esniff": "^2.0.1", "next-tick": "^1.1.0" } }, "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg=="],
"es6-iterator": ["es6-iterator@2.0.3", "", { "dependencies": { "d": "1", "es5-ext": "^0.10.35", "es6-symbol": "^3.1.1" } }, "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g=="],
"es6-symbol": ["es6-symbol@3.1.4", "", { "dependencies": { "d": "^1.0.2", "ext": "^1.7.0" } }, "sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg=="],
"escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="],
"esniff": ["esniff@2.0.1", "", { "dependencies": { "d": "^1.0.1", "es5-ext": "^0.10.62", "event-emitter": "^0.3.5", "type": "^2.7.2" } }, "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg=="],
"etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="],
"event-emitter": ["event-emitter@0.3.5", "", { "dependencies": { "d": "1", "es5-ext": "~0.10.14" } }, "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA=="],
"eventsource": ["eventsource@3.0.5", "", { "dependencies": { "eventsource-parser": "^3.0.0" } }, "sha512-LT/5J605bx5SNyE+ITBDiM3FxffBiq9un7Vx0EwMDM3vg8sWKx/tO2zC+LMqZ+smAM0F2hblaDZUVZF0te2pSw=="],
"eventsource-parser": ["eventsource-parser@3.0.0", "", {}, "sha512-T1C0XCUimhxVQzW4zFipdx0SficT651NnkR0ZSH3yQwh+mFMdLfgjABVi4YtMTtaL4s168593DaoaRLMqryavA=="],
"express": ["express@5.0.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.0.1", "content-disposition": "^1.0.0", "content-type": "~1.0.4", "cookie": "0.7.1", "cookie-signature": "^1.2.1", "debug": "4.3.6", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "^2.0.0", "fresh": "2.0.0", "http-errors": "2.0.0", "merge-descriptors": "^2.0.0", "methods": "~1.1.2", "mime-types": "^3.0.0", "on-finished": "2.4.1", "once": "1.4.0", "parseurl": "~1.3.3", "proxy-addr": "~2.0.7", "qs": "6.13.0", "range-parser": "~1.2.1", "router": "^2.0.0", "safe-buffer": "5.2.1", "send": "^1.1.0", "serve-static": "^2.1.0", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "^2.0.0", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-ORF7g6qGnD+YtUG9yx4DFoqCShNMmUKiXuT5oWMHiOvt/4WFbHC6yCwQMTSBMno7AqntNCAzzcnnjowRkTL9eQ=="],
"express-rate-limit": ["express-rate-limit@7.5.0", "", { "peerDependencies": { "express": "^4.11 || 5 || ^5.0.0-beta.1" } }, "sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg=="],
"ext": ["ext@1.7.0", "", { "dependencies": { "type": "^2.7.2" } }, "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw=="],
"external-editor": ["external-editor@3.1.0", "", { "dependencies": { "chardet": "^0.7.0", "iconv-lite": "^0.4.24", "tmp": "^0.0.33" } }, "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew=="],
"finalhandler": ["finalhandler@2.1.0", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q=="],
"forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
"fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="],
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
"http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="],
"iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
"ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
"inquirer": ["inquirer@12.5.0", "", { "dependencies": { "@inquirer/core": "^10.1.9", "@inquirer/prompts": "^7.4.0", "@inquirer/type": "^3.0.5", "ansi-escapes": "^4.3.2", "mute-stream": "^2.0.0", "run-async": "^3.0.0", "rxjs": "^7.8.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-aiBBq5aKF1k87MTxXDylLfwpRwToShiHrSv4EmB07EYyLgmnjEz5B3rn0aGw1X3JA/64Ngf2T54oGwc+BCsPIQ=="],
"ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
"is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
"is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="],
"is-typedarray": ["is-typedarray@1.0.0", "", {}, "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA=="],
"light-bolt11-decoder": ["light-bolt11-decoder@3.2.0", "", { "dependencies": { "@scure/base": "1.1.1" } }, "sha512-3QEofgiBOP4Ehs9BI+RkZdXZNtSys0nsJ6fyGeSiAGCBsMwHGUDS/JQlY/sTnWs91A2Nh0S9XXfA8Sy9g6QpuQ=="],
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
"media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="],
"merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="],
"methods": ["methods@1.1.2", "", {}, "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w=="],
"mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
"mime-types": ["mime-types@3.0.0", "", { "dependencies": { "mime-db": "^1.53.0" } }, "sha512-XqoSHeCGjVClAmoGFG3lVFqQFRIrTVw2OH3axRqAcfaw+gHWIfnASS92AV+Rl/mk0MupgZTRHQOjxY6YVnzK5w=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"mute-stream": ["mute-stream@2.0.0", "", {}, "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA=="],
"negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
"next-tick": ["next-tick@1.1.0", "", {}, "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ=="],
"node-gyp-build": ["node-gyp-build@4.8.4", "", { "bin": { "node-gyp-build": "bin.js", "node-gyp-build-optional": "optional.js", "node-gyp-build-test": "build-test.js" } }, "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ=="],
"nostr-tools": ["nostr-tools@2.11.0", "", { "dependencies": { "@noble/ciphers": "^0.5.1", "@noble/curves": "1.2.0", "@noble/hashes": "1.3.1", "@scure/base": "1.1.1", "@scure/bip32": "1.3.1", "@scure/bip39": "1.2.1" }, "optionalDependencies": { "nostr-wasm": "0.1.0" }, "peerDependencies": { "typescript": ">=5.0.0" }, "optionalPeers": ["typescript"] }, "sha512-kRtXI9j5f45NvIcdJacQ0UEAfEb7p/jhZqhAGLQWtUd5idZJPYdSyR8hdw+MmpGH4TCMH5plZrXzFltIIZrkEA=="],
"nostr-wasm": ["nostr-wasm@0.1.0", "", {}, "sha512-78BTryCLcLYv96ONU8Ws3Q1JzjlAt+43pWQhIl86xZmWeegYCNLPml7yQ+gG3vR6V5h4XGj+TxO+SS5dsThQIA=="],
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
"on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
"os-tmpdir": ["os-tmpdir@1.0.2", "", {}, "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g=="],
"parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
"path-to-regexp": ["path-to-regexp@8.2.0", "", {}, "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ=="],
"pkce-challenge": ["pkce-challenge@4.1.0", "", {}, "sha512-ZBmhE1C9LcPoH9XZSdwiPtbPHZROwAnMy+kIFQVrnMCxY4Cudlz3gBOpzilgc0jOgRaiT3sIWfpMomW2ar2orQ=="],
"proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
"qs": ["qs@6.13.0", "", { "dependencies": { "side-channel": "^1.0.6" } }, "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg=="],
"range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="],
"raw-body": ["raw-body@3.0.0", "", { "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", "iconv-lite": "0.6.3", "unpipe": "1.0.0" } }, "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g=="],
"router": ["router@2.1.0", "", { "dependencies": { "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-/m/NSLxeYEgWNtyC+WtNHCF7jbGxOibVWKnn+1Psff4dJGOfoXP+MuC/f2CwSmyiHdOIzYnYFp4W6GxWfekaLA=="],
"run-async": ["run-async@3.0.0", "", {}, "sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q=="],
"rxjs": ["rxjs@7.8.2", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA=="],
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
"send": ["send@1.1.0", "", { "dependencies": { "debug": "^4.3.5", "destroy": "^1.2.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^0.5.2", "http-errors": "^2.0.0", "mime-types": "^2.1.35", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.1" } }, "sha512-v67WcEouB5GxbTWL/4NeToqcZiAWEq90N888fczVArY8A79J0L4FD7vj5hm3eUMua5EpoQ59wa/oovY6TLvRUA=="],
"serve-static": ["serve-static@2.1.0", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.0.0" } }, "sha512-A3We5UfEjG8Z7VkDv6uItWw6HY2bBSBJT1KtVESn6EOoOr2jAxNhxWCLY3jDE2WcuHXByWju74ck3ZgLwL8xmA=="],
"setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="],
"side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="],
"side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="],
"side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="],
"side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="],
"signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
"statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="],
"string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
"strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"tmp": ["tmp@0.0.33", "", { "dependencies": { "os-tmpdir": "~1.0.2" } }, "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw=="],
"toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
"tseep": ["tseep@1.3.1", "", {}, "sha512-ZPtfk1tQnZVyr7BPtbJ93qaAh2lZuIOpTMjhrYa4XctT8xe7t4SAW9LIxrySDuYMsfNNayE51E/WNGrNVgVicQ=="],
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"tstl": ["tstl@2.5.16", "", {}, "sha512-+O2ybLVLKcBwKm4HymCEwZIT0PpwS3gCYnxfSDEjJEKADvIFruaQjd3m7CAKNU1c7N3X3WjVz87re7TA2A5FUw=="],
"type": ["type@2.7.3", "", {}, "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ=="],
"type-fest": ["type-fest@0.21.3", "", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="],
"type-is": ["type-is@2.0.0", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-gd0sGezQYCbWSbkZr75mln4YBidWUN60+devscpLF5mtRDUpiaTvKpBNrdaCvel1NdR2k6vclXybU5fBd2i+nw=="],
"typedarray-to-buffer": ["typedarray-to-buffer@3.1.5", "", { "dependencies": { "is-typedarray": "^1.0.0" } }, "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q=="],
"typescript": ["typescript@5.8.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ=="],
"typescript-lru-cache": ["typescript-lru-cache@2.0.0", "", {}, "sha512-Jp57Qyy8wXeMkdNuZiglE6v2Cypg13eDA1chHwDG6kq51X7gk4K7P7HaDdzZKCxkegXkVHNcPD0n5aW6OZH3aA=="],
"undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
"unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
"utf-8-validate": ["utf-8-validate@5.0.10", "", { "dependencies": { "node-gyp-build": "^4.3.0" } }, "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ=="],
"utf8-buffer": ["utf8-buffer@1.0.0", "", {}, "sha512-ueuhzvWnp5JU5CiGSY4WdKbiN/PO2AZ/lpeLiz2l38qwdLy/cW40XobgyuIWucNyum0B33bVB0owjFCeGBSLqg=="],
"utils-merge": ["utils-merge@1.0.1", "", {}, "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="],
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
"webln": ["webln@0.3.2", "", { "dependencies": { "@types/chrome": "^0.0.74" } }, "sha512-YYT83aOCLup2AmqvJdKtdeBTaZpjC6/JDMe8o6x1kbTYWwiwrtWHyO//PAsPixF3jwFsAkj5DmiceB6w/QSe7Q=="],
"websocket": ["websocket@1.0.35", "", { "dependencies": { "bufferutil": "^4.0.1", "debug": "^2.2.0", "es5-ext": "^0.10.63", "typedarray-to-buffer": "^3.1.5", "utf-8-validate": "^5.0.2", "yaeti": "^0.0.6" } }, "sha512-/REy6amwPZl44DDzvRCkaI1q1bIiQB0mEFQLUrhz3z2EK91cp3n72rAjUlrTP0zV22HJIUOVHQGPxhFRjxjt+Q=="],
"websocket-polyfill": ["websocket-polyfill@0.0.3", "", { "dependencies": { "tstl": "^2.0.7", "websocket": "^1.0.28" } }, "sha512-pF3kR8Uaoau78MpUmFfzbIRxXj9PeQrCuPepGE6JIsfsJ/o/iXr07Q2iQNzKSSblQJ0FiGWlS64N4pVSm+O3Dg=="],
"wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="],
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
"yaeti": ["yaeti@0.0.6", "", {}, "sha512-MvQa//+KcZCUkBTIC9blM+CU9J2GzuTytsOUwf2lidtvkx/6gnEp1QvJv34t9vdjhFmha/mUiNDbN0D0mJWdug=="],
"yaml": ["yaml@2.7.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA=="],
"yoctocolors-cjs": ["yoctocolors-cjs@2.1.2", "", {}, "sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA=="],
"zod": ["zod@3.24.2", "", {}, "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ=="],
"zod-to-json-schema": ["zod-to-json-schema@3.24.5", "", { "peerDependencies": { "zod": "^3.24.1" } }, "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g=="],
"@cashu/crypto/@scure/bip32": ["@scure/bip32@1.6.2", "", { "dependencies": { "@noble/curves": "~1.8.1", "@noble/hashes": "~1.7.1", "@scure/base": "~1.2.2" } }, "sha512-t96EPDMbtGgtb7onKKqxRLfE5g05k7uHnHRM2xdE6BP/ZmxaLtPek4J4KfVn/90IQNrU1IOAqMgiDtUdtbe3nw=="],
"@cashu/crypto/@scure/bip39": ["@scure/bip39@1.5.4", "", { "dependencies": { "@noble/hashes": "~1.7.1", "@scure/base": "~1.2.4" } }, "sha512-TFM4ni0vKvCfBpohoh+/lY05i9gRbSwXWngAsF4CABQxoaOHijxuaZ2R6cStDQ5CHtHO9aGJTr4ksVJASRRyMA=="],
"@scure/bip32/@noble/curves": ["@noble/curves@1.1.0", "", { "dependencies": { "@noble/hashes": "1.3.1" } }, "sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA=="],
"@scure/bip32/@noble/hashes": ["@noble/hashes@1.3.2", "", {}, "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ=="],
"@scure/bip32/@scure/base": ["@scure/base@1.1.1", "", {}, "sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA=="],
"@scure/bip39/@noble/hashes": ["@noble/hashes@1.3.2", "", {}, "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ=="],
"@scure/bip39/@scure/base": ["@scure/base@1.1.1", "", {}, "sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA=="],
"body-parser/iconv-lite": ["iconv-lite@0.5.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-kERHXvpSaB4aU3eANwidg79K8FlrN77m8G9V+0vOR3HYaRifrlwMEpT7ZBJqLSEIHnEgJTHcWK82wwLwwKwtag=="],
"body-parser/qs": ["qs@6.14.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w=="],
"express/debug": ["debug@4.3.6", "", { "dependencies": { "ms": "2.1.2" } }, "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg=="],
"external-editor/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="],
"light-bolt11-decoder/@scure/base": ["@scure/base@1.1.1", "", {}, "sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA=="],
"nostr-tools/@noble/curves": ["@noble/curves@1.2.0", "", { "dependencies": { "@noble/hashes": "1.3.2" } }, "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw=="],
"nostr-tools/@noble/hashes": ["@noble/hashes@1.3.1", "", {}, "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA=="],
"nostr-tools/@scure/base": ["@scure/base@1.1.1", "", {}, "sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA=="],
"send/fresh": ["fresh@0.5.2", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="],
"send/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
"websocket/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
"@scure/bip32/@noble/curves/@noble/hashes": ["@noble/hashes@1.3.1", "", {}, "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA=="],
"express/debug/ms": ["ms@2.1.2", "", {}, "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="],
"nostr-tools/@noble/curves/@noble/hashes": ["@noble/hashes@1.3.2", "", {}, "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ=="],
"send/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
"websocket/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
}
}

45
commands/find-snippets.ts Normal file
View File

@@ -0,0 +1,45 @@
import { Command } from 'commander';
import {
formatPartialMatches,
formatSnippets,
getSnippets
} from '../lib/nostr/snippets.js';
export function registerFindSnippetsCommand(program: Command): void {
program
.command('find-snippets')
.description('Find code snippets with optional filters')
.option('--limit <number>', 'Maximum number of snippets to return')
.option('--languages <list>', 'Comma-separated list of languages to filter by')
.option('--tags <list>', 'Comma-separated list of tags to filter by')
.option('--authors <list>', 'Comma-separated list of authors to filter by')
.action(async (options) => {
try {
// Parse options
const limit = options.limit ? parseInt(options.limit, 10) : undefined;
const languages = options.languages ? options.languages.split(',') : undefined;
const tags = options.tags ? options.tags.split(',') : undefined;
const authors = options.authors ? options.authors.split(',') : undefined;
const { snippets, otherSnippets } = await getSnippets({
limit,
languages,
tags,
authors,
});
if (snippets.length === 0) {
console.log("No code snippets found matching the criteria.");
} else {
const formattedSnippets = formatSnippets(snippets);
const partialMatchesText = formatPartialMatches(otherSnippets);
console.log(
`Found ${snippets.length} code snippets:\n\n${formattedSnippets}${partialMatchesText}`
);
}
} catch (error) {
console.error("Error executing find-snippets command:", error);
process.exit(1);
}
});
}

43
commands/find-user.ts Normal file
View File

@@ -0,0 +1,43 @@
import { Command } from 'commander';
import { ndk } from '../ndk.js';
import { knownUsers } from '../users.js';
import { identifierToPubkeys } from '../lib/nostr/utils.js';
export function registerFindUserCommand(program: Command): void {
program
.command('find-user')
.description('Find a user by identifier')
.argument('<query>', 'User identifier to search for')
.action(async (query: string) => {
try {
const pubkeys = identifierToPubkeys(query);
if (pubkeys.length > 0) {
const result = pubkeys.map(formatUser).join('\n\n---\n\n');
console.log(result);
} else {
console.log("No user found matching the query.");
}
} catch (error) {
console.error('Error executing find-user command:', error);
process.exit(1);
}
});
}
// Helper function to format user profiles
function formatUser(pubkey: string) {
const profile = knownUsers[pubkey]?.profile;
const user = ndk.getUser({ pubkey });
const keys: Record<string, string> = {
Npub: user.npub,
};
if (profile?.name) keys.Name = profile.name;
if (profile?.about) keys.About = profile.about;
if (profile?.picture) keys.Picture = profile.picture;
return Object.entries(keys)
.map(([key, value]) => `${key}: ${value}`)
.join("\n");
}

34
commands/index.ts Normal file
View File

@@ -0,0 +1,34 @@
import { Command } from 'commander';
import { registerFindUserCommand } from './find-user.js';
import { registerFindSnippetsCommand } from './find-snippets.js';
import { registerWotCommand } from './wot.js';
import { registerListUsernamesCommand } from './list-usernames.js';
import { registerMcpCommand } from './mcp.js';
import { registerSetupCommand } from './setup.js';
// Create a new Commander program
const program = new Command();
// Setup program metadata
program
.name('mcp-nostr')
.description('Model Context Protocol for Nostr')
.version('1.0.0');
// Register all commands
registerMcpCommand(program);
registerFindUserCommand(program);
registerFindSnippetsCommand(program);
registerWotCommand(program);
registerListUsernamesCommand(program);
registerSetupCommand(program);
// Function to run the CLI
export async function runCli(args: string[]) {
program.parse(args);
// If no command was specified, show help by default
if (args.length <= 2) {
program.help();
}
}

View File

@@ -0,0 +1,24 @@
import { Command } from 'commander';
import { listUsernames } from '../logic/list_usernames.js';
export function registerListUsernamesCommand(program: Command): void {
program
.command('list-usernames')
.description('List all usernames in the database')
.action(async () => {
try {
const result = await listUsernames();
// Extract text content from the response
if (result.content && result.content.length > 0) {
const textContent = result.content.find(item => item.type === 'text');
if (textContent) {
console.log(textContent.text);
}
}
} catch (error) {
console.error("Error executing list-usernames command:", error);
process.exit(1);
}
});
}

74
commands/mcp.ts Normal file
View File

@@ -0,0 +1,74 @@
import { Command } from 'commander';
import { readConfig } from "../config.js";
import { addCreatePubkeyCommand } from "../logic/create-pubkey.js";
import { addFindSnippetsCommand } from "../logic/find_snippets.js";
import { addFindUserCommand } from "../logic/find_user.js";
import { addListUsernamesCommand } from "../logic/list_usernames.js";
import { addPublishCodeSnippetCommand } from "../logic/publish-code-snippet.js";
import { addPublishCommand } from "../logic/publish.js";
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
// Define type for command functions
type CommandFunction = (server: McpServer) => void;
// Map of command names to their registration functions
const commandMap: Record<string, CommandFunction> = {
publish: addPublishCommand,
"publish-snippet": addPublishCodeSnippetCommand,
"create-pubkey": addCreatePubkeyCommand,
"find-user": addFindUserCommand,
"find-snippets": addFindSnippetsCommand,
"list-usernames": addListUsernamesCommand,
};
// Global server instance
let mcpServer: McpServer | null = null;
export function registerMcpCommand(program: Command): void {
program
.command('mcp')
.description('Start the MCP server')
.action(async () => {
try {
// Create the MCP server
mcpServer = new McpServer({
name: "Nostr Publisher",
version: "1.0.0",
});
// Register all MCP commands
registerMcpCommands(mcpServer);
// Connect the server to the transport
const transport = new StdioServerTransport();
await mcpServer.connect(transport);
} catch (error) {
console.error("Error starting MCP server:", error);
process.exit(1);
}
});
}
// Register all MCP commands, filtered by config if specified
export function registerMcpCommands(server: McpServer) {
const config = readConfig();
const enabledCommands = config.mcpCommands;
// If mcpCommands is specified in config, only register those commands
if (enabledCommands && enabledCommands.length > 0) {
for (const cmd of enabledCommands) {
if (commandMap[cmd]) {
commandMap[cmd](server);
}
}
} else {
// Otherwise register all commands
addPublishCommand(server);
addPublishCodeSnippetCommand(server);
addCreatePubkeyCommand(server);
addFindUserCommand(server);
addFindSnippetsCommand(server);
addListUsernamesCommand(server);
}
}

18
commands/setup.ts Normal file
View File

@@ -0,0 +1,18 @@
import { Command } from "commander";
import { runConfigWizard } from "../wizard";
import { readConfig } from "../config.js";
/**
* Register the setup command with the Commander program
* @param program The Commander program instance
*/
export function registerSetupCommand(program: Command) {
program
.command("setup")
.description("Run the configuration wizard to set up MCP-Nostr")
.action(async () => {
const config = readConfig();
await runConfigWizard(config);
console.log("Setup complete! You can now use MCP-Nostr.");
});
}

92
commands/wot.ts Normal file
View File

@@ -0,0 +1,92 @@
import { Command } from 'commander';
import { ndk } from '../ndk.js';
import { db } from '../db.js';
import { knownUsers } from '../users.js';
import { getFollowerCount, getFollowing } from '../wot.js';
export function registerWotCommand(program: Command): void {
program
.command('wot')
.description('Get Web of Trust information for a user')
.argument('<pubkey>', 'Public key or npub of the user')
.action(async (pubkey: string) => {
try {
if (!pubkey) {
console.log("Missing pubkey parameter. Usage: wot <pubkey>");
process.exit(1);
}
// Validate pubkey format (simple check - proper validation would be more complex)
if (pubkey.length !== 64 && !pubkey.startsWith("npub")) {
console.log(
"Invalid pubkey format. Please provide a valid hex pubkey or npub."
);
process.exit(1);
}
// Get actual hex pubkey if npub was provided
let hexPubkey = pubkey;
if (pubkey.startsWith("npub")) {
hexPubkey = ndk.getUser({ npub: pubkey }).pubkey;
}
// Get follower count
const followerCount = getFollowerCount(hexPubkey);
// Get following count
const following = getFollowing(hexPubkey);
const followingCount = following.length;
// Get profile info if available
const profile = knownUsers[hexPubkey]?.profile;
const name = profile?.name || hexPubkey;
console.log(`Web of Trust for ${name}:`);
console.log(`Pubkey: ${hexPubkey}`);
if (ndk.getUser({ pubkey: hexPubkey }).npub) {
console.log(`Npub: ${ndk.getUser({ pubkey: hexPubkey }).npub}`);
}
console.log(`Followers: ${followerCount}`);
console.log(`Following: ${followingCount}`);
// Calculate follow score ratio (simple metric)
const ratio =
followingCount > 0
? (followerCount / followingCount).toFixed(2)
: "N/A";
console.log(`Follower/Following Ratio: ${ratio}`);
// Most popular followers (if any)
if (followerCount > 0) {
const popularFollowers = db
.query(`
SELECT f.follower, COUNT(*) as count
FROM wot f
JOIN wot f2 ON f.follower = f2.followed
WHERE f.followed = ?
GROUP BY f.follower
ORDER BY count DESC
LIMIT 5
`)
.all(hexPubkey) as { follower: string; count: number }[];
if (popularFollowers.length > 0) {
console.log("\nMost influential followers:");
for (const follower of popularFollowers) {
const followerProfile =
knownUsers[follower.follower]?.profile;
const followerName =
followerProfile?.name ||
`${follower.follower.substring(0, 8)}...`;
console.log(
`- ${followerName} (followed by ${follower.count} users)`
);
}
}
}
} catch (error) {
console.error("Error executing wot command:", error);
process.exit(1);
}
});
}

116
config.ts Normal file
View File

@@ -0,0 +1,116 @@
import { existsSync, readFileSync, writeFileSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
// Define interfaces for config structure
export interface UserData {
nsec?: string;
npub?: string;
display_name?: string;
about?: string;
profile?: {
name?: string;
about?: string;
picture?: string;
};
}
export interface ConfigData {
privateKey?: string;
bunker?: string;
bunkerLocalKey?: string;
dbPath?: string;
relays?: string[];
wotFrom?: string;
mcpCommands?: string[];
users?: Record<string, UserData>;
editor?: string;
}
// Path to the config file
const CONFIG_PATH = join(homedir(), ".mcp-nostr.json");
/**
* Read the config file and parse its contents
* @returns The parsed config data
*/
export function readConfig(): ConfigData {
let config: ConfigData = {};
if (existsSync(CONFIG_PATH)) {
try {
config = JSON.parse(readFileSync(CONFIG_PATH, "utf-8"));
} catch (err) {
console.error("Error reading config file:", err);
}
}
// Ensure config is an object
return config ?? {};
}
/**
* Write config data to the config file
* @param config The config data to write
*/
export function writeConfig(config: ConfigData): void {
writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
}
/**
* Get a user from the config by username
* @param username The username to look up
* @returns The user data if found, undefined otherwise
*/
export function getUser(username: string): UserData | undefined {
const config = readConfig();
return config.users?.[username];
}
/**
* Save a user to the config
* @param username The username to save
* @param userData The user data to save
*/
export function saveUser(username: string, userData: UserData): void {
const config = readConfig();
if (!config.users) {
config.users = {};
}
config.users[username] = userData;
writeConfig(config);
}
/**
* Get all users from the config
* @returns A record of all users
*/
export function getAllUsers(): Record<string, UserData> {
const config = readConfig();
return config.users || {};
}
/**
* Get the path to the config file
* @returns The path to the config file
*/
export function getConfigPath(): string {
return CONFIG_PATH;
}
/**
* Ensure the config has a default database path
* @returns The updated config
*/
export function initConfig(): ConfigData {
const config = readConfig();
if (!config.dbPath) {
config.dbPath = join(homedir(), ".mcp-nostr.db");
writeConfig(config);
}
return config;
}

76
db.ts Normal file
View File

@@ -0,0 +1,76 @@
import { Database } from "bun:sqlite";
import { homedir } from "node:os";
import { join } from "node:path";
import { readConfig } from "./config.js";
// Load config
const config = readConfig();
// Set database path from config or use default
const dbPath = config.dbPath || join(homedir(), ".mcp-nostr.db");
export const db = new Database(dbPath);
// Create migration table if it doesn't exist
db.run(`
CREATE TABLE IF NOT EXISTS migrations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`);
// Define migration interface
interface Migration {
name: string;
module: () => Promise<{ up: (db: Database) => Promise<void> }>;
}
// Import all migrations
// Add new migrations to this array when created
export const migrations: Migration[] = [
// Example:
// { name: '001_initial_schema', module: () => import('./migrations/001_initial_schema.js') },
// Add all migrations here
];
/**
* Apply any pending migrations
*/
export async function applyMigrations() {
// Get already applied migrations
const appliedMigrations = db.query("SELECT name FROM migrations").all() as {
name: string;
}[];
const appliedSet = new Set(appliedMigrations.map((m) => m.name));
for (const migration of migrations) {
// Skip if already applied
if (appliedSet.has(migration.name)) {
continue;
}
console.log(`Applying migration: ${migration.name}`);
try {
// Import and run the migration
const module = await migration.module();
await module.up(db);
// Record that the migration was applied
db.run("INSERT INTO migrations (name) VALUES (?)", [
migration.name,
]);
console.log(`Successfully applied migration: ${migration.name}`);
} catch (error) {
console.error(
`Failed to apply migration ${migration.name}:`,
error
);
throw error;
}
}
}
// Call this during app initialization
await applyMigrations();

22
index.ts Normal file
View File

@@ -0,0 +1,22 @@
import { initConfig, readConfig, writeConfig } from "./config.js";
import { initNDK, ndk } from "./ndk.js";
import "./db.js";
import { runCli } from "./commands/index.js";
import { runConfigWizard } from "./wizard";
// Load config and ensure defaults
const config = initConfig();
// If there's no privateKey or bunker configured, run the setup wizard
if (!config.privateKey && !config.bunker) {
const updatedConfig = await runConfigWizard(config);
await initNDK(updatedConfig);
} else {
await initNDK(config);
}
// Process command-line arguments
const args = process.argv;
// Check if running with "mcp" command
await runCli(args);

30
lib/README.md Normal file
View File

@@ -0,0 +1,30 @@
# MCP-NDK Library Structure
This directory contains reusable code organized into modules to reduce duplication and improve maintainability.
## Directory Structure
- `/lib` - Core libraries and utilities
- `/types` - TypeScript type definitions used across the application
- `/nostr` - Nostr-related functionality
- `utils.ts` - Utility functions for working with Nostr
- `snippets.ts` - Functions for managing code snippets
- `/utils` - General utility functions
- `log.ts` - Logging functionality
## Design Principles
1. **Single Responsibility**: Each module has a specific, focused purpose
2. **DRY (Don't Repeat Yourself)**: Common code is extracted into reusable functions
3. **Separation of Concerns**: Clear separation between types, utilities, and business logic
4. **Consistency**: Consistent patterns and naming conventions throughout the codebase
## How to Use
When adding new functionality, follow these guidelines:
1. Place type definitions in `/lib/types`
2. Place general utilities in `/lib/utils`
3. Organize Nostr-specific code under `/lib/nostr`
4. Aim to minimize duplication by leveraging existing utilities
5. Keep command files focused on their specific command, delegating to library code for implementation

136
lib/nostr/snippets.ts Normal file
View File

@@ -0,0 +1,136 @@
import type { NDKEvent, NDKFilter } from "@nostr-dev-kit/ndk";
import { ndk } from "../../ndk.js";
import { knownUsers } from "../../users.js";
import type { CodeSnippet, FindSnippetsParams } from "../types/index.js";
import { log } from "../utils/log.js";
import { SNIPPET_KIND, eventToSnippet, identifierToPubkeys } from "./utils.js";
/**
* Get code snippets from Nostr events of kind 1337
*
* @param params - Parameters to filter snippets
* @returns Array of code snippets
*/
export async function getSnippets(params: FindSnippetsParams = {}): Promise<{
snippets: CodeSnippet[];
otherSnippets: CodeSnippet[];
}> {
// Construct filter based on params
const filter: NDKFilter = {
kinds: [SNIPPET_KIND as number],
limit: params.limit || 500,
};
// Add optional filters
if (params.since) {
filter.since = params.since;
}
if (params.until) {
filter.until = params.until;
}
if (params.authors && params.authors.length > 0) {
for (const author of params.authors) {
const pubkeys = identifierToPubkeys(author);
if (pubkeys.length) {
filter.authors ??= [];
filter.authors.push(...pubkeys);
} else {
log(`Unknown author: ${author}`);
}
}
}
// Add custom tag filters for languages and tags
if (params.languages && params.languages.length > 0) {
filter["#l"] = params.languages;
}
if (params.tags && params.tags.length > 0) {
filter["#t"] = params.tags;
}
log(`Fetching snippets with filter: ${JSON.stringify(filter, null, 2)}`);
// Fetch events
const events = await ndk.fetchEvents(filter);
let maxMatchCount = 0;
function getMatchCount(event: NDKEvent) {
if (!params.tags || params.tags.length === 0) return 1;
const aTags = event.tags
.filter((tag) => tag[0] === "t")
.map((tag) => tag[1])
.filter((t) => t !== undefined);
return params.tags.filter((tag) =>
aTags.some((t) => t.match(new RegExp(tag, "i")))
).length;
}
for (const event of events) {
const aMatches = getMatchCount(event);
if (aMatches > maxMatchCount) maxMatchCount = aMatches;
}
const selectedEvents: NDKEvent[] = [];
const notSelectedEvents: NDKEvent[] = [];
for (const event of events) {
if (getMatchCount(event) === maxMatchCount) {
selectedEvents.push(event);
} else {
notSelectedEvents.push(event);
}
}
// Convert events to snippets
const snippets = selectedEvents.map(eventToSnippet);
const otherSnippets = notSelectedEvents.map(eventToSnippet);
return { snippets, otherSnippets };
}
/**
* Format snippets for display
* @param snippets Array of code snippets
* @returns Formatted string representation
*/
export function formatSnippets(snippets: CodeSnippet[]): string {
return snippets
.map((snippet) => {
const author = knownUsers[snippet.pubkey];
const keys: Record<string, string> = {
Title: snippet.title,
Language: snippet.language,
Tags: snippet.tags.join(", "),
Code: snippet.code,
};
if (author?.profile?.name) keys.Author = author.profile.name;
return Object.entries(keys)
.map(([key, value]) => `${key}: ${value}`)
.join("\n");
})
.join("\n\n---\n\n");
}
/**
* Format partial match snippets for display
* @param snippets Array of code snippets
* @returns Formatted string representation
*/
export function formatPartialMatches(snippets: CodeSnippet[]): string {
if (snippets.length === 0) return "";
let text =
"\n\nSome other events not included in this result since they had less in common with your search, here is a list of the events that had partial matches:\n\n";
text += snippets
.map((snippet) => {
return ` * ${snippet.title}:\n Tags: ${snippet.tags.join(", ")}`;
})
.join("\n");
return text;
}

76
lib/nostr/utils.ts Normal file
View File

@@ -0,0 +1,76 @@
import type { NDKEvent, NDKSigner } from "@nostr-dev-kit/ndk";
import { NDKPrivateKeySigner } from "@nostr-dev-kit/ndk";
import { ndk } from "../../ndk.js";
import { queryUser } from "../../users.js";
import type { CodeSnippet } from "../types/index.js";
import { getUser } from "../../config.js";
export const SNIPPET_KIND = 1337;
/**
* Gets the appropriate signer based on the username
* @param username Username to get signer for (or "main" for default)
* @returns The signer to use
* @throws Error if user not found or missing nsec
*/
export async function getSigner(username?: string): Promise<NDKSigner> {
// If no username or "main", return the default signer
if (!username || username === "main") {
if (!ndk.signer) {
throw new Error("No default signer configured");
}
return ndk.signer;
}
// Otherwise, get the user's signer
const userData = getUser(username);
if (!userData?.nsec) {
throw new Error(`User "${username}" not found in config or missing nsec`);
}
return new NDKPrivateKeySigner(userData.nsec);
}
/**
* Converts an identifier (pubkey, npub, or name) to pubkeys
* @param identifier The identifier to convert
* @returns Array of pubkeys
*/
export function identifierToPubkeys(identifier: string): string[] {
// If it's an npub, convert directly
if (identifier.startsWith("npub")) {
return [ndk.getUser({ npub: identifier }).pubkey];
}
// If it's a hex pubkey, return as is
if (identifier.length === 64 && /^[0-9a-f]+$/i.test(identifier)) {
return [identifier];
}
// Otherwise, search by profile name or other attributes
return queryUser(identifier);
}
/**
* Converts an NDKEvent into a CodeSnippet
* @param event NDKEvent of kind 1337
* @returns CodeSnippet object
*/
export function eventToSnippet(event: NDKEvent): CodeSnippet {
const title = event.tagValue("title") ?? event.tagValue("name");
const language = event.tagValue("l");
const tags = event.tags
.filter((tag) => tag[0] === "t" && tag[1] !== undefined)
.map((tag) => tag[1] as string);
return {
id: event.id,
title: title || "Untitled",
code: event.content,
language: language || "text",
pubkey: event.pubkey,
createdAt: event.created_at || 0,
tags,
};
}

25
lib/types/index.ts Normal file
View File

@@ -0,0 +1,25 @@
import type { NDKUserProfile } from "@nostr-dev-kit/ndk";
export type UserEntry = {
profile: NDKUserProfile;
data: string;
};
export interface FindSnippetsParams {
limit?: number;
since?: number;
until?: number;
authors?: string[];
languages?: string[];
tags?: string[];
}
export type CodeSnippet = {
id: string;
title: string;
code: string;
language: string;
pubkey: string;
createdAt: number;
tags: string[];
};

5
lib/utils/log.ts Normal file
View File

@@ -0,0 +1,5 @@
/**
* Simple logging utility
* @param message Message to log
*/
export function log(_message: string): void {}

100
logic/create-pubkey.ts Normal file
View File

@@ -0,0 +1,100 @@
import NDK, { NDKPrivateKeySigner, NDKUser, NDKEvent } from "@nostr-dev-kit/ndk";
import { z } from "zod";
import { getUser, saveUser } from "../config.js";
import { log } from "../lib/utils/log.js";
import { ndk } from "../ndk.js";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
export async function createPubkey({
username,
display_name,
about,
}: {
username: string;
display_name: string;
about?: string;
}) {
try {
// Check if username already exists
const existingUser = getUser(username);
if (existingUser) {
throw new Error(`Username "${username}" already exists`);
}
// Generate a new keypair for this user
const signer = NDKPrivateKeySigner.generate();
// Create profile metadata event (kind 0)
const profileContent = JSON.stringify({
display_name,
name: display_name,
about: about || "",
});
// Create the event
const event = new NDKEvent(ndk, {
kind: 0,
content: profileContent,
tags: [],
});
// Sign with the new signer
await event.sign(signer);
// Publish the event
await event.publish();
// Save the user to the config
saveUser(username, {
nsec: signer.privateKey,
display_name,
about: about || "",
});
// Create user object for return
const user = ndk.getUser({ pubkey: signer.pubkey });
// Return success message
log(`Created pubkey for ${username} with public key ${signer.pubkey}`);
return {
user,
message: `Created pubkey for ${username} with npub ${user.npub}`,
};
} catch (error: unknown) {
const errorMessage =
error instanceof Error ? error.message : String(error);
throw new Error(`Failed to create pubkey: ${errorMessage}`);
}
}
export function addCreatePubkeyCommand(server: McpServer) {
server.tool(
"create_pubkey",
"Create a new keypair and save it to config",
{
username: z.string().describe("Username to create"),
display_name: z.string().describe("Display name for the pubkey"),
about: z
.string()
.optional()
.describe("About information for this pubkey"),
},
async ({ username, display_name, about }) => {
const result = await createPubkey({
username,
display_name,
about,
});
return {
content: [
{
type: "text",
text: result.message,
},
],
};
}
);
}

95
logic/find_snippets.ts Normal file
View File

@@ -0,0 +1,95 @@
import { z } from "zod";
import {
formatPartialMatches,
formatSnippets,
getSnippets,
} from "../lib/nostr/snippets.js";
import type { FindSnippetsParams } from "../lib/types/index.js";
import { log } from "../lib/utils/log.js";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
/**
* Find code snippets with optional filtering
* @param params Search parameters for filtering snippets
* @returns Formatted snippets that match the criteria
*/
export async function findSnippets({
since,
until,
authors,
languages,
tags,
}: FindSnippetsParams) {
try {
const { snippets, otherSnippets } = await getSnippets({
limit: 500, // Get more snippets to find the max matches
since,
until,
authors,
languages,
tags,
});
if (snippets.length === 0) {
return {
content: [
{
type: "text" as const,
text: "No code snippets found matching the criteria.",
},
],
};
}
// Format snippets for display
const formattedSnippets = formatSnippets(snippets);
const partialMatchesText = formatPartialMatches(otherSnippets);
return {
content: [
{
type: "text" as const,
text: `Found ${snippets.length} code snippets:\n\n${formattedSnippets}${partialMatchesText}`,
},
],
};
} catch (error: unknown) {
const errorMessage =
error instanceof Error ? error.message : String(error);
throw new Error(`Failed to find snippets: ${errorMessage}`);
}
}
export function addFindSnippetsCommand(server: McpServer) {
server.tool(
"find_snippets",
"Find code snippets with optional filtering by author, language, and tags",
{
since: z
.number()
.optional()
.describe("Unix timestamp to fetch snippets from"),
until: z
.number()
.optional()
.describe("Unix timestamp to fetch snippets until"),
authors: z
.array(z.string())
.optional()
.describe(
"List of author names to filter by (in username format!)"
),
languages: z
.array(z.string())
.optional()
.describe("List of programming languages to filter by"),
tags: z
.array(z.string())
.optional()
.describe(
"List of tags to filter by, be exhaustive, e.g. [ 'ndk', 'nostr', 'pubkey', 'signer' ]"
),
},
async (args) => findSnippets(args)
);
}

77
logic/find_user.ts Normal file
View File

@@ -0,0 +1,77 @@
import { z } from "zod";
import { ndk } from "../ndk.js";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { knownUsers } from "../users.js";
import { queryUser } from "../users.js";
/**
* Find a user by name, npub, or other profile information
* @param query The search query to find a user
* @returns Results with formatted user information
*/
export async function findUser(query: string) {
try {
const pubkeys = queryUser(query);
if (pubkeys.length === 0) {
return {
content: [
{
type: "text" as const,
text: "No users found matching the query.",
},
],
};
}
// Format the found users for display
const formattedUsers = pubkeys.map(formatUser).join("\n\n---\n\n");
return {
content: [
{
type: "text" as const,
text: `Found ${pubkeys.length} users:\n\n${formattedUsers}`,
},
],
};
} catch (error: unknown) {
const errorMessage =
error instanceof Error ? error.message : String(error);
throw new Error(`Failed to find user: ${errorMessage}`);
}
}
export function addFindUserCommand(server: McpServer) {
server.tool(
"find_user",
"Find a user by name, npub, or other profile information",
{
query: z.string().describe("The search query to find a user"),
},
async ({ query }) => {
return findUser(query);
}
);
}
/**
* Format user profile data for display
* @param pubkey User public key
* @returns Formatted string representation
*/
function formatUser(pubkey: string): string {
const profile = knownUsers[pubkey]?.profile;
const user = ndk.getUser({ pubkey });
const keys: Record<string, string> = {
Npub: user.npub,
};
if (profile?.name) keys.Name = profile.name;
if (profile?.about) keys.About = profile.about;
if (profile?.picture) keys.Picture = profile.picture;
return Object.entries(keys)
.map(([key, value]) => `${key}: ${value}`)
.join("\n");
}

68
logic/list_usernames.ts Normal file
View File

@@ -0,0 +1,68 @@
import { z } from "zod";
import { ndk } from "../ndk.js";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { getAllUsers } from "../config.js";
import { NDKPrivateKeySigner } from "@nostr-dev-kit/ndk";
/**
* List all available usernames in the system
* @returns Formatted list of available usernames
*/
export async function listUsernames() {
try {
const users = await Promise.all(
Object.values(getAllUsers()).map(async (user) => {
const signer = new NDKPrivateKeySigner(user.nsec);
const npub = (await signer.user()).npub;
return {
name: user.display_name,
npub: npub,
};
})
);
if (users.length === 0) {
return {
content: [
{
type: "text" as const,
text: "No users found. Try creating a user with the create_pubkey command first.",
},
],
};
}
const usersList = users
.map((user) => `${user.name} - ${user.npub}`)
.join("\n");
return {
content: [
{
type: "text" as const,
text: `Available users (${users.length}):\n${usersList}`,
},
],
};
} catch (error: unknown) {
const errorMessage =
error instanceof Error ? error.message : String(error);
throw new Error(`Failed to list usernames: ${errorMessage}`);
}
}
export function addListUsernamesCommand(server: McpServer) {
server.tool(
"list_usernames",
"List all available usernames in the system",
{
random_string: z
.string()
.optional()
.describe("Dummy parameter for no-parameter tools"),
},
async () => {
return listUsernames();
}
);
}

View File

@@ -0,0 +1,253 @@
import { NDKEvent } from "@nostr-dev-kit/ndk";
import { z } from "zod";
import { SNIPPET_KIND, getSigner } from "../lib/nostr/utils.js";
import { ndk } from "../ndk.js";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { writeFileSync, readFileSync } from "node:fs";
import { join } from "node:path";
import { homedir, tmpdir } from "node:os";
import { readConfig, getUser } from "../config.js";
import { existsSync } from "node:fs";
import * as Bun from "bun";
function log(message: string, ...args: any[]) {
// append to ~/.nmcp-nostr.log
const logFilePath = join(homedir(), ".nmcp-nostr.log");
const logMessage = `${new Date().toISOString()} - ${message}\n`;
writeFileSync(logFilePath, logMessage, { flag: "a" });
}
/**
* Parse metadata from the beginning of a file
* Format:
* ---METADATA---
* Title: My Title
* Description: My description goes here...
* Language: javascript
* Tags: tag1, tag2, tag3, tag4, tag5
* ---CODE---
*/
export function parseMetadata(fileContent: string): {
metadata: { title: string; description: string; language: string; tags: string[] };
code: string
} {
// Match the metadata and code sections - this regex was matching incorrectly
// Making it non-greedy for the first part and fixing the boundary for CODE marker
const metadataRegex = /^---METADATA---([\s\S]*?)(?=^---CODE---$)(^---CODE---$)([\s\S]*)$/m;
const matches = fileContent.match(metadataRegex);
if (!matches || matches.length < 4) {
throw new Error("Invalid file format: metadata section not found");
}
const metadataSection = matches[1] || "";
let codeSection = matches[3] || "";
// Remove leading newline from code section if present
if (codeSection.startsWith("\n")) {
codeSection = codeSection.substring(1);
}
// Parse each field with proper multiline flag
const titleMatch = metadataSection.match(/^Title:\s*(.+)$/m);
const title = titleMatch && titleMatch[1] ? titleMatch[1].trim() : "";
// Extract description which can be multiline but should stop at Language: or Tags:
const descriptionLines = [];
let inDescription = false;
// Process line by line
const lines = metadataSection.split('\n');
for (const line of lines) {
if (line.trim().startsWith('Description:')) {
inDescription = true;
const content = line.replace(/^Description:\s*/, '').trim();
if (content) {
descriptionLines.push(content);
}
} else if (line.trim().startsWith('Language:') || line.trim().startsWith('Tags:')) {
inDescription = false;
} else if (inDescription) {
descriptionLines.push(line);
}
}
const description = descriptionLines.join('\n').trim();
const languageMatch = metadataSection.match(/^Language:\s*(.+)$/m);
const language = languageMatch && languageMatch[1] ? languageMatch[1].trim() : "";
const tagsMatch = metadataSection.match(/^Tags:\s*(.+)$/m);
const tagsString = tagsMatch && tagsMatch[1] ? tagsMatch[1].trim() : "";
const tags = tagsString.split(',').map(tag => tag.trim()).filter(Boolean);
return {
metadata: {
title,
description,
language,
tags
},
code: codeSection
};
}
/**
* Create a file with metadata and code sections
*/
export function createFileWithMetadata(
title: string,
description: string,
language: string,
tags: string[],
code: string
): string {
return `---METADATA---
# Edit the metadata below. Keep the format exactly as shown (Title:, Description:, Language:, Tags:)
# Description needs to be at least 140 characters and Tags need at least 5 entries
# Don't remove the ---METADATA--- and ---CODE--- markers!
Title: ${title}
Description: ${description}
Language: ${language}
Tags: ${tags.join(', ')}
---CODE---
${code}`;
}
/**
* Publish a code snippet to Nostr
* @param title Title of the snippet
* @param description Description of the snippet
* @param language Programming language
* @param code The code snippet content
* @param tags Tags to categorize the snippet
* @param username Username to publish as
* @returns Publication results
*/
export async function publishCodeSnippet(
title: string,
description: string,
language: string,
code: string,
tags: string[] = [],
username?: string
): Promise<{ content: Array<{ type: "text", text: string }> }> {
try {
// Validate minimum requirements
// if (tags.length < 5) {
// throw new Error(
// "Insufficient tags. At least 5 tags are required. Please add more relevant and accurate information."
// );
// }
// if (description.length < 140) {
// throw new Error(
// "Description is too short. At least 140 characters are required. Please add more relevant and accurate information."
// );
// }
// put the code snippet in a temp file and run the command in config.editor or `code` and wait until it's closed -- then read the file and publish it
const config = readConfig();
const tempFilePath = join(tmpdir(), `snippet-${Date.now()}.${language}`);
// Create file content with metadata section for editing
const fileContent = createFileWithMetadata(title, description, language, tags, code);
// Write the content to the temp file
writeFileSync(tempFilePath, fileContent);
// Use the editor specified in config, or default to 'code' (VS Code)
const editorCommand = (config.editor || 'code --wait').split(' ');
// Spawn the editor process - first arg is the command array including both the command and its arguments
const process = Bun.spawn([...editorCommand, tempFilePath]);
log("spawned editor process to edit " + tempFilePath);
// Wait for the editor to close
await process.exited;
// Read the potentially modified content from the temp file
let updatedTitle = title;
let updatedDescription = description;
let updatedLanguage = language;
let updatedTags = tags;
let updatedCode = code;
if (existsSync(tempFilePath)) {
const updatedContent = readFileSync(tempFilePath, "utf-8");
try {
log("updatedContent: " + updatedContent);
const parsed = parseMetadata(updatedContent);
updatedTitle = parsed.metadata.title || title;
updatedDescription = parsed.metadata.description || description;
updatedLanguage = parsed.metadata.language || language;
updatedTags = parsed.metadata.tags.length >= 5 ? parsed.metadata.tags : tags;
updatedCode = parsed.code;
} catch (error) {
log("error " + error);
console.error("Error parsing metadata:", error);
// Fallback to using the file content as just code if metadata parsing fails
updatedCode = updatedContent;
}
} else {
log("tempFilePath does not exist", tempFilePath);
}
const eventTags = [
["name", updatedTitle],
["description", updatedDescription],
["l", updatedLanguage],
...updatedTags.map((tag) => ["t", tag]),
];
const event = new NDKEvent(ndk, {
kind: SNIPPET_KIND,
content: updatedCode,
tags: eventTags,
});
// Get the appropriate signer based on username
const signer = await getSigner(username);
// Sign the event with the selected signer
await event.sign(signer);
// Publish the already signed event
await event.publish();
return {
content: [
{
type: "text",
text: `Published code snippet "${updatedTitle}" to Nostr: The snippet can be seeen in https://snipsnip.dev/snippet/${event.id} or https://njump.me/${event.encode()}`,
},
],
};
} catch (error: unknown) {
const errorMessage =
error instanceof Error ? error.message : String(error);
throw new Error(`Failed to publish code snippet: ${errorMessage}`);
}
}
export function addPublishCodeSnippetCommand(server: McpServer) {
server.tool(
"publish-new-code-snippet",
"Publish a new code snippet to Nostr",
{
title: z.string(),
description: z.string(),
language: z.string(),
code: z.string(),
tags: z.array(z.string()),
username: z.string().optional().describe(
"Username to publish as (you can see list_usernames to see available usernames)"
),
},
async ({ title, description, language, code, tags = [], username }, _extra) => {
return publishCodeSnippet(title, description, language, code, tags, username);
}
);
}

139
logic/publish.ts Normal file
View File

@@ -0,0 +1,139 @@
import {
NDKEvent
} from "@nostr-dev-kit/ndk";
import type { NDKFilter } from "@nostr-dev-kit/ndk";
import { z } from "zod";
import { identifierToPubkeys, getSigner } from "../lib/nostr/utils.js";
import { ndk } from "../ndk.js";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { knownUsers } from "../users.js";
/**
* Publish a note to Nostr
* @param content The content of the note to publish
* @param username Username to publish as
* @param obey Array of pubkeys, npubs, or names to wait for replies from
* @returns Publication results
*/
export async function publishNote(
content: string,
username?: string,
obey?: string[]
): Promise<{ content: Array<{ type: "text", text: string }> }> {
const userToPublishAs = username ?? "main";
try {
// Create the event
const event = new NDKEvent(ndk, {
kind: 1,
content,
tags: [],
});
// Get the appropriate signer based on username
const signer = await getSigner(userToPublishAs);
// Sign the event with the selected signer
await event.sign(signer);
// Publish the already signed event
await event.publish();
const eventId = event.id;
// If obey parameter exists, wait for replies from the specified users
if (obey && obey.length > 0) {
// Convert all identifiers to pubkeys
const pubkeysToObey: string[] = [];
for (const identifier of obey) {
const pubkeys = identifierToPubkeys(identifier);
if (pubkeys.length > 0) {
pubkeysToObey.push(...pubkeys);
}
}
if (pubkeysToObey.length === 0) {
throw new Error("No valid users found to obey");
}
// Set up filter to listen for replies
const filter: NDKFilter = {
kinds: [1], // Normal notes
authors: pubkeysToObey,
"#e": [eventId], // References to our event
};
// Wait for a reply
return await new Promise((resolve, reject) => {
// Set a timeout of 5 minutes
const timeout = setTimeout(
() => {
sub.stop();
reject(new Error("Timed out waiting for reply"));
},
5 * 60 * 1000
);
// Subscribe to replies
const sub = ndk.subscribe(filter);
sub.on("event", (replyEvent) => {
clearTimeout(timeout);
sub.stop();
// Format the reply output
const npub = replyEvent.author.npub;
const name = knownUsers[replyEvent.pubkey]?.profile?.name;
const replyFrom = name ? `${name} (${npub})` : npub;
resolve({
content: [
{
type: "text",
text: `${replyFrom} says:\n\n${replyEvent.content}`,
},
],
});
});
});
}
return {
content: [
{
type: "text",
text: `Published to Nostr with ID: ${event.encode()}`,
},
],
};
} catch (error: unknown) {
const errorMessage =
error instanceof Error ? error.message : String(error);
throw new Error(`Failed to publish: ${errorMessage}`);
}
}
export function addPublishCommand(server: McpServer) {
// Add publish tool
server.tool(
"publish",
"Publish a tweet to Nostr",
{
content: z
.string()
.describe("The content of the tweet you want to publish"),
username: z
.string()
.optional()
.describe(
"Username to publish as (you can see list_usernames to see available usernames)"
),
obey: z
.array(z.string())
.optional()
.describe(
"Array of pubkeys, npubs, or names to wait for replies from"
),
},
async ({ content, username, obey }, _extra) => {
return await publishNote(content, username, obey);
}
);
}

View File

@@ -0,0 +1,21 @@
import type { Database } from "bun:sqlite";
export const up = async (db: Database) => {
db.run(`
CREATE TABLE IF NOT EXISTS wot (
follower TEXT NOT NULL,
followed TEXT NOT NULL,
PRIMARY KEY (follower, followed)
)
`);
// Create index for faster lookup by followed pubkey
db.run(`
CREATE INDEX IF NOT EXISTS idx_wot_followed
ON wot (followed)
`);
};
export const down = async (db: Database) => {
db.run("DROP TABLE IF EXISTS wot");
};

View File

@@ -0,0 +1,17 @@
import type { Database } from "bun:sqlite";
export const up = async (db: Database) => {
db.run(`
CREATE TABLE IF NOT EXISTS profiles (
pubkey TEXT PRIMARY KEY,
profile TEXT NOT NULL,
data TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`);
};
export const down = async (db: Database) => {
db.run("DROP TABLE IF EXISTS profiles");
};

57
ndk.ts Normal file
View File

@@ -0,0 +1,57 @@
import NDK, {
NDKPrivateKeySigner,
type NDKUser,
type NDKSigner,
} from "@nostr-dev-kit/ndk";
import { NDKNip46Signer } from "@nostr-dev-kit/ndk";
import { type ConfigData, writeConfig } from "./config";
import { updateFollowList } from "./update-follow-list";
const DEFAULT_RELAYS = [
"wss://relay.primal.net",
"wss://relay.damus.io",
"wss://nos.lol",
];
// Initialize NDK with signer
export const ndk = new NDK();
export async function initNDK(config: ConfigData) {
ndk.explicitRelayUrls = config.relays || DEFAULT_RELAYS;
await ndk.connect();
let signer: NDKSigner;
if (config.privateKey) {
signer = new NDKPrivateKeySigner(config.privateKey);
} else if (config.bunker) {
let localSigner: NDKPrivateKeySigner;
if (config.bunkerLocalKey) {
localSigner = new NDKPrivateKeySigner(config.bunkerLocalKey);
} else {
localSigner = NDKPrivateKeySigner.generate();
// save it to the config
config.bunkerLocalKey = localSigner.privateKey;
writeConfig(config);
}
signer = new NDKNip46Signer(ndk, config.bunker, localSigner);
await signer.blockUntilReady();
} else {
throw new Error("No private key or bunker provided");
}
ndk.signer = signer;
let mainUser: NDKUser;
if (config.wotFrom) {
const u = await ndk.getUserFromNip05(config.wotFrom);
if (u) mainUser = u;
}
mainUser ??= await signer.user();
setTimeout(() => updateFollowList(mainUser), 1000);
}

32
package.json Normal file
View File

@@ -0,0 +1,32 @@
{
"name": "mcp-code",
"version": "0.1.1",
"module": "index.ts",
"type": "module",
"bin": {
"mcp-code": "./index.ts"
},
"scripts": {
"build": "bun build --compile --outfile mcp-code index.ts ",
"lint": "biome lint .",
"format": "biome format . --write",
"check": "biome check --write .",
"setup": "bun run index.ts setup"
},
"devDependencies": {
"@biomejs/biome": "^1.9.4",
"@types/bun": "latest",
"@types/inquirer": "^9.0.7"
},
"peerDependencies": {
"typescript": "^5"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.7.0",
"@nostr-dev-kit/ndk": "^2.13.0-rc2",
"@nostr-dev-kit/ndk-wallet": "0.5.0",
"commander": "^13.1.0",
"inquirer": "^12.5.0",
"yaml": "^2.7.0"
}
}

108
test.ts Normal file
View File

@@ -0,0 +1,108 @@
import NDK, { NDKPrivateKeySigner, NDKKind } from "@nostr-dev-kit/ndk";
import { NDKCashuWallet, NDKWalletStatus } from "@nostr-dev-kit/ndk-wallet";
const ndk = new NDK({ explicitRelayUrls: ["wss://relay.primal.net"] });
ndk.signer = NDKPrivateKeySigner.generate();
/**
* Example function to set up an NDKCashuWallet
*/
async function setupCashuWallet(ndk: NDK, mints: string[]) {
// Create the Cashu wallet instance
const wallet = new NDKCashuWallet(ndk);
// Add mints to the wallet
for (const mint of mints) {
wallet.mints.push(mint);
}
// Generate or load a p2pk (Pay-to-Public-Key) token
// This is used for receiving payments with NIP-61 (nutzaps)
const p2pk = await wallet.getP2pk();
console.log(`Wallet p2pk: ${p2pk}`);
// Set up event listeners
wallet.on("ready", () => {
console.log("Cashu wallet is ready");
});
wallet.on("balance_updated", (balance) => {
console.log(`Wallet balance updated: ${balance?.amount} sats`);
// You might want to update your UI here
});
// Start the wallet - this will load the state of the wallet and begin monitoring for events
wallet.start();
// Assign to NDK instance for integration with other NDK features
ndk.wallet = wallet;
return wallet;
}
/**
* Get balance for a specific mint
*/
function getMintBalance(wallet: NDKCashuWallet, mintUrl: string) {
const balance = wallet.mintBalance(mintUrl);
console.log(`Balance for mint ${mintUrl}: ${balance} sats`);
return balance;
}
/**
* Check if the user already has a nutsack (NIP-60) wallet.
**/
async function findExistingWallet(
ndk: NDK
): Promise<NDKCashuWallet | undefined> {
const activeUser = ndk.activeUser;
if (!activeUser) throw "we need a user first, set a signer in ndk";
const event = await ndk.fetchEvent([
{ kinds: [NDKKind.CashuWallet], authors: [activeUser.pubkey] },
]);
// if we receive a CashuWallet event we load the wallet
if (event) return await NDKCashuWallet.from(event);
}
/**
* Example usage
*/
async function main() {
// we assume ndk is already connected and ready
// ...
let wallet: NDKCashuWallet | undefined;
wallet = await findExistingWallet(ndk);
// if we don't have a wallet, we create one
if (!wallet) {
// List of mints to use
const mints = ["https://8333.space:3338"];
// Setup the wallet
wallet = await setupCashuWallet(ndk, mints);
}
// Example: Check wallet balance
const totalBalance = wallet.balance?.amount || 0;
console.log(`Total wallet balance: ${totalBalance} sats`);
// Example: Need to fund wallet?
// See the Cashu Deposits snippet for funding your wallet with lightning
// Example: Get balance for specific mint
for (const mint of mints) {
getMintBalance(wallet, mint);
}
// Note: For monitoring nutzaps, see the Nutzap Monitor snippet
// Keep the connection open for monitoring
// In a real app, you'd use proper lifecycle management
}
setTimeout(main, 2500);

View File

@@ -0,0 +1,158 @@
import { describe, test, expect } from "bun:test";
import { parseMetadata, createFileWithMetadata } from "../logic/publish-code-snippet";
describe("Metadata Parser Tests", () => {
test("should parse valid metadata", () => {
const testContent = `---METADATA---
# Edit the metadata below. Keep the format exactly as shown (Title:, Description:, Language:, Tags:)
# Description needs to be at least 140 characters and Tags need at least 5 entries
# Don't remove the ---METADATA--- and ---CODE--- markers!
Title: Test Title
Description: This is a test description that is long enough to meet the required minimum length of 140 characters. It includes information about the code snippet and provides context for anyone who might want to use it.
Language: typescript
Tags: test, unit, parser, metadata, typescript
---CODE---
function testCode() {
return "This is test code";
}
`;
const result = parseMetadata(testContent);
expect(result.metadata.title).toBe("Test Title");
expect(result.metadata.description).toContain("This is a test description");
expect(result.metadata.language).toBe("typescript");
expect(result.metadata.tags).toEqual(["test", "unit", "parser", "metadata", "typescript"]);
expect(result.code).toContain("function testCode()");
});
test("should parse metadata with blank lines", () => {
const testContent = `---METADATA---
# Comments
Title: Test With Spaces
Description: This is a test description with blank lines that is long enough to meet the required minimum length of 140 characters. It includes information about the code snippet and provides context for anyone who might want to use it.
Language: javascript
Tags: test, blank, lines, metadata, parsing
---CODE---
console.log("hello world");
`;
const result = parseMetadata(testContent);
expect(result.metadata.title).toBe("Test With Spaces");
expect(result.metadata.description).toContain("This is a test description with blank lines");
expect(result.metadata.language).toBe("javascript");
expect(result.metadata.tags).toEqual(["test", "blank", "lines", "metadata", "parsing"]);
expect(result.code).toBe('console.log("hello world");\n');
});
test("should handle missing fields by returning empty strings", () => {
const testContent = `---METADATA---
Title: Only Title
---CODE---
const x = 1;
`;
const result = parseMetadata(testContent);
expect(result.metadata.title).toBe("Only Title");
expect(result.metadata.description).toBe("");
expect(result.metadata.language).toBe("");
expect(result.metadata.tags).toEqual([]);
expect(result.code).toBe("const x = 1;\n");
});
test("should throw error for invalid format", () => {
const testContent = `No metadata markers here
Just code without proper formatting
`;
expect(() => parseMetadata(testContent)).toThrow("Invalid file format");
});
test("createFileWithMetadata should create properly formatted file", () => {
const title = "Generated Title";
const description = "Generated description for testing purposes";
const language = "python";
const tags = ["generate", "test", "create", "metadata", "format"];
const code = "print('Hello World')";
const result = createFileWithMetadata(title, description, language, tags, code);
expect(result).toContain("Title: Generated Title");
expect(result).toContain("Description: Generated description");
expect(result).toContain("Language: python");
expect(result).toContain("Tags: generate, test, create, metadata, format");
expect(result).toContain("print('Hello World')");
expect(result).toContain("---METADATA---");
expect(result).toContain("---CODE---");
});
test("should correctly parse multiline content", () => {
const testContent = `---METADATA---
Title: Multiline Test
Description: This is a multiline description
that spans multiple lines
with different indentation
and should be preserved correctly
without including the following Language or Tags.
Language: python
Tags: multiline, test, parsing, description, metadata
---CODE---
def main():
print("Hello, world!")
if __name__ == "__main__":
main()
`;
const result = parseMetadata(testContent);
expect(result.metadata.title).toBe("Multiline Test");
expect(result.metadata.description).toBe(
"This is a multiline description\nthat spans multiple lines\nwith different indentation\n and should be preserved correctly\nwithout including the following Language or Tags."
);
expect(result.metadata.language).toBe("python");
expect(result.metadata.tags).toEqual(["multiline", "test", "parsing", "description", "metadata"]);
expect(result.code).toContain("def main():");
});
test("should handle complex content with comments and blank lines", () => {
const testContent = `---METADATA---
# Comments and instructions
# should be ignored
Title: Complex Example
# Another comment
Description: This is a complex
description with blank lines
and comments
# This should be part of description
with special characters: !@#$%^&*()
Language: javascript
# Comment between fields
Tags: complex, edge-case, parsing, metadata, test
---CODE---
function complexTest() {
console.log("Testing complex parsing");
// Comments in code
return {
status: "ok"
};
}
`;
const result = parseMetadata(testContent);
expect(result.metadata.title).toBe("Complex Example");
expect(result.metadata.description).toBe(
"This is a complex\ndescription with blank lines\n\nand comments\n# This should be part of description\nwith special characters: !@#$%^&*()"
);
expect(result.metadata.language).toBe("javascript");
expect(result.metadata.tags).toEqual(["complex", "edge-case", "parsing", "metadata", "test"]);
expect(result.code).toContain("function complexTest()");
});
});

28
tsconfig.json Normal file
View File

@@ -0,0 +1,28 @@
{
"compilerOptions": {
// Environment setup & latest features
"lib": ["esnext"],
"target": "ESNext",
"module": "ESNext",
"moduleDetection": "force",
"jsx": "react-jsx",
"allowJs": true,
// Bundler mode
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"noEmit": true,
// Best practices
"strict": true,
"skipLibCheck": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
// Some stricter flags (disabled by default)
"noUnusedLocals": false,
"noUnusedParameters": false,
"noPropertyAccessFromIndexSignature": false
}
}

0
types.ts Normal file
View File

51
update-follow-list.ts Normal file
View File

@@ -0,0 +1,51 @@
import {
type NDKEvent,
type NDKUser,
profileFromEvent,
} from "@nostr-dev-kit/ndk";
import { ndk } from "./ndk.js";
import { knownUsers, saveKnownUsers, saveUserProfile } from "./users.js";
import { addFollows } from "./wot.js";
export async function updateFollowList(user: NDKUser) {
const follows = await user.followSet();
const followsList = Array.from(follows.keys());
// Store follows in the database
addFollows(user.pubkey, followsList);
const unknownUsers = new Set<string>();
for (const follow of followsList) {
if (!knownUsers[follow]) {
unknownUsers.add(follow);
}
}
const profilesSub = ndk.subscribe([
{ kinds: [0, 3], authors: Array.from(unknownUsers) },
]);
profilesSub.on("event", (event: NDKEvent) => {
if (event.kind === 0) handleProfileEvent(event);
else if (event.kind === 3) handleFollowEvent(event);
});
profilesSub.on("eose", () => {
saveKnownUsers();
});
}
function handleProfileEvent(event: NDKEvent) {
const profile = profileFromEvent(event);
// Save directly to database instead of just updating the cache
saveUserProfile(event.pubkey, profile, event.content);
}
function handleFollowEvent(event: NDKEvent) {
const follows = event.tags
.filter((tag) => tag[0] === "p")
.map((tag) => tag[1])
.filter(Boolean) as string[];
addFollows(event.pubkey, follows);
}

101
users.ts Normal file
View File

@@ -0,0 +1,101 @@
import type { NDKUserProfile } from "@nostr-dev-kit/ndk";
import { db } from "./db.js";
import type { UserEntry } from "./lib/types/index.js";
import { log } from "./lib/utils/log.js";
// Cache for known users to avoid frequent DB queries
export const knownUsers: Record<string, UserEntry> = {};
/**
* Save a user profile to the database
* @param pubkey The user's public key
* @param profile The user's profile information
* @param data The raw content data
*/
export function saveUserProfile(
pubkey: string,
profile: NDKUserProfile,
data: string
): void {
// Update the cache
knownUsers[pubkey] = { profile, data };
// Save to database
db.run(
`INSERT INTO profiles (pubkey, profile, data)
VALUES (?, ?, ?)
ON CONFLICT(pubkey) DO UPDATE SET
profile = excluded.profile,
data = excluded.data,
updated_at = CURRENT_TIMESTAMP`,
[pubkey, JSON.stringify(profile), data]
);
}
/**
* Save all cached known users to the database
*/
export async function saveKnownUsers(): Promise<void> {
db.transaction(() => {
for (const [pubkey, user] of Object.entries(knownUsers)) {
saveUserProfile(pubkey, user.profile, user.data);
}
})();
log(`Saved ${Object.keys(knownUsers).length} users to database`);
}
/**
* Load known users from the database into the cache
*/
export async function loadKnownUsers(): Promise<void> {
const results = db
.query("SELECT pubkey, profile, data FROM profiles")
.all() as {
pubkey: string;
profile: string;
data: string;
}[];
for (const row of results) {
try {
const profile = JSON.parse(row.profile) as NDKUserProfile;
knownUsers[row.pubkey] = {
profile,
data: row.data,
};
} catch (err) {
console.error(`Error parsing profile for ${row.pubkey}:`, err);
}
}
log(`Loaded ${Object.keys(knownUsers).length} users from database`);
}
// Initialize the cache
if (!Object.keys(knownUsers).length) {
await loadKnownUsers();
}
/**
* Returns the pubkey of users that match the query
* @param query The search query
* @returns Array of matching pubkeys
*/
export function queryUser(query: string): string[] {
const lower = query.toLowerCase();
// Search in memory cache first
const cachedResults = Object.entries(knownUsers)
.filter(([_, u]) => u.data.toLowerCase().includes(lower))
.map(([pubkey, _]) => pubkey);
if (cachedResults.length) {
return cachedResults;
}
// Fall back to database search if not found in cache
const dbResults = db
.query("SELECT pubkey FROM profiles WHERE data LIKE ?")
.all(`%${lower}%`) as { pubkey: string }[];
return dbResults.map((row) => row.pubkey);
}

2
utils/log.ts Normal file
View File

@@ -0,0 +1,2 @@
// Redirect to the new log utility
export { log } from "../lib/utils/log.js";

166
wizard.ts Normal file
View File

@@ -0,0 +1,166 @@
import inquirer from "inquirer";
import NDK from "@nostr-dev-kit/ndk";
import { type ConfigData, writeConfig } from "./config.js";
// Available MCP commands
const MCP_COMMANDS = [
{ name: "Publish Notes", value: "publish" },
{ name: "Publish Code Snippets", value: "publish-snippet" },
{ name: "Create Public Key", value: "create-pubkey" },
{ name: "Find User", value: "find-user" },
{ name: "Find Code Snippets", value: "find-snippets" },
{ name: "List Usernames", value: "list-usernames" },
];
/**
* Run the configuration wizard to guide the user through first-time setup
* @param config Current configuration object
* @returns Updated configuration object
*/
export async function runConfigWizard(config: ConfigData): Promise<ConfigData> {
console.log("\n🔧 Welcome to MCP-Nostr Configuration Wizard 🔧\n");
console.log("Let's set up your configuration for first use.\n");
// Create a temporary NDK instance to validate NIP-05
const tempNdk = new NDK({ explicitRelayUrls: ["wss://relay.damus.io"] });
await tempNdk.connect();
// Step 1: Ask for Web-of-trust entry point
const { wotFrom } = await inquirer.prompt([
{
type: "input",
name: "wotFrom",
message: "Enter Web-of-trust entry point (NIP-05):",
default: "pablo@f7z.io",
},
]);
config.wotFrom = wotFrom;
// Validate NIP-05 and show npub
console.log(`\nValidating NIP-05: ${wotFrom}...`);
try {
const user = await tempNdk.getUserFromNip05(wotFrom);
if (user) {
console.log(`✅ Valid NIP-05! Found npub: ${user.npub}`);
} else {
console.log(`⚠️ Could not verify NIP-05: ${wotFrom}`);
}
} catch (error: unknown) {
const errorMessage = error instanceof Error ? error.message : String(error);
console.error(`❌ Error validating NIP-05: ${errorMessage}`);
}
console.log("🔑 Authentication Methods");
console.log("This is what will be used to publish code snippets and notes when you choose to publish without a dedicated profile.");
// Step 2: Ask for authentication method
const { authMethod } = await inquirer.prompt([
{
type: "list",
name: "authMethod",
message: "Choose authentication method:",
choices: [
{ name: "Private Key (nsec)", value: "nsec" },
{ name: "Bunker Connection (bunker://)", value: "bunker" },
],
},
]);
// Step 3: Ask for authentication value based on chosen method
if (authMethod === "nsec") {
const { privateKey } = await inquirer.prompt([
{
type: "password",
name: "privateKey",
message: "Enter your private key (nsec):",
mask: "*",
},
]);
config.privateKey = privateKey;
} else {
const { bunker } = await inquirer.prompt([
{
type: "input",
name: "bunker",
message: "Enter bunker connection string (bunker://):",
validate: (input) => {
return input.startsWith("bunker://")
? true
: "Bunker connection string must start with 'bunker://'";
},
},
]);
config.bunker = bunker;
}
// Step 4: Ask for relays
const { useDefaultRelays } = await inquirer.prompt([
{
type: "confirm",
name: "useDefaultRelays",
message: "Use default relays?",
default: true,
},
]);
if (useDefaultRelays) {
// Default relays are already set in ndk.ts, no need to set here
} else {
const { relays } = await inquirer.prompt([
{
type: "input",
name: "relays",
message: "Enter comma-separated relay URLs (wss://...):",
default: "wss://relay.damus.io,wss://relay.primal.net,wss://nos.lol",
validate: (input) => {
const relayList = input.split(",").map((r: string) => r.trim());
const allValid = relayList.every((r: string) => r.startsWith("wss://"));
return allValid ? true : "All relay URLs should start with 'wss://'";
},
filter: (input) => {
return input.split(",").map((r: string) => r.trim());
}
},
]);
config.relays = relays;
}
// Step 5: Ask for MCP commands to enable
const { enableAllCommands } = await inquirer.prompt([
{
type: "confirm",
name: "enableAllCommands",
message: "Enable all MCP commands?",
default: true,
},
]);
if (enableAllCommands) {
// All commands are enabled by default by setting an empty array or undefined
config.mcpCommands = undefined;
} else {
const { selectedCommands } = await inquirer.prompt([
{
type: "checkbox",
name: "selectedCommands",
message: "Select which MCP commands to enable:",
choices: MCP_COMMANDS,
default: MCP_COMMANDS.map(cmd => cmd.value),
validate: (input) => {
return input.length > 0
? true
: "You must select at least one command";
},
},
]);
config.mcpCommands = selectedCommands;
}
// Save configuration
writeConfig(config);
console.log("\n✅ Configuration saved successfully!\n");
return config;
}

74
wot.ts Normal file
View File

@@ -0,0 +1,74 @@
import { db } from "./db.js";
/**
* Add follows for a given follower
* @param follower The pubkey of the follower
* @param followed Array of pubkeys being followed
*/
export function addFollows(follower: string, followed: string[]): void {
const stmt = db.prepare(
"INSERT OR IGNORE INTO wot (follower, followed) VALUES (?, ?)"
);
// Use a transaction for better performance
db.transaction(() => {
for (const followedPubkey of followed) {
stmt.run(follower, followedPubkey);
}
})();
stmt.finalize();
}
/**
* Get the count of followers for a given pubkey
* @param pubkey The pubkey to get follower count for
* @returns The number of distinct followers
*/
export function getFollowerCount(pubkey: string): number {
const result = db
.query(
"SELECT COUNT(DISTINCT follower) as count FROM wot WHERE followed = ?"
)
.get(pubkey) as { count: number };
return result.count;
}
/**
* Get all followers for a given pubkey
* @param pubkey The pubkey to get followers for
* @returns Array of follower pubkeys
*/
export function getFollowers(pubkey: string): string[] {
const results = db
.query("SELECT DISTINCT follower FROM wot WHERE followed = ?")
.all(pubkey) as { follower: string }[];
return results.map((row) => row.follower);
}
/**
* Get all pubkeys followed by a given pubkey
* @param pubkey The pubkey to get follows for
* @returns Array of followed pubkeys
*/
export function getFollowing(pubkey: string): string[] {
const results = db
.query("SELECT DISTINCT followed FROM wot WHERE follower = ?")
.all(pubkey) as { followed: string }[];
return results.map((row) => row.followed);
}
/**
* Remove a follow relationship
* @param follower The follower pubkey
* @param followed The followed pubkey
*/
export function removeFollow(follower: string, followed: string): void {
db.run("DELETE FROM wot WHERE follower = ? AND followed = ?", [
follower,
followed,
]);
}