#!/usr/bin/env node /** * Fetches kinds.json from the nostr library and generates eventKinds.js * Run: node scripts/fetch-kinds.js */ import { fileURLToPath } from 'url'; import { dirname, join } from 'path'; import { writeFileSync } from 'fs'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const KINDS_URL = 'https://git.mleku.dev/mleku/nostr/raw/branch/main/encoders/kind/kinds.json'; async function fetchKinds() { console.log(`Fetching kinds from ${KINDS_URL}...`); const response = await fetch(KINDS_URL); if (!response.ok) { throw new Error(`Failed to fetch kinds.json: ${response.status} ${response.statusText}`); } const data = await response.json(); console.log(`Fetched ${Object.keys(data.kinds).length} kinds (version: ${data.version})`); return data; } function generateEventKinds(data) { const kinds = []; for (const [kindNum, info] of Object.entries(data.kinds)) { const k = parseInt(kindNum, 10); // Determine classification let isReplaceable = false; let isAddressable = false; let isEphemeral = false; if (info.classification === 'replaceable' || k === 0 || k === 3 || (k >= data.ranges.replaceable.start && k < data.ranges.replaceable.end)) { isReplaceable = true; } else if (info.classification === 'parameterized' || (k >= data.ranges.parameterized.start && k <= data.ranges.parameterized.end)) { isAddressable = true; } else if (info.classification === 'ephemeral' || (k >= data.ranges.ephemeral.start && k < data.ranges.ephemeral.end)) { isEphemeral = true; } const entry = { kind: k, name: info.name, description: info.description, nip: info.nip || null, }; if (isReplaceable) entry.isReplaceable = true; if (isAddressable) entry.isAddressable = true; if (isEphemeral) entry.isEphemeral = true; if (info.deprecated) entry.deprecated = true; if (info.spec) entry.spec = info.spec; // Add basic template entry.template = { kind: k, content: "", tags: [] }; // Add d tag for addressable events if (isAddressable) { entry.template.tags = [["d", "identifier"]]; } kinds.push(entry); } // Sort by kind number kinds.sort((a, b) => a.kind - b.kind); return kinds; } function generateJS(kinds, data) { return `/** * Nostr Event Kinds Database * Auto-generated from ${KINDS_URL} * Version: ${data.version} * Source: ${data.source} * * DO NOT EDIT - This file is auto-generated by scripts/fetch-kinds.js */ export const eventKinds = ${JSON.stringify(kinds, null, 2)}; // Kind ranges for classification export const kindRanges = ${JSON.stringify(data.ranges, null, 2)}; // Privileged kinds (require auth) export const privilegedKinds = ${JSON.stringify(data.privileged)}; // Directory kinds (public discovery) export const directoryKinds = ${JSON.stringify(data.directory)}; // Kind aliases export const kindAliases = ${JSON.stringify(data.aliases, null, 2)}; // Helper function to get event kind by number export function getEventKind(kindNumber) { return eventKinds.find(k => k.kind === kindNumber); } // Alias for compatibility export function getKindInfo(kind) { return getEventKind(kind); } export function getKindName(kind) { const info = getEventKind(kind); return info ? info.name : \`Kind \${kind}\`; } // Helper function to search event kinds by name or description export function searchEventKinds(query) { const lowerQuery = query.toLowerCase(); return eventKinds.filter(k => k.name.toLowerCase().includes(lowerQuery) || k.description.toLowerCase().includes(lowerQuery) || k.kind.toString().includes(query) ); } // Helper function to get all event kinds grouped by category export function getEventKindsByCategory() { return { regular: eventKinds.filter(k => k.kind < 10000 && !k.isReplaceable), replaceable: eventKinds.filter(k => k.isReplaceable), ephemeral: eventKinds.filter(k => k.isEphemeral), addressable: eventKinds.filter(k => k.isAddressable) }; } // Helper function to create a template event with current timestamp export function createTemplateEvent(kindNumber, userPubkey = null) { const kindInfo = getEventKind(kindNumber); if (!kindInfo) { return { kind: kindNumber, content: "", tags: [], created_at: Math.floor(Date.now() / 1000), pubkey: userPubkey || "" }; } return { ...kindInfo.template, created_at: Math.floor(Date.now() / 1000), pubkey: userPubkey || "" }; } export function isReplaceable(kind) { if (kind === 0 || kind === 3) return true; return kind >= ${data.ranges.replaceable.start} && kind < ${data.ranges.replaceable.end}; } export function isEphemeral(kind) { return kind >= ${data.ranges.ephemeral.start} && kind < ${data.ranges.ephemeral.end}; } export function isAddressable(kind) { return kind >= ${data.ranges.parameterized.start} && kind <= ${data.ranges.parameterized.end}; } export function isPrivileged(kind) { return privilegedKinds.includes(kind); } // Export kind categories for filtering in UI export const kindCategories = [ { id: "all", name: "All Kinds", filter: () => true }, { id: "regular", name: "Regular Events (0-9999)", filter: k => k.kind < 10000 && !k.isReplaceable }, { id: "replaceable", name: "Replaceable (10000-19999)", filter: k => k.isReplaceable }, { id: "ephemeral", name: "Ephemeral (20000-29999)", filter: k => k.isEphemeral }, { id: "addressable", name: "Addressable (30000-39999)", filter: k => k.isAddressable }, { id: "social", name: "Social", filter: k => [0, 1, 3, 6, 7].includes(k.kind) }, { id: "messaging", name: "Messaging", filter: k => [4, 9, 10, 11, 12, 14, 15, 40, 41, 42].includes(k.kind) }, { id: "lists", name: "Lists", filter: k => k.name.toLowerCase().includes("list") || k.name.toLowerCase().includes("set") }, { id: "marketplace", name: "Marketplace", filter: k => [30017, 30018, 30019, 30020, 1021, 1022, 30402, 30403].includes(k.kind) }, { id: "lightning", name: "Lightning/Zaps", filter: k => [9734, 9735, 9041, 9321, 7374, 7375, 7376].includes(k.kind) }, { id: "media", name: "Media", filter: k => [20, 21, 22, 1063, 1222, 1244].includes(k.kind) }, { id: "git", name: "Git/Code", filter: k => [818, 1337, 1617, 1618, 1619, 1621, 1622, 30617, 30618].includes(k.kind) }, { id: "calendar", name: "Calendar", filter: k => [31922, 31923, 31924, 31925].includes(k.kind) }, { id: "groups", name: "Groups", filter: k => (k.kind >= 9000 && k.kind <= 9030) || (k.kind >= 39000 && k.kind <= 39009) }, ]; `; } async function main() { try { const data = await fetchKinds(); const kinds = generateEventKinds(data); const js = generateJS(kinds, data); // Write to src/eventKinds.js const outPath = join(__dirname, '..', 'src', 'eventKinds.js'); writeFileSync(outPath, js); console.log(`Generated ${outPath} with ${kinds.length} kinds`); } catch (error) { console.error('Error:', error.message); process.exit(1); } } main();