mirror of
https://github.com/aljazceru/mcp-code.git
synced 2025-12-18 13:15:01 +01:00
feat: Add zap command for sending Bitcoin Lightning tips
- Implemented the `zap` command in the CLI to allow users to send sats to a user, event, or snippet using a NIP-60 wallet. - Created a new `zap.ts` file to handle the command logic and integrated it into the MCP server. - Added wallet balance command to check the balance of a user's wallet. - Enhanced the MCP server to register the new zap command and wallet balance command. - Introduced caching for wallets to optimize performance and reduce redundant network requests. - Updated database schema to include snippets table for storing code snippets. - Improved logging functionality for better debugging and tracking of operations. - Added functionality to save snippets to the database upon retrieval. - Updated project overview documentation to reflect new features and structure. - Refactored existing commands and logic for better modularity and maintainability.
This commit is contained in:
108
lib/cache/wallets.ts
vendored
Normal file
108
lib/cache/wallets.ts
vendored
Normal file
@@ -0,0 +1,108 @@
|
||||
import { NDKCashuWallet, NDKNutzapMonitor } from "@nostr-dev-kit/ndk-wallet";
|
||||
import { NDKCashuMintList, NDKPrivateKeySigner, type NDKSigner } from "@nostr-dev-kit/ndk";
|
||||
import { ndk } from "../../ndk.js";
|
||||
import { log } from "../utils/log.js";
|
||||
|
||||
/**
|
||||
* In-memory cache to store NDKCashuWallets by pubkey for efficient retrieval
|
||||
* Used by the zap command to avoid repeatedly fetching wallet events
|
||||
*/
|
||||
export const walletsCache: Record<string, NDKCashuWallet> = {};
|
||||
|
||||
/**
|
||||
* Get a wallet for a pubkey, retrieving from cache if available,
|
||||
* otherwise loading from Nostr
|
||||
* @param pubkey The public key to get the wallet for
|
||||
* @returns The wallet (from cache or newly loaded)
|
||||
*/
|
||||
export async function getWallet(pubkey: string, signer: NDKSigner): Promise<NDKCashuWallet | undefined> {
|
||||
// Return from cache if available
|
||||
if (walletsCache[pubkey]) {
|
||||
console.log(`Returning cached wallet for ${pubkey}`);
|
||||
return walletsCache[pubkey];
|
||||
}
|
||||
|
||||
ndk.signer = signer;
|
||||
|
||||
// Not in cache, fetch from Nostr
|
||||
try {
|
||||
let wallet: NDKCashuWallet | undefined;
|
||||
const user = ndk.getUser({pubkey});
|
||||
const event = await ndk.fetchEvent({ kinds: [17375], authors: [pubkey] });
|
||||
|
||||
// Use the existing wallet
|
||||
if (event) {
|
||||
console.log(`Found wallet event for ${pubkey}: ${event.id}`, event.inspect);
|
||||
wallet = await NDKCashuWallet.from(event);
|
||||
} else {
|
||||
console.log('No wallet event found for', pubkey);
|
||||
}
|
||||
|
||||
if (!wallet) {
|
||||
wallet = new NDKCashuWallet(ndk);
|
||||
wallet.mints = ["https://mint.coinos.io"];
|
||||
|
||||
// Generate a P2PK address for receiving nutzaps
|
||||
await wallet.getP2pk();
|
||||
log(`Generated P2PK address for ${pubkey}: ${wallet.p2pk}`);
|
||||
|
||||
// Publish the wallet info event (kind 17375)
|
||||
await wallet.publish();
|
||||
log(`Published wallet info event for ${pubkey}`);
|
||||
|
||||
// Set up the mint list for nutzap reception (kind 10019)
|
||||
const mintList = new NDKCashuMintList(ndk);
|
||||
mintList.mints = wallet.mints;
|
||||
mintList.relays = ['wss://relay.pri']
|
||||
mintList.p2pk = wallet.p2pk;
|
||||
|
||||
// Publish the mint list
|
||||
await mintList.publish();
|
||||
log(`Published mint list for ${pubkey} with mints: ${mintList.mints.join(', ')}`);
|
||||
}
|
||||
|
||||
|
||||
// Start wallet for monitoring balance and nutzaps
|
||||
console.log(`Starting wallet for ${pubkey}`);
|
||||
await wallet.start();
|
||||
console.log(`Wallet started for ${pubkey}: ${wallet.balance}`);
|
||||
log(`Started wallet for ${pubkey}: ${wallet.balance}`);
|
||||
|
||||
try {
|
||||
const nutzapMonitor = new NDKNutzapMonitor(ndk, user, {});
|
||||
nutzapMonitor.wallet = wallet;
|
||||
nutzapMonitor.on("seen", () => {
|
||||
log('seen nutzap');
|
||||
});
|
||||
nutzapMonitor.on("redeemed", (events) => {
|
||||
log(`Nutzap redeemed for ${pubkey}: ${events.reduce((acc, event) => acc + event.amount, 0)} sats`);
|
||||
});
|
||||
nutzapMonitor.start({});
|
||||
log(`Started nutzap monitor for ${pubkey}`);
|
||||
|
||||
// Set up balance update listener
|
||||
wallet.on('balance_updated', (newBalance) => {
|
||||
log(`Balance updated for ${pubkey}: ${newBalance?.amount || 0} sats`);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(`Error starting nutzap monitor for ${pubkey}:`, error);
|
||||
}
|
||||
|
||||
walletsCache[pubkey] = wallet;
|
||||
|
||||
// No wallet found
|
||||
return wallet;
|
||||
} catch (error) {
|
||||
console.error(`Error fetching wallet for ${pubkey}:`, error);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the wallet cache for testing or memory management
|
||||
*/
|
||||
export function clearWalletsCache(): void {
|
||||
for (const key of Object.keys(walletsCache)) {
|
||||
delete walletsCache[key];
|
||||
}
|
||||
}
|
||||
@@ -70,10 +70,13 @@ export function formatSnippets(snippets: CodeSnippet[]): string {
|
||||
.map((snippet) => {
|
||||
const author = knownUsers[snippet.pubkey];
|
||||
const keys: Record<string, string> = {
|
||||
ID: snippet.id,
|
||||
Title: snippet.title,
|
||||
Description: snippet.description,
|
||||
Language: snippet.language,
|
||||
Tags: snippet.tags.join(", "),
|
||||
Code: snippet.code,
|
||||
Date: new Date(snippet.createdAt * 1000).toISOString(),
|
||||
};
|
||||
if (author?.profile?.name) keys.Author = author.profile.name;
|
||||
return Object.entries(keys)
|
||||
@@ -95,7 +98,7 @@ export function formatPartialMatches(snippets: CodeSnippet[]): string {
|
||||
"\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(", ")}`;
|
||||
return ` * ${snippet.title}:\n Tags: ${snippet.tags.join(", ")} (ID: ${snippet.id})`;
|
||||
})
|
||||
.join("\n");
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { NDKEvent, NDKFilter } from "@nostr-dev-kit/ndk";
|
||||
import { db } from "../../db.js";
|
||||
import { ndk } from "../../ndk.js";
|
||||
import type { CodeSnippet, FindSnippetsParams } from "../types/index.js";
|
||||
import { log } from "../utils/log.js";
|
||||
@@ -58,15 +59,22 @@ export async function getSnippets(params: FindSnippetsParams = {}): Promise<{
|
||||
|
||||
let maxMatchCount = 0;
|
||||
|
||||
/**
|
||||
* Function to calculate the number of tags in an event that match the search tags.
|
||||
* Used for ranking snippets based on tag relevance.
|
||||
* If no tags are provided in params, all snippets are considered equally relevant (match count = 1).
|
||||
*/
|
||||
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")))
|
||||
.filter((tag) => tag[0] === "t") // Filter for 't' tags
|
||||
.map((tag) => tag[1]) // Get the tag value
|
||||
.filter((t): t is string => t !== undefined); // Ensure tag value exists and narrow type
|
||||
|
||||
// Count how many of the searched tags are present in the event's tags
|
||||
return params.tags.filter((searchTag) =>
|
||||
aTags.some((eventTag) => eventTag.match(new RegExp(searchTag, "i"))) // Case-insensitive tag matching
|
||||
).length;
|
||||
}
|
||||
|
||||
@@ -90,6 +98,39 @@ export async function getSnippets(params: FindSnippetsParams = {}): Promise<{
|
||||
const snippets = selectedEvents.map(toSnippet);
|
||||
const otherSnippets = notSelectedEvents.map(toSnippet);
|
||||
|
||||
|
||||
// --- BEGIN DATABASE INSERTION ---
|
||||
const allSnippets = [...snippets, ...otherSnippets];
|
||||
if (allSnippets.length > 0) {
|
||||
log(`Saving ${allSnippets.length} snippets to the database...`);
|
||||
const insertStmt = db.prepare(`
|
||||
INSERT OR REPLACE INTO snippets (id, title, description, code, language, pubkey, createdAt, tags)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
try {
|
||||
db.transaction(() => {
|
||||
for (const snippet of allSnippets) {
|
||||
insertStmt.run(
|
||||
snippet.id,
|
||||
snippet.title,
|
||||
snippet.description,
|
||||
snippet.code,
|
||||
snippet.language,
|
||||
snippet.pubkey,
|
||||
snippet.createdAt,
|
||||
JSON.stringify(snippet.tags) // Store tags as JSON string
|
||||
);
|
||||
}
|
||||
})(); // Immediately invoke the transaction
|
||||
log("Snippets saved successfully.");
|
||||
} catch (error) {
|
||||
console.error("Failed to save snippets to database:", error);
|
||||
// Decide if we should throw or just log the error
|
||||
// For now, just log it and continue returning fetched snippets
|
||||
}
|
||||
}
|
||||
// --- END DATABASE INSERTION ---
|
||||
return { snippets, otherSnippets };
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,10 @@ import { toPubkeys, toSnippet } from "../converters/index.js";
|
||||
|
||||
export const SNIPPET_KIND = 1337;
|
||||
|
||||
export function getUsernameFromPubkey(pubkey: string): string | null {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the appropriate signer based on the username
|
||||
* @param username Username to get signer for (or "main" for default)
|
||||
|
||||
@@ -1,5 +1,30 @@
|
||||
import * as fs from "node:fs";
|
||||
import { promises as fsPromises } from "node:fs";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
|
||||
const timeZero = Date.now();
|
||||
|
||||
/**
|
||||
* Simple logging utility
|
||||
* @param message Message to log
|
||||
* @param logFilePath Optional custom log file path
|
||||
*/
|
||||
export function log(_message: string): void {}
|
||||
export function log(
|
||||
message: string,
|
||||
logFilePath: string = path.join(os.homedir(), ".mcp-code23.log")
|
||||
): void {
|
||||
// Ensure the directory exists
|
||||
const logDir = path.dirname(logFilePath);
|
||||
fs.mkdirSync(logDir, { recursive: true });
|
||||
|
||||
const now = new Date();
|
||||
const timestamp = new Date().toISOString();
|
||||
const relativeTime = now.getTime() - timeZero;
|
||||
const logMessage = `[${relativeTime}ms] ${timestamp} - ${message}\n`;
|
||||
fs.appendFile(logFilePath, logMessage, (err) => {
|
||||
if (err) {
|
||||
console.error("Error writing to log file:", err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user