This commit is contained in:
pablof7z
2025-04-09 11:45:30 +01:00
parent 630ed0cbd0
commit b258f49201
8 changed files with 134 additions and 48 deletions

View File

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

View File

@@ -3,6 +3,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
import type { Command } from "commander"; import type { Command } from "commander";
import { readConfig } from "../config.js"; import { readConfig } from "../config.js";
import { addCreatePubkeyCommand } from "../logic/create-pubkey.js"; import { addCreatePubkeyCommand } from "../logic/create-pubkey.js";
import { addDepositCommand } from "../logic/deposit.js";
import { addFetchSnippetByIdCommand } from "../logic/fetch_snippet_by_id.js"; import { addFetchSnippetByIdCommand } from "../logic/fetch_snippet_by_id.js";
import { addFindSnippetsCommand } from "../logic/find_snippets.js"; import { addFindSnippetsCommand } from "../logic/find_snippets.js";
import { addFindUserCommand } from "../logic/find_user.js"; import { addFindUserCommand } from "../logic/find_user.js";
@@ -29,6 +30,7 @@ const commandMap: Record<string, CommandFunction> = {
"fetch-snippet-by-id": addFetchSnippetByIdCommand, "fetch-snippet-by-id": addFetchSnippetByIdCommand,
zap: addZapCommand, zap: addZapCommand,
"wallet-balance": addWalletBalanceCommand, "wallet-balance": addWalletBalanceCommand,
deposit: addDepositCommand,
}; };
// Global server instance // Global server instance
@@ -87,4 +89,5 @@ export function registerMcpCommands(server: McpServer) {
addZapCommand(server); addZapCommand(server);
addWalletBalanceCommand(server); addWalletBalanceCommand(server);
} }
addDepositCommand(server);
} }

View File

@@ -2,9 +2,12 @@ import { initConfig, readConfig, writeConfig } from "./config.js";
import { initNDK, ndk } from "./ndk.js"; import { initNDK, ndk } from "./ndk.js";
import "./db.js"; import "./db.js";
import { runCli } from "./commands/index.js"; import { runCli } from "./commands/index.js";
import { applyMigrations } from "./db.js";
import { log } from "./lib/utils/log.js"; import { log } from "./lib/utils/log.js";
import { runConfigWizard } from "./wizard"; import { runConfigWizard } from "./wizard";
await applyMigrations();
log("starting up...: args: " + process.argv.join(" ")); log("starting up...: args: " + process.argv.join(" "));
// Load config and ensure defaults // Load config and ensure defaults

65
lib/cache/wallets.ts vendored
View File

@@ -1,5 +1,9 @@
import {
NDKCashuMintList,
NDKPrivateKeySigner,
type NDKSigner,
} from "@nostr-dev-kit/ndk";
import { NDKCashuWallet, NDKNutzapMonitor } from "@nostr-dev-kit/ndk-wallet"; 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 { ndk } from "../../ndk.js";
import { log } from "../utils/log.js"; import { log } from "../utils/log.js";
@@ -15,7 +19,12 @@ export const walletsCache: Record<string, NDKCashuWallet> = {};
* @param pubkey The public key to get the wallet for * @param pubkey The public key to get the wallet for
* @returns The wallet (from cache or newly loaded) * @returns The wallet (from cache or newly loaded)
*/ */
export async function getWallet(pubkey: string, signer: NDKSigner): Promise<NDKCashuWallet | undefined> { export async function getWallet(
pubkey: string,
signer: NDKSigner
): Promise<NDKCashuWallet | undefined> {
let newWallet = false;
// Return from cache if available // Return from cache if available
if (walletsCache[pubkey]) { if (walletsCache[pubkey]) {
console.log(`Returning cached wallet for ${pubkey}`); console.log(`Returning cached wallet for ${pubkey}`);
@@ -28,17 +37,24 @@ export async function getWallet(pubkey: string, signer: NDKSigner): Promise<NDKC
try { try {
let wallet: NDKCashuWallet | undefined; let wallet: NDKCashuWallet | undefined;
const user = ndk.getUser({ pubkey }); const user = ndk.getUser({ pubkey });
const event = await ndk.fetchEvent({ kinds: [17375], authors: [pubkey] }); const event = await ndk.fetchEvent({
kinds: [17375],
authors: [pubkey],
});
// Use the existing wallet // Use the existing wallet
if (event) { if (event) {
console.log(`Found wallet event for ${pubkey}: ${event.id}`, event.inspect); console.log(
`Found wallet event for ${pubkey}: ${event.id}`,
event.inspect
);
wallet = await NDKCashuWallet.from(event); wallet = await NDKCashuWallet.from(event);
} else { } else {
console.log('No wallet event found for', pubkey); console.log("No wallet event found for", pubkey);
} }
if (!wallet) { if (!wallet) {
newWallet = true;
wallet = new NDKCashuWallet(ndk); wallet = new NDKCashuWallet(ndk);
wallet.mints = ["https://mint.coinos.io"]; wallet.mints = ["https://mint.coinos.io"];
@@ -53,15 +69,16 @@ export async function getWallet(pubkey: string, signer: NDKSigner): Promise<NDKC
// Set up the mint list for nutzap reception (kind 10019) // Set up the mint list for nutzap reception (kind 10019)
const mintList = new NDKCashuMintList(ndk); const mintList = new NDKCashuMintList(ndk);
mintList.mints = wallet.mints; mintList.mints = wallet.mints;
mintList.relays = ['wss://relay.pri'] mintList.relays = ["wss://relay.primal.net"];
mintList.p2pk = wallet.p2pk; mintList.p2pk = wallet.p2pk;
// Publish the mint list // Publish the mint list
await mintList.publish(); await mintList.publish();
log(`Published mint list for ${pubkey} with mints: ${mintList.mints.join(', ')}`); log(
`Published mint list for ${pubkey} with mints: ${mintList.mints.join(", ")}`
);
} }
// Start wallet for monitoring balance and nutzaps // Start wallet for monitoring balance and nutzaps
console.log(`Starting wallet for ${pubkey}`); console.log(`Starting wallet for ${pubkey}`);
await wallet.start(); await wallet.start();
@@ -72,25 +89,45 @@ export async function getWallet(pubkey: string, signer: NDKSigner): Promise<NDKC
const nutzapMonitor = new NDKNutzapMonitor(ndk, user, {}); const nutzapMonitor = new NDKNutzapMonitor(ndk, user, {});
nutzapMonitor.wallet = wallet; nutzapMonitor.wallet = wallet;
nutzapMonitor.on("seen", () => { nutzapMonitor.on("seen", () => {
log('seen nutzap'); log("seen nutzap");
}); });
nutzapMonitor.on("redeemed", (events) => { nutzapMonitor.on("redeemed", (events) => {
log(`Nutzap redeemed for ${pubkey}: ${events.reduce((acc, event) => acc + event.amount, 0)} sats`); log(
`Nutzap redeemed for ${pubkey}: ${events.reduce((acc, event) => acc + event.amount, 0)} sats`
);
}); });
nutzapMonitor.start({}); nutzapMonitor.start({});
log(`Started nutzap monitor for ${pubkey}`); log(`Started nutzap monitor for ${pubkey}`);
// Set up balance update listener // Set up balance update listener
wallet.on('balance_updated', (newBalance) => { wallet.on("balance_updated", (newBalance) => {
log(`Balance updated for ${pubkey}: ${newBalance?.amount || 0} sats`); log(
`Balance updated for ${pubkey}: ${newBalance?.amount || 0} sats`
);
}); });
} catch (error) { } catch (error) {
console.error(`Error starting nutzap monitor for ${pubkey}:`, error); console.error(
`Error starting nutzap monitor for ${pubkey}:`,
error
);
} }
walletsCache[pubkey] = wallet; walletsCache[pubkey] = wallet;
// No wallet found if (!newWallet) {
// Total hack, but we have a race condition getting the balance of the wallet,
// if we're starting the wallet with a get_balance command we'll always return zero
// in the first call, so let's give it some time to catch up. wallet.start() should
// have returned once it knows it has all the proofs
return new Promise((resolve) => {
setTimeout(() => {
resolve(wallet);
}, 1000);
wallet.on("balance_updated", () => resolve(wallet));
});
}
// NKo wallet found
return wallet; return wallet;
} catch (error) { } catch (error) {
console.error(`Error fetching wallet for ${pubkey}:`, error); console.error(`Error fetching wallet for ${pubkey}:`, error);

View File

@@ -1,10 +1,14 @@
import type { NDKEvent, NDKFilter } from "@nostr-dev-kit/ndk"; import type { NDKEvent, NDKFilter } from "@nostr-dev-kit/ndk";
import { db } from "../../db.js"; import { db } from "../../db.js";
import { ndk } from "../../ndk.js"; import { ndk } from "../../ndk.js";
import {
formatPartialMatches,
formatSnippets,
toSnippet,
} from "../converters/index.js";
import type { CodeSnippet, FindSnippetsParams } from "../types/index.js"; import type { CodeSnippet, FindSnippetsParams } from "../types/index.js";
import { log } from "../utils/log.js"; import { log } from "../utils/log.js";
import { SNIPPET_KIND, identifierToPubkeys } from "./utils.js"; import { SNIPPET_KIND, identifierToPubkeys } from "./utils.js";
import { formatPartialMatches, formatSnippets, toSnippet } from "../converters/index.js";
/** /**
* Get code snippets from Nostr events of kind 1337 * Get code snippets from Nostr events of kind 1337
@@ -54,6 +58,19 @@ export async function getSnippets(params: FindSnippetsParams = {}): Promise<{
log(`Fetching snippets with filter: ${JSON.stringify(filter, null, 2)}`); log(`Fetching snippets with filter: ${JSON.stringify(filter, null, 2)}`);
// ndk.subscribe(
// [filter],
// { closeOnEose: true },
// {
// onEvent: (event) => {
// log(`Received event: ${event.id}`);
// },
// onEose: () => {
// log("EOSE received.");
// },
// }
// );
// Fetch events // Fetch events
const events = await ndk.fetchEvents(filter); const events = await ndk.fetchEvents(filter);
@@ -73,8 +90,11 @@ export async function getSnippets(params: FindSnippetsParams = {}): Promise<{
.filter((t): t is string => t !== undefined); // Ensure tag value exists and narrow type .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 // Count how many of the searched tags are present in the event's tags
return params.tags.filter((searchTag) => return params.tags.filter(
aTags.some((eventTag) => eventTag.match(new RegExp(searchTag, "i"))) // Case-insensitive tag matching (searchTag) =>
aTags.some((eventTag) =>
eventTag.match(new RegExp(searchTag, "i"))
) // Case-insensitive tag matching
).length; ).length;
} }
@@ -98,7 +118,6 @@ export async function getSnippets(params: FindSnippetsParams = {}): Promise<{
const snippets = selectedEvents.map(toSnippet); const snippets = selectedEvents.map(toSnippet);
const otherSnippets = notSelectedEvents.map(toSnippet); const otherSnippets = notSelectedEvents.map(toSnippet);
// --- BEGIN DATABASE INSERTION --- // --- BEGIN DATABASE INSERTION ---
const allSnippets = [...snippets, ...otherSnippets]; const allSnippets = [...snippets, ...otherSnippets];
if (allSnippets.length > 0) { if (allSnippets.length > 0) {

View File

@@ -1,8 +1,8 @@
import * as fs from "node:fs"; import * as fs from "node:fs";
import { promises as fsPromises } from "node:fs";
import * as os from "node:os"; import * as os from "node:os";
import * as path from "node:path"; import * as path from "node:path";
const id = Math.floor(Math.random() * 1000000);
const timeZero = Date.now(); const timeZero = Date.now();
/** /**
@@ -21,7 +21,7 @@ export function log(
const now = new Date(); const now = new Date();
const timestamp = new Date().toISOString(); const timestamp = new Date().toISOString();
const relativeTime = now.getTime() - timeZero; const relativeTime = now.getTime() - timeZero;
const logMessage = `[${relativeTime}ms] ${timestamp} - ${message}\n`; const logMessage = `[${id}] [${relativeTime}ms] ${timestamp} - ${message}\n`;
fs.appendFile(logFilePath, logMessage, (err) => { fs.appendFile(logFilePath, logMessage, (err) => {
if (err) { if (err) {
console.error("Error writing to log file:", err); console.error("Error writing to log file:", err);

11
ndk.ts
View File

@@ -2,10 +2,12 @@ import NDK, {
NDKPrivateKeySigner, NDKPrivateKeySigner,
type NDKUser, type NDKUser,
type NDKSigner, type NDKSigner,
type NDKRelay,
} from "@nostr-dev-kit/ndk"; } from "@nostr-dev-kit/ndk";
import { NDKNip46Signer } from "@nostr-dev-kit/ndk"; import { NDKNip46Signer } from "@nostr-dev-kit/ndk";
import { type ConfigData, writeConfig } from "./config"; import { type ConfigData, writeConfig } from "./config";
import { updateFollowList } from "./update-follow-list"; import { updateFollowList } from "./update-follow-list";
import { log } from "./utils/log";
const DEFAULT_RELAYS = [ const DEFAULT_RELAYS = [
"wss://relay.primal.net", "wss://relay.primal.net",
@@ -18,6 +20,13 @@ export const ndk = new NDK();
export async function initNDK(config: ConfigData) { export async function initNDK(config: ConfigData) {
ndk.explicitRelayUrls = config.relays || DEFAULT_RELAYS; ndk.explicitRelayUrls = config.relays || DEFAULT_RELAYS;
ndk.pool.on("relay:connect", (r: NDKRelay) => log(`Connected to ${r.url}`));
ndk.pool.on("relay:disconnect", (r: NDKRelay) =>
log(`Disconnected from ${r.url}`)
);
ndk.pool.on("relay:connecting", (r: NDKRelay) =>
log(`Connecting to ${r.url}`)
);
await ndk.connect(); await ndk.connect();
let signer: NDKSigner; let signer: NDKSigner;
@@ -53,5 +62,5 @@ export async function initNDK(config: ConfigData) {
mainUser ??= await signer.user(); mainUser ??= await signer.user();
setTimeout(() => updateFollowList(mainUser), 1000); // setTimeout(() => updateFollowList(mainUser), 1000);
} }

View File

@@ -23,7 +23,7 @@
}, },
"dependencies": { "dependencies": {
"@modelcontextprotocol/sdk": "^1.7.0", "@modelcontextprotocol/sdk": "^1.7.0",
"@nostr-dev-kit/ndk": "2.13.1-rc2", "@nostr-dev-kit/ndk": "2.13.1-rc7",
"@nostr-dev-kit/ndk-wallet": "0.5.3-1", "@nostr-dev-kit/ndk-wallet": "0.5.3-1",
"commander": "^13.1.0", "commander": "^13.1.0",
"inquirer": "^12.5.0", "inquirer": "^12.5.0",