UNPKG

askexperts

Version:

AskExperts SDK: build and use AI experts - ask them questions and pay with bitcoin on an open protocol

242 lines 10.1 kB
import { AskExpertsChatClient } from "../../client/AskExpertsChatClient.js"; import { debugError, enableAllDebug, enableErrorDebug, } from "../../common/debug.js"; import * as readline from "readline"; import * as fs from "fs"; import * as path from "path"; import { getWalletByNameOrDefault } from "./wallet/utils.js"; import { createDBClientForCommands } from "./utils.js"; /** * Execute the chat command with the given options and expert pubkey * * @param expertPubkey The pubkey of the expert to chat with * @param options Command line options */ export async function executeChatCommand(expertIdentifier, options) { try { const db = await createDBClientForCommands(options); // Check if the identifier is a pubkey or a nickname let expertPubkey = expertIdentifier; let foundByNickname = false; // Try to find expert by nickname if it doesn't look like a pubkey if (!expertIdentifier.startsWith('npub') && expertIdentifier.length !== 64) { const experts = await db.listExperts(); const expertByNickname = experts.find((expert) => expert.nickname.toLowerCase() === expertIdentifier.toLowerCase()); if (expertByNickname) { expertPubkey = expertByNickname.pubkey; foundByNickname = true; debugError(`Found expert with nickname "${expertIdentifier}", using pubkey: ${expertPubkey}`); } else { // If not found by nickname, assume it's a pubkey debugError(`No expert found with nickname "${expertIdentifier}", assuming it's a pubkey`); } } // Get the wallet and NWC string const wallet = await getWalletByNameOrDefault(db, options.wallet); const nwcString = wallet.nwc; if (!nwcString) { throw new Error(`Wallet ${options.wallet || 'default'} does not have an NWC string configured.`); } // Create the chat client with the NWC string const chatClient = new AskExpertsChatClient(expertPubkey, { nwcString, relays: options.relays, maxAmount: options.maxAmount, debug: options.debug, stream: options.stream }); // Initialize the client and fetch expert profile const expert = await chatClient.initialize(); if (foundByNickname) { console.log(`Expert: ${expert.name} (nickname: ${expertIdentifier})`); } else { console.log(`Expert: ${expert.name}`); } console.log(`Description: ${expert.description}`); console.log("-----------------------------------------------------------"); console.log('Type your messages and press Enter to send. Type "exit" to quit.'); console.log("-----------------------------------------------------------"); // Function to process a message and get a response const processMessage = async (message) => { if (!message) { return; } const start = Date.now(); try { // Process the message using the chat client const expertReply = await chatClient.processMessage(message); // Display the expert's reply if (options.stream) { // In stream mode, the output is already written to stdout // Just add a newline at the end console.log(); } else { // In non-stream mode, write the full reply process.stdout.write(`${expert.name || "Expert"} > ${expertReply}\n`); } } catch (error) { debugError("Error in chat:", error instanceof Error ? error.message : String(error)); console.error(`Error: ${error instanceof Error ? error.message : String(error)}`); } console.log(`-------------------------(${Date.now() - start} ms)----------------------------`); }; // Check if we should read from stdin if (options.stdin) { // Read the first message from stdin let stdinMessage = ''; const stdinChunks = []; process.stdin.on('data', (chunk) => { stdinChunks.push(chunk); }); process.stdin.on('end', async () => { stdinMessage = Buffer.concat(stdinChunks).toString().trim(); if (stdinMessage) { await processMessage(stdinMessage); // Dispose of resources and exit chatClient[Symbol.dispose](); process.exit(0); } else { console.error("Error: No message provided via stdin"); process.exit(1); } }); // Make sure stdin is in flowing mode process.stdin.resume(); } else { // Interactive mode with readline const rl = readline.createInterface({ input: process.stdin, output: process.stdout, prompt: "You > ", }); // Start the prompt rl.prompt(); // Handle user input rl.on("line", async (line) => { // Check if user wants to exit if (line.trim().toLowerCase() === "exit") { rl.close(); return; } // Get the user's message const message = line.trim(); if (!message) { rl.prompt(); return; } // Process file attachments const processedMessage = await processFileAttachments(message, rl); await processMessage(processedMessage); rl.prompt(); }); // Handle readline close rl.on("close", () => { console.log("Chat ended."); // Dispose of the chat client resources chatClient[Symbol.dispose](); process.exit(0); }); } } catch (error) { debugError("Failed to execute chat command:", error); throw error; } } /** * Process a message to handle file attachments * * @param message The original message * @param rl The readline interface to use for confirmation prompts * @returns The processed message with file contents appended (if any) */ async function processFileAttachments(message, rl) { // Regular expression to match attach_file: prefix const attachFileRegex = /\battach_file:([^\s]+)\b/g; // Find all file paths in the message const filePaths = []; let match; while ((match = attachFileRegex.exec(message)) !== null) { filePaths.push(match[1]); } // If no file attachments, return the original message if (filePaths.length === 0) { return message; } // Remove all attach_file: words from the message let processedMessage = message.replace(attachFileRegex, '').trim(); // Process each file for (const filePath of filePaths) { try { // Ask for confirmation const answer = await new Promise((resolve) => { rl.question(`Attach file ${filePath}? y/N `, resolve); }); // If confirmed, read the file and append to message if (answer.toLowerCase() === 'y') { try { // Resolve the file path (handle relative paths) const resolvedPath = path.resolve(filePath); // Read the file as UTF-8 const fileContent = fs.readFileSync(resolvedPath, 'utf8'); // Append the file content to the message processedMessage += `\n${fileContent}`; debugError(`Attached file: ${filePath}`); } catch (error) { console.error(`Error reading file ${filePath}: ${error instanceof Error ? error.message : String(error)}`); } } } catch (error) { debugError(`Error processing file ${filePath}:`, error); } } return processedMessage; } /** * Helper function to parse comma-separated lists * @param value The comma-separated string * @returns Array of trimmed strings */ function commaSeparatedList(value) { return value.split(",").map((item) => item.trim()); } /** * Register the chat command with the CLI * * @param program The commander program */ export function registerChatCommand(program) { program .command("chat") .description("Start a chat with a specific expert using pubkey or nickname") .argument("<expert-identifier>", "The pubkey or nickname of the expert to chat with") .option("-w, --wallet <name>", "Wallet name to use for payments (uses default if not specified)") .option("--relays <items>", "Comma-separated list of discovery relays", commaSeparatedList) .option("-m, --max-amount <number>", "Maximum amount to pay in satoshis per message", "100") .option("-d, --debug", "Enable debug logging") .option("-s, --stream", "Enable streaming") .option("--stdin", "Read first message from stdin and exit after reply") .option("-r, --remote", "Use remote wallet client") .option("-u, --url <url>", "URL of remote wallet server (default: https://api.askexperts.io)") .action(async (expertIdentifier, options) => { if (options.debug) enableAllDebug(); else enableErrorDebug(); try { await executeChatCommand(expertIdentifier, options); } catch (error) { debugError("Error executing chat command:", error); process.exit(1); } }); } //# sourceMappingURL=chat.js.map