diff --git a/bot/README.md b/bot/README.md new file mode 100644 index 0000000..4cc919b --- /dev/null +++ b/bot/README.md @@ -0,0 +1,117 @@ +# Grimoire REQ Assistant Bot + +A Nostr bot that listens for mentions in the Grimoire group chat and helps users craft REQ queries for the Nostr protocol. + +## Features + +- Listens for mentions in NIP-29 group chats +- Uses LLM (Claude) to understand user questions +- Provides REQ command suggestions for querying Nostr relays +- Has tools to look up event kinds and NIPs + +## Setup + +### Prerequisites + +- Node.js 18+ +- An Anthropic API key (for Claude) + +### Installation + +```bash +cd bot +npm install +``` + +### Configuration + +Set environment variables: + +```bash +# Required: Anthropic API key +export ANTHROPIC_API_KEY="your-api-key" + +# Optional: Override bot settings +export BOT_PRIVATE_KEY="your-hex-private-key" +export RELAY_URL="wss://groups.0xchat.com" +export GROUP_ID="NkeVhXuWHGKKJCpn" +``` + +### Running + +Development mode (with hot reload): + +```bash +npm run dev +``` + +Production: + +```bash +npm run build +npm start +``` + +## Usage + +In the group chat, mention the bot with a question about REQ queries: + +``` +@grimoire-bot how do I find all notes from the last 24 hours? +``` + +The bot will respond with: + +``` +To find all notes from the last 24 hours: + +req -k 1 --since 1d relay.damus.io + +This command: +- `-k 1` filters for kind 1 (short text notes) +- `--since 1d` gets events from the last day +- `relay.damus.io` is the relay to query +``` + +## Available Tools + +The bot can look up: + +- **Event Kinds**: What each kind number means +- **NIPs**: Nostr Implementation Possibilities specifications +- **Kinds for NIP**: What kinds are defined in a specific NIP + +## Architecture + +``` +bot/ +├── src/ +│ ├── index.ts # Main bot entry point +│ ├── llm.ts # LLM integration with tools +│ └── data/ +│ ├── kinds.ts # Event kind definitions +│ └── nips.ts # NIP definitions +├── package.json +└── tsconfig.json +``` + +## Bot Identity + +Default bot pubkey: `4f2d3e...` (derived from the configured private key) + +The bot signs messages with its own identity and responds as a member of the group. + +## Supported REQ Options + +The bot can help with all grimoire REQ options: + +- `-k, --kind` - Filter by event kind +- `-a, --author` - Filter by author pubkey +- `-l, --limit` - Limit results +- `-i, --id` - Fetch by event ID +- `-e` - Filter by referenced events +- `-p` - Filter by mentioned pubkey +- `-t` - Filter by hashtag +- `--since`, `--until` - Time filters +- `--search` - Full-text search (NIP-50) +- And more... diff --git a/bot/package.json b/bot/package.json new file mode 100644 index 0000000..9f42fc6 --- /dev/null +++ b/bot/package.json @@ -0,0 +1,24 @@ +{ + "name": "@grimoire/bot", + "version": "0.0.1", + "description": "Grimoire REQ assistant bot for Nostr group chat", + "type": "module", + "main": "dist/index.js", + "scripts": { + "build": "tsc", + "start": "node dist/index.js", + "dev": "tsx src/index.ts" + }, + "dependencies": { + "@mariozechner/pi-ai": "^0.51.2", + "@noble/hashes": "^1.8.0", + "@sinclair/typebox": "^0.34.48", + "nostr-tools": "^2.10.4", + "websocket-polyfill": "^0.0.3" + }, + "devDependencies": { + "@types/node": "^22.10.5", + "tsx": "^4.19.2", + "typescript": "^5.7.2" + } +} diff --git a/bot/src/data/kinds.ts b/bot/src/data/kinds.ts new file mode 100644 index 0000000..d596d4f --- /dev/null +++ b/bot/src/data/kinds.ts @@ -0,0 +1,341 @@ +/** + * Event kind definitions for Nostr + * Used by the bot to provide information about event kinds + */ + +export interface EventKindInfo { + kind: number; + name: string; + description: string; + nip: string; +} + +export const EVENT_KINDS: Record = { + // Core protocol kinds + 0: { + kind: 0, + name: "Profile", + description: + "User Metadata (kind 0) - Contains user profile information like name, about, picture, nip05, etc. This is a replaceable event.", + nip: "01", + }, + 1: { + kind: 1, + name: "Note", + description: + "Short Text Note (kind 1) - The most common event type, used for posting text content similar to tweets. Supports mentions, hashtags, and references to other events.", + nip: "01", + }, + 2: { + kind: 2, + name: "Relay Recommendation", + description: + "Recommend Relay (kind 2) - Deprecated. Was used to recommend relays.", + nip: "01", + }, + 3: { + kind: 3, + name: "Contact List", + description: + "Follows/Contact List (kind 3) - Contains the list of pubkeys a user follows. Also may include relay preferences.", + nip: "02", + }, + 4: { + kind: 4, + name: "Encrypted DM", + description: + "Encrypted Direct Messages (kind 4) - Legacy encrypted DMs. Deprecated in favor of NIP-17 (kind 14).", + nip: "04", + }, + 5: { + kind: 5, + name: "Deletion", + description: + "Event Deletion Request (kind 5) - Request to delete previously published events.", + nip: "09", + }, + 6: { + kind: 6, + name: "Repost", + description: + "Repost (kind 6) - Repost/boost another event (specifically kind 1 notes).", + nip: "18", + }, + 7: { + kind: 7, + name: "Reaction", + description: + "Reaction (kind 7) - React to events with emoji or '+'/'-'. Contains e-tag pointing to the target event.", + nip: "25", + }, + 8: { + kind: 8, + name: "Badge Award", + description: "Badge Award (kind 8) - Award a badge to users.", + nip: "58", + }, + 9: { + kind: 9, + name: "Chat Message", + description: + "Chat Message (kind 9) - Used in NIP-29 relay-based groups and NIP-C7 chats. Contains h-tag for group context.", + nip: "29", + }, + 10: { + kind: 10, + name: "Group Reply", + description: "Group Chat Threaded Reply (kind 10)", + nip: "29", + }, + 11: { + kind: 11, + name: "Thread", + description: "Thread (kind 11) - Thread root event.", + nip: "7D", + }, + 14: { + kind: 14, + name: "Direct Message", + description: + "Direct Message (kind 14) - Private direct messages using NIP-17 encryption.", + nip: "17", + }, + 16: { + kind: 16, + name: "Generic Repost", + description: + "Generic Repost (kind 16) - Repost any event type, not just kind 1.", + nip: "18", + }, + 40: { + kind: 40, + name: "Channel Create", + description: "Channel Creation (kind 40) - Create a public chat channel.", + nip: "28", + }, + 41: { + kind: 41, + name: "Channel Metadata", + description: "Channel Metadata (kind 41) - Update channel metadata.", + nip: "28", + }, + 42: { + kind: 42, + name: "Channel Message", + description: "Channel Message (kind 42) - Send a message to a channel.", + nip: "28", + }, + 1063: { + kind: 1063, + name: "File Metadata", + description: + "File Metadata (kind 1063) - Metadata about files stored on servers.", + nip: "94", + }, + 1111: { + kind: 1111, + name: "Comment", + description: "Comment (kind 1111) - Comment on any addressable content.", + nip: "22", + }, + 1311: { + kind: 1311, + name: "Live Chat", + description: + "Live Chat Message (kind 1311) - Chat messages in live activities/streams.", + nip: "53", + }, + 1617: { + kind: 1617, + name: "Patch", + description: "Git Patches (kind 1617) - Git patch for code collaboration.", + nip: "34", + }, + 1984: { + kind: 1984, + name: "Report", + description: + "Reporting (kind 1984) - Report content or users for moderation.", + nip: "56", + }, + 1985: { + kind: 1985, + name: "Label", + description: "Label (kind 1985) - Add labels/tags to content.", + nip: "32", + }, + 5000: { + kind: 5000, + name: "Job Request", + description: + "DVM Job Request (kind 5000-5999) - Data Vending Machine job requests.", + nip: "90", + }, + 6000: { + kind: 6000, + name: "Job Result", + description: + "DVM Job Result (kind 6000-6999) - Data Vending Machine job results.", + nip: "90", + }, + 7000: { + kind: 7000, + name: "Job Feedback", + description: "Job Feedback (kind 7000) - Feedback for DVM jobs.", + nip: "90", + }, + 9734: { + kind: 9734, + name: "Zap Request", + description: "Zap Request (kind 9734) - Request to create a lightning zap.", + nip: "57", + }, + 9735: { + kind: 9735, + name: "Zap", + description: + "Zap (kind 9735) - Lightning zap receipt created by LNURL providers.", + nip: "57", + }, + 9802: { + kind: 9802, + name: "Highlight", + description: + "Highlights (kind 9802) - Highlight text from articles or content.", + nip: "84", + }, + 10000: { + kind: 10000, + name: "Mute List", + description: "Mute List (kind 10000) - List of muted users/content.", + nip: "51", + }, + 10002: { + kind: 10002, + name: "Relay List", + description: + "Relay List Metadata (kind 10002) - User's relay list for inbox/outbox model.", + nip: "65", + }, + 10003: { + kind: 10003, + name: "Bookmark List", + description: "Bookmark List (kind 10003) - User's bookmarked content.", + nip: "51", + }, + 30000: { + kind: 30000, + name: "Follow Set", + description: "Follow Sets (kind 30000) - Named sets of followed users.", + nip: "51", + }, + 30001: { + kind: 30001, + name: "Generic List", + description: "Generic Lists (kind 30001) - Deprecated generic list.", + nip: "51", + }, + 30008: { + kind: 30008, + name: "Profile Badge", + description: "Profile Badges (kind 30008) - Badges displayed on profiles.", + nip: "58", + }, + 30009: { + kind: 30009, + name: "Badge", + description: "Badge Definition (kind 30009) - Define a badge.", + nip: "58", + }, + 30023: { + kind: 30023, + name: "Article", + description: + "Long-form Content (kind 30023) - Blog posts and articles with markdown support.", + nip: "23", + }, + 30078: { + kind: 30078, + name: "App Data", + description: + "Application-specific Data (kind 30078) - App-specific user data.", + nip: "78", + }, + 30311: { + kind: 30311, + name: "Live Event", + description: "Live Event (kind 30311) - Live streaming events.", + nip: "53", + }, + 30617: { + kind: 30617, + name: "Repository", + description: + "Repository Announcement (kind 30617) - Git repository metadata.", + nip: "34", + }, + 39000: { + kind: 39000, + name: "Group Metadata", + description: "Group Metadata (kind 39000) - NIP-29 group information.", + nip: "29", + }, + 39001: { + kind: 39001, + name: "Group Admins", + description: + "Group Admins List (kind 39001) - Admin list for NIP-29 groups.", + nip: "29", + }, + 39002: { + kind: 39002, + name: "Group Members", + description: + "Group Members List (kind 39002) - Member list for NIP-29 groups.", + nip: "29", + }, +}; + +/** + * Get kind info by number + */ +export function getKindInfo(kind: number): EventKindInfo | undefined { + return EVENT_KINDS[kind]; +} + +/** + * Search kinds by name or description + */ +export function searchKinds(query: string): EventKindInfo[] { + const lowerQuery = query.toLowerCase(); + return Object.values(EVENT_KINDS).filter( + (k) => + k.name.toLowerCase().includes(lowerQuery) || + k.description.toLowerCase().includes(lowerQuery), + ); +} + +/** + * Get all kinds for a specific NIP + */ +export function getKindsForNip(nipId: string): EventKindInfo[] { + return Object.values(EVENT_KINDS).filter((k) => k.nip === nipId); +} + +/** + * Get formatted list of common kinds for the LLM + */ +export function getCommonKindsReference(): string { + const commonKinds = [ + 0, 1, 3, 4, 5, 6, 7, 9, 14, 16, 1111, 9734, 9735, 10002, 30023, 30311, + ]; + return commonKinds + .map((k) => { + const info = EVENT_KINDS[k]; + return info + ? `- Kind ${k}: ${info.name} - ${info.description.split(".")[0]}` + : null; + }) + .filter(Boolean) + .join("\n"); +} diff --git a/bot/src/data/nips.ts b/bot/src/data/nips.ts new file mode 100644 index 0000000..052a543 --- /dev/null +++ b/bot/src/data/nips.ts @@ -0,0 +1,313 @@ +/** + * NIP (Nostr Implementation Possibilities) definitions + * Used by the bot to provide information about NIPs + */ + +export interface NipInfo { + id: string; + title: string; + description: string; + deprecated?: boolean; +} + +export const NIP_DATA: Record = { + "01": { + id: "01", + title: "Basic protocol flow description", + description: + "Defines the basic protocol: event structure, signatures, relay communication, and basic event kinds (0=metadata, 1=text note, 2=relay recommendation).", + }, + "02": { + id: "02", + title: "Follow List", + description: + "Kind 3 contact list - stores the list of pubkeys a user follows along with relay preferences.", + }, + "03": { + id: "03", + title: "OpenTimestamps Attestations for Events", + description: "Timestamping events using OpenTimestamps.", + }, + "04": { + id: "04", + title: "Encrypted Direct Message", + description: + "Legacy encrypted DMs (kind 4). DEPRECATED - use NIP-17 instead for better privacy.", + deprecated: true, + }, + "05": { + id: "05", + title: "Mapping Nostr keys to DNS-based internet identifiers", + description: + "NIP-05 verification - maps user@domain.com to a pubkey via .well-known/nostr.json.", + }, + "06": { + id: "06", + title: "Basic key derivation from mnemonic seed phrase", + description: "Derive Nostr keys from BIP-39 mnemonic seed phrases.", + }, + "07": { + id: "07", + title: "window.nostr capability for web browsers", + description: + "Browser extension interface for signing - allows web apps to request event signing from extensions like nos2x, Alby.", + }, + "09": { + id: "09", + title: "Event Deletion Request", + description: + "Kind 5 deletion requests - request relays to delete specified events.", + }, + "10": { + id: "10", + title: "Text Notes and Threads", + description: + "Conventions for text notes (kind 1), threading with e-tags (root, reply, mention markers).", + }, + "11": { + id: "11", + title: "Relay Information Document", + description: + "NIP-11 relay info - GET /.well-known/nostr.json returns relay metadata (name, description, supported NIPs, fees).", + }, + "13": { + id: "13", + title: "Proof of Work", + description: + "Add proof of work to events by finding a nonce that produces a hash with leading zeros.", + }, + "15": { + id: "15", + title: "Nostr Marketplace", + description: + "Stalls (kind 30017), products (kind 30018), and orders for marketplace functionality.", + }, + "17": { + id: "17", + title: "Private Direct Messages", + description: + "Modern encrypted DMs (kind 14) using gift wrapping for better metadata privacy. Replaces NIP-04.", + }, + "18": { + id: "18", + title: "Reposts", + description: + "Kind 6 for reposting kind 1 notes, kind 16 for generic reposts of any event type.", + }, + "19": { + id: "19", + title: "bech32-encoded entities", + description: + "Bech32 encoding: npub (pubkey), nsec (private key), note (event id), nprofile, nevent, naddr, nrelay.", + }, + "21": { + id: "21", + title: "nostr: URI scheme", + description: "nostr: URI scheme for linking to Nostr entities.", + }, + "22": { + id: "22", + title: "Comment", + description: + "Kind 1111 comments - comment on any addressable content (articles, videos, etc.).", + }, + "23": { + id: "23", + title: "Long-form Content", + description: + "Kind 30023 articles - long-form markdown content with title, summary, published_at tags.", + }, + "25": { + id: "25", + title: "Reactions", + description: + "Kind 7 reactions - react to events with '+', '-', or emoji. Uses e-tag to reference target.", + }, + "27": { + id: "27", + title: "Text Note References", + description: + "Reference and quote other events within notes using nostr: URIs.", + }, + "28": { + id: "28", + title: "Public Chat", + description: + "Public chat channels - kind 40 (create), kind 41 (metadata), kind 42 (messages).", + }, + "29": { + id: "29", + title: "Relay-based Groups", + description: + "NIP-29 groups - relay-enforced groups with kind 9 messages, group metadata (39000), admins (39001), members (39002). Messages use h-tag for group context.", + }, + "30": { + id: "30", + title: "Custom Emoji", + description: + "Custom emoji using emoji tags - :shortcode: syntax with image URLs.", + }, + "32": { + id: "32", + title: "Labeling", + description: "Kind 1985 labels - categorize and tag content.", + }, + "34": { + id: "34", + title: "git stuff", + description: + "Git collaboration - repositories (30617), patches (1617), issues (1621), and more.", + }, + "36": { + id: "36", + title: "Sensitive Content", + description: "Content warnings using content-warning tag.", + }, + "38": { + id: "38", + title: "User Statuses", + description: + "Kind 30315 user statuses - what user is doing, listening to, etc.", + }, + "42": { + id: "42", + title: "Authentication of clients to relays", + description: + "AUTH message for relay authentication - kind 22242 for client auth challenges.", + }, + "44": { + id: "44", + title: "Encrypted Payloads (Versioned)", + description: "Versioned encryption for payloads.", + }, + "45": { + id: "45", + title: "Counting results", + description: + "COUNT message to get event counts without fetching all events.", + }, + "46": { + id: "46", + title: "Nostr Remote Signing", + description: + "Remote signing via kind 24133 - allows signing events on a different device.", + }, + "47": { + id: "47", + title: "Nostr Wallet Connect", + description: + "NWC - connect wallets to apps for lightning payments via kind 13194, 23194, 23195.", + }, + "50": { + id: "50", + title: "Search Capability", + description: "Search filter support in REQ - use search field in filters.", + }, + "51": { + id: "51", + title: "Lists", + description: + "Various list types - mute (10000), pin (10001), bookmark (10003), follow sets (30000), etc.", + }, + "52": { + id: "52", + title: "Calendar Events", + description: + "Calendar events - date-based (31922), time-based (31923), calendars (31924), RSVPs (31925).", + }, + "53": { + id: "53", + title: "Live Activities", + description: + "Live streaming - kind 30311 live events, kind 1311 chat messages.", + }, + "56": { + id: "56", + title: "Reporting", + description: "Kind 1984 reports for content moderation.", + }, + "57": { + id: "57", + title: "Lightning Zaps", + description: + "Zaps - kind 9734 zap requests, kind 9735 zap receipts. Integrates lightning payments.", + }, + "58": { + id: "58", + title: "Badges", + description: + "Badge system - kind 30009 (define), kind 8 (award), kind 30008 (profile badges).", + }, + "59": { + id: "59", + title: "Gift Wrap", + description: + "Gift wrap encryption (kind 1059) - wraps events for privacy. Used by NIP-17.", + }, + "65": { + id: "65", + title: "Relay List Metadata", + description: + "Kind 10002 relay list - defines user's read/write relay preferences for inbox/outbox model.", + }, + "72": { + id: "72", + title: "Moderated Communities", + description: + "Communities with moderation - kind 34550 community definition, kind 4550 approved posts.", + }, + "78": { + id: "78", + title: "Application-specific data", + description: "Kind 30078 for app-specific user data storage.", + }, + "84": { + id: "84", + title: "Highlights", + description: + "Kind 9802 highlights - save and share text highlights from content.", + }, + "89": { + id: "89", + title: "Recommended Application Handlers", + description: + "Kind 31990 app definitions, kind 31989 recommendations for handling event kinds.", + }, + "90": { + id: "90", + title: "Data Vending Machines", + description: + "DVMs - kind 5000-5999 job requests, kind 6000-6999 results, kind 7000 feedback.", + }, + "94": { + id: "94", + title: "File Metadata", + description: "Kind 1063 file metadata for files stored on servers.", + }, + "98": { + id: "98", + title: "HTTP Auth", + description: "Kind 27235 HTTP authentication using Nostr.", + }, +}; + +/** + * Get NIP info by ID + */ +export function getNipInfo(nipId: string): NipInfo | undefined { + // Normalize to uppercase and pad if needed + const normalized = nipId.toUpperCase().padStart(2, "0"); + return NIP_DATA[normalized]; +} + +/** + * Search NIPs by title or description + */ +export function searchNips(query: string): NipInfo[] { + const lowerQuery = query.toLowerCase(); + return Object.values(NIP_DATA).filter( + (n) => + n.title.toLowerCase().includes(lowerQuery) || + n.description.toLowerCase().includes(lowerQuery), + ); +} diff --git a/bot/src/index.ts b/bot/src/index.ts new file mode 100644 index 0000000..5ddea57 --- /dev/null +++ b/bot/src/index.ts @@ -0,0 +1,187 @@ +/** + * Grimoire REQ Assistant Bot + * + * A Nostr bot that listens for mentions in the Grimoire group chat + * and helps users craft REQ queries for the Nostr protocol. + */ + +import "websocket-polyfill"; +import { SimplePool, finalizeEvent, getPublicKey } from "nostr-tools"; +import type { NostrEvent, Filter } from "nostr-tools"; +import { hexToBytes } from "@noble/hashes/utils"; +import { processMessage } from "./llm.js"; + +// Configuration +const BOT_PRIVATE_KEY = + process.env.BOT_PRIVATE_KEY || + "99079e2ac9596a6e27f53f074b9b5303d7b58da8ee6a88c42e74f7cfb261dbe3"; +const RELAY_URL = process.env.RELAY_URL || "wss://groups.0xchat.com"; +const GROUP_ID = process.env.GROUP_ID || "NkeVhXuWHGKKJCpn"; + +// Derive bot pubkey from private key +const botSecretKey = hexToBytes(BOT_PRIVATE_KEY); +const botPubkey = getPublicKey(botSecretKey); + +console.log("Grimoire REQ Assistant Bot"); +console.log("=========================="); +console.log(`Bot pubkey: ${botPubkey}`); +console.log(`Relay: ${RELAY_URL}`); +console.log(`Group: ${GROUP_ID}`); +console.log(""); + +// Create relay pool +const pool = new SimplePool(); + +// Track processed event IDs to avoid duplicates +const processedEvents = new Set(); + +/** + * Check if a message mentions the bot + */ +function isBotMentioned(event: NostrEvent): boolean { + // Check p-tags for bot pubkey mention + for (const tag of event.tags) { + if (tag[0] === "p" && tag[1] === botPubkey) { + return true; + } + } + + // Also check content for npub mention (fallback) + // This is less reliable but some clients may not use p-tags + return false; +} + +/** + * Extract the user's question from a mention message + * Removes the bot mention prefix from the content + */ +function extractQuestion(event: NostrEvent): string { + // The content might have nostr:npub... or @npub... mentions + // Remove them to get the actual question + let content = event.content; + + // Remove nostr:npub... mentions + content = content.replace(/nostr:npub1[a-z0-9]+/gi, "").trim(); + + // Remove @mention patterns + content = content.replace(/@[a-z0-9]+/gi, "").trim(); + + return content; +} + +/** + * Send a message to the group chat + */ +async function sendGroupMessage( + content: string, + replyToEvent?: NostrEvent, +): Promise { + const tags: string[][] = [["h", GROUP_ID]]; + + // Add reply reference if replying to a message + if (replyToEvent) { + tags.push(["q", replyToEvent.id, RELAY_URL, replyToEvent.pubkey]); + } + + const eventTemplate = { + kind: 9, + created_at: Math.floor(Date.now() / 1000), + tags, + content, + }; + + const signedEvent = finalizeEvent(eventTemplate, botSecretKey); + + console.log(`Sending message: ${content.substring(0, 100)}...`); + + try { + await Promise.any(pool.publish([RELAY_URL], signedEvent)); + console.log(`Message sent: ${signedEvent.id}`); + } catch (error) { + console.error("Failed to send message:", error); + } +} + +/** + * Handle an incoming message that mentions the bot + */ +async function handleMention(event: NostrEvent): Promise { + // Skip if already processed + if (processedEvents.has(event.id)) { + return; + } + processedEvents.add(event.id); + + // Don't respond to our own messages + if (event.pubkey === botPubkey) { + return; + } + + const question = extractQuestion(event); + + if (!question) { + console.log("Empty question, skipping"); + return; + } + + console.log(`\nReceived question from ${event.pubkey.substring(0, 8)}...`); + console.log(`Question: ${question}`); + + try { + // Process with LLM + const response = await processMessage(question); + + // Send response as a reply + await sendGroupMessage(response, event); + } catch (error) { + console.error("Error processing message:", error); + await sendGroupMessage( + "Sorry, I encountered an error processing your request. Please try again.", + event, + ); + } +} + +/** + * Start the bot + */ +async function main(): Promise { + console.log("Connecting to relay and subscribing to group...\n"); + + // Subscribe to group messages + const filter: Filter = { + kinds: [9], // Chat messages + "#h": [GROUP_ID], // Group filter + since: Math.floor(Date.now() / 1000), // Only new messages + }; + + const sub = pool.subscribeMany([RELAY_URL], filter, { + onevent(event: NostrEvent) { + // Check if this message mentions the bot + if (isBotMentioned(event)) { + handleMention(event).catch(console.error); + } + }, + oneose() { + console.log("Subscription established, listening for mentions...\n"); + }, + }); + + // Handle graceful shutdown + process.on("SIGINT", () => { + console.log("\nShutting down..."); + sub.close(); + process.exit(0); + }); + + process.on("SIGTERM", () => { + console.log("\nShutting down..."); + sub.close(); + process.exit(0); + }); + + // Keep the process alive + console.log("Bot is running. Press Ctrl+C to stop.\n"); +} + +main().catch(console.error); diff --git a/bot/src/llm.ts b/bot/src/llm.ts new file mode 100644 index 0000000..7c32dc3 --- /dev/null +++ b/bot/src/llm.ts @@ -0,0 +1,303 @@ +/** + * LLM integration for the Grimoire REQ Assistant Bot + * + * Uses pi-ai to process user questions and generate REQ commands + */ + +import { + getModel, + complete, + validateToolCall, + type Tool, + type Context, +} from "@mariozechner/pi-ai"; +import { Type } from "@sinclair/typebox"; +import { + getKindInfo, + searchKinds, + getKindsForNip, + getCommonKindsReference, +} from "./data/kinds.js"; +import { getNipInfo, searchNips } from "./data/nips.js"; + +// Get model - default to Claude Haiku for fast, cheap responses +// Can be overridden with environment variable +const PROVIDER = process.env.LLM_PROVIDER || "anthropic"; +const MODEL_ID = process.env.LLM_MODEL || "claude-3-5-haiku-20241022"; + +// Use default model for simplicity (typed correctly) +const model = getModel("anthropic", "claude-3-5-haiku-20241022"); + +console.log(`Using LLM: ${PROVIDER}/${MODEL_ID}`); + +// System prompt for the REQ assistant +const SYSTEM_PROMPT = `You are the Grimoire REQ Assistant, a helpful bot that assists users in crafting Nostr REQ queries for the Grimoire protocol explorer. + +## Your Role +Help users construct REQ commands to query Nostr relays. Users will describe what they want to find, and you should respond with the appropriate REQ command syntax. + +## REQ Command Syntax +The REQ command follows this format: +\`\`\` +req [options] [relay...] +\`\`\` + +### Common Options: +- \`-k, --kind \` - Filter by event kind (supports comma-separated: -k 1,3,7) +- \`-a, --author \` - Filter by author (supports npub, hex, NIP-05, $me, $contacts) +- \`-l, --limit \` - Maximum events to return +- \`-i, --id \` - Fetch specific event by ID (note1, nevent1, hex) +- \`-e \` - Filter by referenced events (#e/#a tags) +- \`-p \` - Filter by mentioned pubkey (#p tag) +- \`-t \` - Filter by hashtag (#t tag) +- \`-d \` - Filter by d-tag +- \`--since