mirror of
https://github.com/purrgrammer/grimoire.git
synced 2026-04-10 23:47:12 +02:00
feat(bot): Add Grimoire REQ Assistant bot
Adds a Nostr bot that listens for mentions in NIP-29 group chats and helps users craft REQ queries for the Nostr protocol. Features: - Listens for @mentions in groups.0xchat.com'NkeVhXuWHGKKJCpn - Uses Claude (via pi-ai SDK) to understand user questions - Provides REQ command suggestions with explanations - Tools to look up event kinds, NIPs, and their relationships Stack: TypeScript, nostr-tools, @mariozechner/pi-ai https://claude.ai/code/session_01X4HWkMGrghBv2RfY89L5Lz
This commit is contained in:
117
bot/README.md
Normal file
117
bot/README.md
Normal file
@@ -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...
|
||||
24
bot/package.json
Normal file
24
bot/package.json
Normal file
@@ -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"
|
||||
}
|
||||
}
|
||||
341
bot/src/data/kinds.ts
Normal file
341
bot/src/data/kinds.ts
Normal file
@@ -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<number, EventKindInfo> = {
|
||||
// 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");
|
||||
}
|
||||
313
bot/src/data/nips.ts
Normal file
313
bot/src/data/nips.ts
Normal file
@@ -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<string, NipInfo> = {
|
||||
"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),
|
||||
);
|
||||
}
|
||||
187
bot/src/index.ts
Normal file
187
bot/src/index.ts
Normal file
@@ -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<string>();
|
||||
|
||||
/**
|
||||
* 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<void> {
|
||||
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<void> {
|
||||
// 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<void> {
|
||||
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);
|
||||
303
bot/src/llm.ts
Normal file
303
bot/src/llm.ts
Normal file
@@ -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 <number>\` - Filter by event kind (supports comma-separated: -k 1,3,7)
|
||||
- \`-a, --author <pubkey>\` - Filter by author (supports npub, hex, NIP-05, $me, $contacts)
|
||||
- \`-l, --limit <number>\` - Maximum events to return
|
||||
- \`-i, --id <id>\` - Fetch specific event by ID (note1, nevent1, hex)
|
||||
- \`-e <id>\` - Filter by referenced events (#e/#a tags)
|
||||
- \`-p <pubkey>\` - Filter by mentioned pubkey (#p tag)
|
||||
- \`-t <hashtag>\` - Filter by hashtag (#t tag)
|
||||
- \`-d <identifier>\` - Filter by d-tag
|
||||
- \`--since <time>\` - Events after time (unix timestamp, or relative: 1h, 7d, 2w)
|
||||
- \`--until <time>\` - Events before time
|
||||
- \`--search <query>\` - Full-text search (relay must support NIP-50)
|
||||
- \`--close-on-eose\` - Close subscription after receiving all events
|
||||
- \`-f, --follow\` - Auto-refresh mode (like tail -f)
|
||||
- \`--view list|compact\` - Display mode
|
||||
|
||||
### Relays:
|
||||
Add relay URLs directly: \`req -k 1 wss://relay.damus.io\`
|
||||
Shorthand: \`req -k 1 relay.damus.io\` (wss:// added automatically)
|
||||
|
||||
### Special Aliases:
|
||||
- \`$me\` - Your logged-in pubkey
|
||||
- \`$contacts\` - Pubkeys you follow
|
||||
|
||||
## Common Event Kinds Reference:
|
||||
${getCommonKindsReference()}
|
||||
|
||||
## Guidelines:
|
||||
1. Always suggest specific, working commands
|
||||
2. Use appropriate kinds for the use case
|
||||
3. Suggest reasonable limits to avoid overwhelming results
|
||||
4. Recommend relays when helpful (popular ones: relay.damus.io, nos.lol, relay.nostr.band)
|
||||
5. Explain briefly what the command does
|
||||
6. If the request is unclear, ask clarifying questions
|
||||
7. If something isn't possible with REQ, explain why and suggest alternatives
|
||||
|
||||
## Response Format:
|
||||
- Keep responses concise and helpful
|
||||
- Show the REQ command in a code block
|
||||
- Add a brief explanation of what it does
|
||||
- If using less common kinds, mention what they are
|
||||
|
||||
Use the tools available to look up specific kind numbers or NIP details when needed.`;
|
||||
|
||||
// Define tools for the LLM
|
||||
const tools: Tool[] = [
|
||||
{
|
||||
name: "lookup_kind",
|
||||
description:
|
||||
"Look up information about a specific Nostr event kind number. Use this when you need to find the kind number for a specific type of event, or when you need details about what a kind does.",
|
||||
parameters: Type.Object({
|
||||
kind: Type.Number({
|
||||
description: "The event kind number to look up (e.g., 0, 1, 7, 30023)",
|
||||
}),
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: "search_kinds",
|
||||
description:
|
||||
"Search for event kinds by name or description. Use this when the user asks about a type of event but doesn't know the kind number (e.g., 'articles', 'zaps', 'reactions').",
|
||||
parameters: Type.Object({
|
||||
query: Type.String({
|
||||
description:
|
||||
"Search query to find matching kinds (e.g., 'article', 'zap', 'reaction', 'profile')",
|
||||
}),
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: "lookup_nip",
|
||||
description:
|
||||
"Look up information about a specific NIP (Nostr Implementation Possibility). Use this when you need details about a NIP specification.",
|
||||
parameters: Type.Object({
|
||||
nip_id: Type.String({
|
||||
description:
|
||||
"The NIP identifier (e.g., '01', '29', '57' for numeric NIPs, or 'C7', 'B0' for hex NIPs)",
|
||||
}),
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: "search_nips",
|
||||
description:
|
||||
"Search NIPs by title or description. Use this when the user asks about a feature and you need to find the relevant NIP.",
|
||||
parameters: Type.Object({
|
||||
query: Type.String({
|
||||
description:
|
||||
"Search query to find matching NIPs (e.g., 'zap', 'group', 'encryption')",
|
||||
}),
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: "get_kinds_for_nip",
|
||||
description:
|
||||
"Get all event kinds defined in a specific NIP. Use this to find what kinds are part of a particular NIP specification.",
|
||||
parameters: Type.Object({
|
||||
nip_id: Type.String({
|
||||
description: "The NIP identifier to get kinds for",
|
||||
}),
|
||||
}),
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Execute a tool call
|
||||
*/
|
||||
function executeTool(name: string, args: Record<string, unknown>): string {
|
||||
switch (name) {
|
||||
case "lookup_kind": {
|
||||
const kind = args.kind as number;
|
||||
const info = getKindInfo(kind);
|
||||
if (info) {
|
||||
return JSON.stringify({
|
||||
kind: info.kind,
|
||||
name: info.name,
|
||||
description: info.description,
|
||||
nip: info.nip,
|
||||
});
|
||||
}
|
||||
return JSON.stringify({
|
||||
error: `Kind ${kind} not found in database. It may be a valid kind not yet documented.`,
|
||||
});
|
||||
}
|
||||
|
||||
case "search_kinds": {
|
||||
const query = args.query as string;
|
||||
const results = searchKinds(query);
|
||||
if (results.length > 0) {
|
||||
return JSON.stringify(
|
||||
results.slice(0, 10).map((k) => ({
|
||||
kind: k.kind,
|
||||
name: k.name,
|
||||
description: k.description.split(".")[0],
|
||||
nip: k.nip,
|
||||
})),
|
||||
);
|
||||
}
|
||||
return JSON.stringify({ error: `No kinds found matching "${query}"` });
|
||||
}
|
||||
|
||||
case "lookup_nip": {
|
||||
const nipId = args.nip_id as string;
|
||||
const info = getNipInfo(nipId);
|
||||
if (info) {
|
||||
return JSON.stringify({
|
||||
id: info.id,
|
||||
title: info.title,
|
||||
description: info.description,
|
||||
deprecated: info.deprecated || false,
|
||||
});
|
||||
}
|
||||
return JSON.stringify({ error: `NIP-${nipId} not found in database` });
|
||||
}
|
||||
|
||||
case "search_nips": {
|
||||
const query = args.query as string;
|
||||
const results = searchNips(query);
|
||||
if (results.length > 0) {
|
||||
return JSON.stringify(
|
||||
results.slice(0, 10).map((n) => ({
|
||||
id: n.id,
|
||||
title: n.title,
|
||||
deprecated: n.deprecated || false,
|
||||
})),
|
||||
);
|
||||
}
|
||||
return JSON.stringify({ error: `No NIPs found matching "${query}"` });
|
||||
}
|
||||
|
||||
case "get_kinds_for_nip": {
|
||||
const nipId = args.nip_id as string;
|
||||
const kinds = getKindsForNip(nipId);
|
||||
if (kinds.length > 0) {
|
||||
return JSON.stringify(
|
||||
kinds.map((k) => ({
|
||||
kind: k.kind,
|
||||
name: k.name,
|
||||
})),
|
||||
);
|
||||
}
|
||||
return JSON.stringify({
|
||||
error: `No kinds found for NIP-${nipId} or NIP not in database`,
|
||||
});
|
||||
}
|
||||
|
||||
default:
|
||||
return JSON.stringify({ error: `Unknown tool: ${name}` });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a user message and generate a response
|
||||
*/
|
||||
export async function processMessage(userMessage: string): Promise<string> {
|
||||
const context: Context = {
|
||||
systemPrompt: SYSTEM_PROMPT,
|
||||
messages: [{ role: "user", content: userMessage, timestamp: Date.now() }],
|
||||
tools,
|
||||
};
|
||||
|
||||
// Run the conversation loop (handle tool calls)
|
||||
let iterations = 0;
|
||||
const maxIterations = 5; // Prevent infinite loops
|
||||
|
||||
while (iterations < maxIterations) {
|
||||
iterations++;
|
||||
|
||||
const response = await complete(model, context);
|
||||
context.messages.push(response);
|
||||
|
||||
// Check for tool calls
|
||||
const toolCalls = response.content.filter((b) => b.type === "toolCall");
|
||||
|
||||
if (toolCalls.length === 0) {
|
||||
// No tool calls, extract text response
|
||||
const textBlocks = response.content.filter((b) => b.type === "text");
|
||||
const textContent = textBlocks.map((b) => (b as any).text).join("\n");
|
||||
return (
|
||||
textContent ||
|
||||
"I couldn't generate a response. Please try rephrasing your question."
|
||||
);
|
||||
}
|
||||
|
||||
// Execute tool calls
|
||||
for (const call of toolCalls) {
|
||||
if (call.type !== "toolCall") continue;
|
||||
|
||||
try {
|
||||
const validatedArgs = validateToolCall(tools, call);
|
||||
const result = executeTool(call.name, validatedArgs);
|
||||
|
||||
context.messages.push({
|
||||
role: "toolResult",
|
||||
toolCallId: call.id,
|
||||
toolName: call.name,
|
||||
content: [{ type: "text", text: result }],
|
||||
isError: false,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
} catch (error) {
|
||||
context.messages.push({
|
||||
role: "toolResult",
|
||||
toolCallId: call.id,
|
||||
toolName: call.name,
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify({
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
}),
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "I reached the maximum number of processing steps. Please try a simpler question.";
|
||||
}
|
||||
16
bot/tsconfig.json
Normal file
16
bot/tsconfig.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"esModuleInterop": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"declaration": true,
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
Reference in New Issue
Block a user