diff --git a/.gitignore b/.gitignore index a14702c..f114965 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,4 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json # Finder (MacOS) folder config .DS_Store +mcp-code diff --git a/commands/find-user.ts b/commands/find-user.ts index 2e992e5..2f6ff9f 100644 --- a/commands/find-user.ts +++ b/commands/find-user.ts @@ -1,43 +1,32 @@ -import { Command } from 'commander'; -import { ndk } from '../ndk.js'; -import { knownUsers } from '../users.js'; -import { identifierToPubkeys } from '../lib/nostr/utils.js'; +import { Command } from "commander"; +import { toPubkeys, formatUser } from "../lib/converters/index.js"; -export function registerFindUserCommand(program: Command): void { - program - .command('find-user') - .description('Find a user by identifier') - .argument('', '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); +// Create a command for finding a user +const findUserCommand = new Command("find-user") + .description("Find a user by name, npub, or other profile information") + .argument("", "The search query to find a user") + .action(async (query: string) => { + try { + // Find matching pubkeys + const pubkeys = toPubkeys(query); + + if (pubkeys.length === 0) { + console.log(`No users found matching query: ${query}`); + return; } - }); -} -// Helper function to format user profiles -function formatUser(pubkey: string) { - const profile = knownUsers[pubkey]?.profile; - const user = ndk.getUser({ pubkey }); - const keys: Record = { - Npub: user.npub, - }; + // Format and display each matching user + console.log(`Found ${pubkeys.length} matching users:`); + for (let i = 0; i < pubkeys.length; i++) { + if (i > 0) console.log("\n---\n"); + const pubkey = pubkeys[i]; + if (pubkey) { + console.log(formatUser(pubkey)); + } + } + } catch (error) { + console.error("Error:", error); + } + }); - 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"); -} \ No newline at end of file +export default findUserCommand; \ No newline at end of file diff --git a/commands/index.ts b/commands/index.ts index f30ca92..901ff27 100644 --- a/commands/index.ts +++ b/commands/index.ts @@ -1,5 +1,4 @@ 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'; @@ -17,7 +16,6 @@ program // Register all commands registerMcpCommand(program); -registerFindUserCommand(program); registerFindSnippetsCommand(program); registerWotCommand(program); registerListUsernamesCommand(program); diff --git a/commands/mcp.ts b/commands/mcp.ts index d4846e1..03c8b7a 100644 --- a/commands/mcp.ts +++ b/commands/mcp.ts @@ -1,4 +1,4 @@ -import { Command } from 'commander'; +import type { Command } from 'commander'; import { readConfig } from "../config.js"; import { addCreatePubkeyCommand } from "../logic/create-pubkey.js"; import { addFindSnippetsCommand } from "../logic/find_snippets.js"; @@ -6,6 +6,8 @@ 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 { addListSnippetsCommand } from "../logic/list_snippets.js"; +import { addFetchSnippetByIdCommand } from "../logic/fetch_snippet_by_id.js"; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; @@ -20,6 +22,8 @@ const commandMap: Record = { "find-user": addFindUserCommand, "find-snippets": addFindSnippetsCommand, "list-usernames": addListUsernamesCommand, + "list-snippets": addListSnippetsCommand, + "fetch-snippet-by-id": addFetchSnippetByIdCommand, }; // Global server instance @@ -70,5 +74,7 @@ export function registerMcpCommands(server: McpServer) { addFindUserCommand(server); addFindSnippetsCommand(server); addListUsernamesCommand(server); + addListSnippetsCommand(server); + addFetchSnippetByIdCommand(server); } } diff --git a/lib/cache/snippets.ts b/lib/cache/snippets.ts new file mode 100644 index 0000000..2d755e7 --- /dev/null +++ b/lib/cache/snippets.ts @@ -0,0 +1,7 @@ +import type { CodeSnippet } from "../types/index.js"; + +/** + * In-memory cache to store snippets by ID for efficient retrieval + * Used by list_snippets and fetch_snippet_by_id commands + */ +export const snippetsCache: Record = {}; \ No newline at end of file diff --git a/lib/converters/index.ts b/lib/converters/index.ts new file mode 100644 index 0000000..7b1d1a9 --- /dev/null +++ b/lib/converters/index.ts @@ -0,0 +1,3 @@ +// Export all converters +export * from './snippets.js'; +export * from './users.js'; \ No newline at end of file diff --git a/lib/converters/snippets.ts b/lib/converters/snippets.ts new file mode 100644 index 0000000..1af0b50 --- /dev/null +++ b/lib/converters/snippets.ts @@ -0,0 +1,195 @@ +import type { NDKEvent } from "@nostr-dev-kit/ndk"; +import { knownUsers } from "../../users.js"; +import type { CodeSnippet } from "../types/index.js"; + +/** + * Converts an NDKEvent into a CodeSnippet + * @param event NDKEvent of kind 1337 + * @returns CodeSnippet object + */ +export function toSnippet(event: NDKEvent): CodeSnippet { + const title = event.tagValue("title") ?? event.tagValue("name"); + const description = event.tagValue("description") ?? ""; + 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", + description, + code: event.content, + language: language || "text", + pubkey: event.pubkey, + createdAt: event.created_at || 0, + tags, + }; +} + +/** + * Converts a CodeSnippet to a formatted metadata string (without code) + * @param snippet CodeSnippet object + * @returns Formatted metadata string + */ +export function toMetadataString(snippet: CodeSnippet): string { + const { title, description, language, tags, id, pubkey, createdAt } = snippet; + const profile = knownUsers[pubkey]?.profile; + + const returns = [ + `ID: ${id}`, + `Title: ${title}`, + `Description: ${description}`, + `Language: ${language}`, + `Tags: ${tags.join(", ")}`, + `Created: ${new Date(createdAt * 1000).toISOString()}`, + `Pubkey: ${pubkey}`, + ]; + + if (profile?.name) returns.push(`Author: ${profile.name}`); + + return returns.join("\n"); +} + +/** + * Formats a single snippet for display + * @param snippet The snippet to format + * @returns Formatted string representation + */ +export function formatSnippet(snippet: CodeSnippet): string { + return formatSnippets([snippet]); +} + +/** + * Formats 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 = { + 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"); +} + +/** + * Formats 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; +} + +/** + * Parses metadata from a file content string + * @param fileContent String containing metadata and code sections + * @returns Object with metadata and code + */ +export function parseMetadataFromString(fileContent: string): { + metadata: { title: string; description: string; language: string; tags: string[] }; + code: string +} { + // Match the metadata and code sections + 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?.[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?.[1] ? languageMatch[1].trim() : ""; + + const tagsMatch = metadataSection.match(/^Tags:\s*(.+)$/m); + const tagsString = tagsMatch?.[1] ? tagsMatch[1].trim() : ""; + const tags = tagsString.split(',').map(tag => tag.trim()).filter(Boolean); + + return { + metadata: { + title, + description, + language, + tags + }, + code: codeSection + }; +} + +/** + * Creates a string with metadata and code sections + */ +export function createMetadataString( + 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}`; +} \ No newline at end of file diff --git a/lib/converters/users.ts b/lib/converters/users.ts new file mode 100644 index 0000000..082e4db --- /dev/null +++ b/lib/converters/users.ts @@ -0,0 +1,44 @@ +import { ndk } from "../../ndk.js"; +import { knownUsers } from "../../users.js"; +import { queryUser } from "../../users.js"; + +/** + * Converts an identifier (pubkey, npub, or name) to pubkeys + * @param identifier The identifier to convert + * @returns Array of pubkeys + */ +export function toPubkeys(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); +} + +/** + * Format user profile data for display + * @param pubkey User public key + * @returns Formatted string representation + */ +export function formatUser(pubkey: string): string { + const profile = knownUsers[pubkey]?.profile; + const user = ndk.getUser({ pubkey }); + const keys: Record = { + 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"); +} \ No newline at end of file diff --git a/lib/nostr/snippets.ts b/lib/nostr/snippets.ts index dccc9a8..3d544a5 100644 --- a/lib/nostr/snippets.ts +++ b/lib/nostr/snippets.ts @@ -1,9 +1,9 @@ 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"; +import { SNIPPET_KIND, identifierToPubkeys } from "./utils.js"; +import { formatPartialMatches, formatSnippets, toSnippet } from "../converters/index.js"; /** * Get code snippets from Nostr events of kind 1337 @@ -87,50 +87,11 @@ export async function getSnippets(params: FindSnippetsParams = {}): Promise<{ } // Convert events to snippets - const snippets = selectedEvents.map(eventToSnippet); - const otherSnippets = notSelectedEvents.map(eventToSnippet); + const snippets = selectedEvents.map(toSnippet); + const otherSnippets = notSelectedEvents.map(toSnippet); 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 = { - 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; -} +// Re-export formatters for backward compatibility +export { formatSnippets, formatPartialMatches }; diff --git a/lib/nostr/utils.ts b/lib/nostr/utils.ts index 3d55aaa..896078b 100644 --- a/lib/nostr/utils.ts +++ b/lib/nostr/utils.ts @@ -1,9 +1,8 @@ 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"; +import { toPubkeys, toSnippet } from "../converters/index.js"; export const SNIPPET_KIND = 1337; @@ -32,45 +31,5 @@ export async function getSigner(username?: string): Promise { 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, - }; -} +// Re-export converter functions for backward compatibility +export { toPubkeys as identifierToPubkeys, toSnippet as eventToSnippet }; diff --git a/lib/types/index.ts b/lib/types/index.ts index 959e3c2..c3c7be1 100644 --- a/lib/types/index.ts +++ b/lib/types/index.ts @@ -17,6 +17,7 @@ export interface FindSnippetsParams { export type CodeSnippet = { id: string; title: string; + description: string; code: string; language: string; pubkey: string; diff --git a/logic/fetch_snippet_by_id.ts b/logic/fetch_snippet_by_id.ts new file mode 100644 index 0000000..41c848c --- /dev/null +++ b/logic/fetch_snippet_by_id.ts @@ -0,0 +1,78 @@ +import { z } from "zod"; +import { formatSnippets } from "../lib/converters/index.js"; +import { ndk } from "../ndk.js"; +import { SNIPPET_KIND } from "../lib/nostr/utils.js"; +import { log } from "../lib/utils/log.js"; +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { toSnippet } from "../lib/converters/index.js"; + +/** + * Fetch a snippet by its ID + * @param id Snippet ID to fetch + * @returns The snippet content or an error message + */ +export async function fetchSnippetById(id: string): Promise<{ + content: Array<{ type: "text"; text: string }>; +}> { + try { + log(`Fetching snippet with ID: ${id}`); + + // Create filter for the specific event ID + const filter = { + kinds: [SNIPPET_KIND as number], + ids: [id], + }; + + // Fetch the event + const events = await ndk.fetchEvents(filter); + const event = Array.from(events)[0]; // Get the first (and should be only) event + + if (!event) { + return { + content: [ + { + type: "text", + text: `No snippet found with ID: ${id}`, + }, + ], + }; + } + + // Convert event to snippet + const snippet = toSnippet(event); + + // Format the snippet for display + const formattedSnippet = formatSnippets([snippet]); + + return { + content: [ + { + type: "text", + text: formattedSnippet, + }, + ], + }; + } catch (error: unknown) { + const errorMessage = + error instanceof Error ? error.message : String(error); + return { + content: [ + { + type: "text", + text: `Error fetching snippet: ${errorMessage}`, + }, + ], + }; + } +} + +export function addFetchSnippetByIdCommand(server: McpServer) { + server.tool( + "fetch_snippet_by_id", + "Fetch and display a snippet by its ID", + { + id: z.string().describe("ID of the snippet to fetch"), + }, + async ({ id }) => fetchSnippetById(id) + ); +} \ No newline at end of file diff --git a/logic/find_user.ts b/logic/find_user.ts index a38d023..c9d05b7 100644 --- a/logic/find_user.ts +++ b/logic/find_user.ts @@ -1,44 +1,52 @@ 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"; +import { toPubkeys, formatUser } from "../lib/converters/index.js"; +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.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 + * @param query Search query to find a user + * @returns Formatted user data or error message */ -export async function findUser(query: string) { +export async function findUser(query: string): Promise<{ + content: Array<{ type: "text"; text: string }>; +}> { try { - const pubkeys = queryUser(query); + // Convert the query to pubkeys + const pubkeys = toPubkeys(query); if (pubkeys.length === 0) { return { content: [ { - type: "text" as const, - text: "No users found matching the query.", + type: "text", + text: `No users found matching: ${query}`, }, ], }; } - // Format the found users for display + // Format user data for each pubkey const formattedUsers = pubkeys.map(formatUser).join("\n\n---\n\n"); return { content: [ { - type: "text" as const, - text: `Found ${pubkeys.length} users:\n\n${formattedUsers}`, + type: "text", + text: formattedUsers, }, ], }; } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : String(error); - throw new Error(`Failed to find user: ${errorMessage}`); + return { + content: [ + { + type: "text", + text: `Error finding user: ${errorMessage}`, + }, + ], + }; } } @@ -49,29 +57,7 @@ export function addFindUserCommand(server: McpServer) { { query: z.string().describe("The search query to find a user"), }, - async ({ query }) => { - return findUser(query); - } + async ({ query }) => 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 = { - 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"); -} diff --git a/logic/list_snippets.ts b/logic/list_snippets.ts new file mode 100644 index 0000000..4f79b78 --- /dev/null +++ b/logic/list_snippets.ts @@ -0,0 +1,88 @@ +import { z } from "zod"; +import type { CodeSnippet, FindSnippetsParams } from "../lib/types/index.js"; +import { getSnippets } from "../lib/nostr/snippets.js"; +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { toMetadataString } from "../lib/converters/index.js"; + +/** + * List snippets with metadata + * @param params Parameters to filter snippets + * @returns List of snippet metadata + */ +export async function listSnippets( + params: FindSnippetsParams = {} +): Promise<{ content: Array<{ type: "text", text: string }> }> { + try { + const result = await getSnippets(params); + let list = result.snippets + .map(toMetadataString) + .join("\n\n------------------\n\n"); + const extra = result.otherSnippets + .map(toMetadataString) + .join("\n\n------------------\n\n"); + + // Include partial matches if they exist + if (result.otherSnippets.length > 0) { + list += "\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"; + list += extra; + } + + if (list.length === 0) { + return { + content: [ + { + type: "text", + text: "No snippets found", + }, + ], + }; + } + + return { + content: [ + { + type: "text", + text: list, + }, + ], + }; + } catch (error: unknown) { + const errorMessage = + error instanceof Error ? error.message : String(error); + throw new Error(`Failed to list snippets: ${errorMessage}`); + } +} + +export function addListSnippetsCommand(server: McpServer) { + server.tool( + "list_snippets", + "List code snippets metadata (without code content) with filtering by language and tags. Use this to get a large list of available code snippets.", + { + since: z + .number() + .optional() + .describe("Fetch snippets newer than this timestamp"), + until: z + .number() + .optional() + .describe("Fetch snippets older than this timestamp"), + 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' ], we use OR matches" + ), + }, + async (args) => listSnippets(args) + ); +} \ No newline at end of file diff --git a/logic/publish-code-snippet.ts b/logic/publish-code-snippet.ts index 0b10517..e787808 100644 --- a/logic/publish-code-snippet.ts +++ b/logic/publish-code-snippet.ts @@ -2,15 +2,16 @@ 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 type { 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 { readConfig } from "../config.js"; import { existsSync } from "node:fs"; import * as Bun from "bun"; +import { createMetadataString, parseMetadataFromString } from "../lib/converters/index.js"; -function log(message: string, ...args: any[]) { +function log(message: string): void { // append to ~/.nmcp-nostr.log const logFilePath = join(homedir(), ".nmcp-nostr.log"); const logMessage = `${new Date().toISOString()} - ${message}\n`; @@ -134,25 +135,12 @@ export async function publishCodeSnippet( 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); + const fileContent = createMetadataString(title, description, language, tags, code); // Write the content to the temp file writeFileSync(tempFilePath, fileContent); @@ -163,7 +151,7 @@ export async function publishCodeSnippet( // 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); + log(`spawned editor process to edit ${tempFilePath}`); // Wait for the editor to close await process.exited; @@ -178,21 +166,21 @@ export async function publishCodeSnippet( if (existsSync(tempFilePath)) { const updatedContent = readFileSync(tempFilePath, "utf-8"); try { - log("updatedContent: " + updatedContent); - const parsed = parseMetadata(updatedContent); + log(`updatedContent: ${updatedContent}`); + const parsed = parseMetadataFromString(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); + 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); + log(`tempFilePath does not exist ${tempFilePath}`); } const eventTags = [