diff --git a/package-lock.json b/package-lock.json
index 056419c..7650326 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -34,6 +34,7 @@
"hls-video-element": "^1.5.10",
"hls.js": "^1.6.15",
"jotai": "^2.15.2",
+ "js-yaml": "^4.1.1",
"lucide-react": "latest",
"media-chrome": "^4.17.2",
"prismjs": "^1.30.0",
@@ -53,6 +54,7 @@
"devDependencies": {
"@eslint/js": "^9.17.0",
"@react-router/dev": "^7.1.0",
+ "@types/js-yaml": "^4.0.9",
"@types/node": "^24.10.1",
"@types/prismjs": "^1.26.5",
"@types/react": "^19.2.7",
@@ -3817,6 +3819,13 @@
"@types/unist": "*"
}
},
+ "node_modules/@types/js-yaml": {
+ "version": "4.0.9",
+ "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz",
+ "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@types/json-schema": {
"version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
@@ -4625,7 +4634,6 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
- "dev": true,
"license": "Python-2.0"
},
"node_modules/aria-hidden": {
@@ -6312,7 +6320,6 @@
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
- "dev": true,
"license": "MIT",
"dependencies": {
"argparse": "^2.0.1"
diff --git a/package.json b/package.json
index 571e01a..119687f 100644
--- a/package.json
+++ b/package.json
@@ -42,6 +42,7 @@
"hls-video-element": "^1.5.10",
"hls.js": "^1.6.15",
"jotai": "^2.15.2",
+ "js-yaml": "^4.1.1",
"lucide-react": "latest",
"media-chrome": "^4.17.2",
"prismjs": "^1.30.0",
@@ -61,6 +62,7 @@
"devDependencies": {
"@eslint/js": "^9.17.0",
"@react-router/dev": "^7.1.0",
+ "@types/js-yaml": "^4.0.9",
"@types/node": "^24.10.1",
"@types/prismjs": "^1.26.5",
"@types/react": "^19.2.7",
diff --git a/src/components/DynamicWindowTitle.tsx b/src/components/DynamicWindowTitle.tsx
index 2828634..8100f1f 100644
--- a/src/components/DynamicWindowTitle.tsx
+++ b/src/components/DynamicWindowTitle.tsx
@@ -23,6 +23,9 @@ import {
import { getEventDisplayTitle } from "@/lib/event-title";
import { UserName } from "./nostr/UserName";
import { getTagValues } from "@/lib/nostr-utils";
+import { getLiveHost } from "@/lib/live-activity";
+import type { NostrEvent } from "@/types/nostr";
+import { getZapSender } from "applesauce-core/helpers/zap";
export interface WindowTitleData {
title: string | ReactElement;
@@ -30,6 +33,31 @@ export interface WindowTitleData {
tooltip?: string;
}
+/**
+ * Get the semantic author of an event based on kind-specific logic
+ * Returns the pubkey that should be displayed as the "author" for UI purposes
+ *
+ * Examples:
+ * - Zaps (9735): Returns the zapper (P tag), not the lightning service pubkey
+ * - Live activities (30311): Returns the host (first p tag with "Host" role)
+ * - Regular events: Returns event.pubkey
+ */
+function getSemanticAuthor(event: NostrEvent): string {
+ switch (event.kind) {
+ case 9735: {
+ // Zap: show the zapper, not the lightning service pubkey
+ const zapSender = getZapSender(event);
+ return zapSender || event.pubkey;
+ }
+ case 30311: {
+ // Live activity: show the host
+ return getLiveHost(event);
+ }
+ default:
+ return event.pubkey;
+ }
+}
+
/**
* Format profile names with prefix, handling $me and $contacts aliases
* @param prefix - Prefix to use (e.g., 'by ', '@ ')
@@ -266,6 +294,17 @@ function useDynamicTitle(window: WindowInstance): WindowTitleData {
const eventPointer: EventPointer | AddressPointer | undefined =
appId === "open" ? props.pointer : undefined;
const event = useNostrEvent(eventPointer);
+
+ // Get semantic author for events (e.g., zapper for zaps, host for live activities)
+ const semanticAuthorPubkey = useMemo(() => {
+ if (appId !== "open" || !event) return null;
+ return getSemanticAuthor(event);
+ }, [appId, event]);
+
+ // Fetch semantic author profile to ensure it's cached for rendering
+ // Called for side effects (preloading profile data)
+ void useProfile(semanticAuthorPubkey || "");
+
const eventTitle = useMemo(() => {
if (appId !== "open" || !event) return null;
@@ -277,10 +316,13 @@ function useDynamicTitle(window: WindowInstance): WindowTitleData {
{getEventDisplayTitle(event, false)}
-
-
+
);
- }, [appId, event]);
+ }, [appId, event, semanticAuthorPubkey]);
// Kind titles
const kindTitle = useMemo(() => {
@@ -471,12 +513,12 @@ function useDynamicTitle(window: WindowInstance): WindowTitleData {
return `NIP-${props.number}: ${title}`;
}, [appId, props]);
- // Man page titles - just show the command description, icon shows on hover
+ // Man page titles - show command name first, then description
const manTitle = useMemo(() => {
if (appId !== "man") return null;
- // For man pages, we'll show the command's description via tooltip
- // The title can just be generic or empty, as the icon conveys meaning
- return getCommandDescription(props.cmd) || `${props.cmd} manual`;
+ const cmdName = props.cmd?.toUpperCase() || "MAN";
+ const description = getCommandDescription(props.cmd);
+ return description ? `${cmdName} - ${description}` : cmdName;
}, [appId, props]);
// Kinds viewer title
diff --git a/src/components/KindRenderer.tsx b/src/components/KindRenderer.tsx
index 46c175e..563417e 100644
--- a/src/components/KindRenderer.tsx
+++ b/src/components/KindRenderer.tsx
@@ -4,6 +4,11 @@ import { NIPBadge } from "./NIPBadge";
import { Copy, CopyCheck } from "lucide-react";
import { Button } from "./ui/button";
import { useCopy } from "@/hooks/useCopy";
+import {
+ getKindSchema,
+ parseTagStructure,
+ getContentTypeDescription,
+} from "@/lib/nostr-schema";
// NIP-01 Kind ranges
const REPLACEABLE_START = 10000;
@@ -15,6 +20,7 @@ const PARAMETERIZED_REPLACEABLE_END = 40000;
export default function KindRenderer({ kind }: { kind: number }) {
const kindInfo = getKindInfo(kind);
+ const schema = getKindSchema(kind);
const Icon = kindInfo?.icon;
const category = getKindCategory(kind);
const eventType = getEventType(kind);
@@ -94,6 +100,84 @@ export default function KindRenderer({ kind }: { kind: number }) {
>
)}
+
+ {/* Schema Information */}
+ {schema && (
+ <>
+ {/* Content Type */}
+ {schema.content && (
+
+
Content
+
+ {getContentTypeDescription(schema.content.type)}
+
+
+ )}
+
+ {/* Tags */}
+ {schema.tags && schema.tags.length > 0 && (
+
+
+ Supported Tags
+ {schema.required && schema.required.length > 0 && (
+
+ ({schema.required.length} required)
+
+ )}
+
+
+
+
+
+ | name |
+ value |
+ other parameters |
+
+
+
+ {schema.tags.map((tag, i) => {
+ const isRequired = schema.required?.includes(tag.name);
+ const structure = parseTagStructure(tag);
+ return (
+
+
+
+ {tag.name}
+
+ {isRequired && (
+
+ required
+
+ )}
+ |
+
+ {structure.primaryValue || "—"}
+ |
+
+ {structure.otherParameters.length > 0
+ ? structure.otherParameters.join(", ")
+ : "—"}
+ |
+
+ );
+ })}
+
+
+
+
+ )}
+
+ {/* Usage Status */}
+
+ {schema.in_use
+ ? "✓ Actively used in the Nostr ecosystem"
+ : "⚠ Deprecated or experimental"}
+
+ >
+ )}
);
}
diff --git a/src/components/ManPage.tsx b/src/components/ManPage.tsx
index 6f46abc..2c0945d 100644
--- a/src/components/ManPage.tsx
+++ b/src/components/ManPage.tsx
@@ -1,9 +1,48 @@
import { manPages } from "@/types/man";
+import { useGrimoire } from "@/core/state";
interface ManPageProps {
cmd: string;
}
+/**
+ * ExecutableCommand - Renders a clickable command that executes when clicked
+ */
+function ExecutableCommand({
+ commandLine,
+ children,
+}: {
+ commandLine: string;
+ children: React.ReactNode;
+}) {
+ const { addWindow } = useGrimoire();
+
+ const handleClick = async () => {
+ const parts = commandLine.trim().split(/\s+/);
+ const commandName = parts[0]?.toLowerCase();
+ const cmdArgs = parts.slice(1);
+
+ const command = manPages[commandName];
+ if (command) {
+ // argParser can be async
+ const cmdProps = command.argParser
+ ? await Promise.resolve(command.argParser(cmdArgs))
+ : command.defaultProps || {};
+
+ addWindow(command.appId, cmdProps);
+ }
+ };
+
+ return (
+
+ );
+}
+
export default function ManPage({ cmd }: ManPageProps) {
const page = manPages[cmd];
@@ -23,11 +62,11 @@ export default function ManPage({ cmd }: ManPageProps) {
{/* Header */}
- {page.name.toUpperCase()}({page.section})
+ {page.name.toUpperCase()}
Grimoire Manual
- {page.name.toUpperCase()}({page.section})
+ {page.name.toUpperCase()}
@@ -81,7 +120,9 @@ export default function ManPage({ cmd }: ManPageProps) {
const [, command, description] = match;
return (
-
{command}
+
+ {command}
+
{description.trim()}
@@ -90,8 +131,10 @@ export default function ManPage({ cmd }: ManPageProps) {
}
// Fallback for examples without descriptions
return (
-
- {example}
+
+
+ {example}
+
);
})}
@@ -104,14 +147,16 @@ export default function ManPage({ cmd }: ManPageProps) {
SEE ALSO
-
- {page.seeAlso.map((cmd, i) => (
-
- {cmd}(1)
- {i < page.seeAlso!.length - 1 ? ", " : ""}
-
- ))}
-
+ {page.seeAlso.map((cmd, i) => (
+
+
+ {cmd}
+
+ {i < page.seeAlso!.length - 1 && (
+ ,
+ )}
+
+ ))}
)}
diff --git a/src/components/live/StreamChat.tsx b/src/components/nostr/ChatView.tsx
similarity index 51%
rename from src/components/live/StreamChat.tsx
rename to src/components/nostr/ChatView.tsx
index 0805c5f..717621d 100644
--- a/src/components/live/StreamChat.tsx
+++ b/src/components/nostr/ChatView.tsx
@@ -1,22 +1,20 @@
import { useMemo } from "react";
-import { useLiveTimeline } from "@/hooks/useLiveTimeline";
import type { NostrEvent } from "@/types/nostr";
+import type { EventPointer, AddressPointer } from "nostr-tools/nip19";
import { kinds } from "nostr-tools";
-import { UserName } from "../nostr/UserName";
-import { RichText } from "../nostr/RichText";
-import { Zap } from "lucide-react";
+import { UserName } from "./UserName";
+import { RichText } from "./RichText";
+import { Zap, CornerDownRight, Quote } from "lucide-react";
import { cn } from "@/lib/utils";
-import { getZapAmount, getZapSender } from "applesauce-core/helpers";
+import { getZapAmount, getZapSender, getTagValue } from "applesauce-core/helpers";
+import { getNip10References } from "applesauce-core/helpers/threading";
+import { useNostrEvent } from "@/hooks/useNostrEvent";
-interface StreamChatProps {
- streamEvent: NostrEvent;
- streamRelays: string[];
- hostRelays: string[];
+interface ChatViewProps {
+ events: NostrEvent[];
className?: string;
}
-// isConsecutive removed
-
const isSameDay = (date1: Date, date2: Date) => {
return (
date1.getFullYear() === date2.getFullYear() &&
@@ -25,63 +23,14 @@ const isSameDay = (date1: Date, date2: Date) => {
);
};
-export function StreamChat({
- streamEvent,
- streamRelays,
- hostRelays,
- className,
-}: StreamChatProps) {
- // const [message, setMessage] = useState("");
-
- // Combine stream relays + host relays
- const allRelays = useMemo(
- () => Array.from(new Set([...streamRelays, ...hostRelays])),
- [streamRelays, hostRelays],
- );
-
- // Fetch chat messages (kind 1311) and zaps (kind 9735) that a-tag this stream
- const timelineFilter = useMemo(
- () => ({
- kinds: [1311, 9735],
- "#a": [
- `${streamEvent.kind}:${streamEvent.pubkey}:${streamEvent.tags.find((t) => t[0] === "d")?.[1] || ""}`,
- ],
- limit: 100,
- }),
- [streamEvent],
- );
-
- const { events: allMessages } = useLiveTimeline(
- `stream-feed-${streamEvent.id}`,
- timelineFilter,
- allRelays,
- { stream: true },
- );
-
- /*
- const handleSubmit = (e: React.FormEvent) => {
- e.preventDefault();
- // TODO: Implement sending chat message
- console.log("Send message:", message);
- setMessage("");
- };
- */
-
+export function ChatView({ events, className }: ChatViewProps) {
return (
{/* Chat messages area */}
- {allMessages.map((event, index) => {
+ {events.map((event, index) => {
const currentDate = new Date(event.created_at * 1000);
- const prevEvent = allMessages[index + 1];
- // If prevEvent exists, compare days. If different, we need a separator AFTER this message (visually before/above it)
- // Actually, in flex-col-reverse:
- // [Newest Message] (index 0)
- //
- // [Old Message] (index 1)
-
- // Wait, logic is simpler:
- // Loop through events. Determine if Date Header is needed between this event and the next one (older one).
+ const prevEvent = events[index + 1];
const prevDate = prevEvent
? new Date(prevEvent.created_at * 1000)
@@ -138,17 +87,93 @@ export function StreamChat({
}
function ChatMessage({ event }: { event: NostrEvent }) {
+ const threadRefs = useMemo(() => getNip10References(event), [event]);
+ const replyToId = threadRefs.reply?.e?.id;
+ const qTagValue = useMemo(() => getTagValue(event, "q"), [event]);
+
return (
-
+
+ {replyToId && }
+ {qTagValue && }
+
+
+
+
+ );
+}
+
+function ReplyIndicator({ eventId }: { eventId: string }) {
+ const replyToEvent = useNostrEvent(eventId);
+
+ if (!replyToEvent) {
+ return null;
+ }
+
+ return (
+
+
-
+ {replyToEvent.content}
+
+ );
+}
+
+/**
+ * Parse q-tag value into EventPointer or AddressPointer
+ * Format can be:
+ * - Event ID: "abc123..." (64-char hex)
+ * - Address: "kind:pubkey:d-tag"
+ */
+function parseQTag(qValue: string): EventPointer | AddressPointer | null {
+ // Check if it's an address (contains colons)
+ if (qValue.includes(":")) {
+ const parts = qValue.split(":");
+ if (parts.length >= 2) {
+ const kind = parseInt(parts[0], 10);
+ const pubkey = parts[1];
+ const identifier = parts.slice(2).join(":") || "";
+
+ if (!isNaN(kind) && pubkey) {
+ return { kind, pubkey, identifier };
+ }
+ }
+ }
+
+ // Assume it's an event ID (hex string)
+ if (/^[0-9a-f]{64}$/i.test(qValue)) {
+ return { id: qValue };
+ }
+
+ return null;
+}
+
+function QuoteIndicator({ qValue }: { qValue: string }) {
+ const pointer = useMemo(() => parseQTag(qValue), [qValue]);
+ const quotedEvent = useNostrEvent(pointer || undefined);
+
+ if (!quotedEvent) {
+ return null;
+ }
+
+ return (
+
+
+
+ {quotedEvent.content}
+
);
}
diff --git a/src/components/nostr/RichText.tsx b/src/components/nostr/RichText.tsx
index 0c92085..1387729 100644
--- a/src/components/nostr/RichText.tsx
+++ b/src/components/nostr/RichText.tsx
@@ -91,15 +91,15 @@ export function RichText({
// Call hook unconditionally - it will handle undefined/null
const trimmedEvent = event
? {
- ...event,
- content: event.content.trim(),
- }
+ ...event,
+ content: event.content.trim(),
+ }
: undefined;
const renderedContent = useRenderedContent(
content
? ({
- content,
- } as NostrEvent)
+ content,
+ } as NostrEvent)
: trimmedEvent,
contentComponents,
);
diff --git a/src/components/nostr/kinds/LiveActivityDetailRenderer.tsx b/src/components/nostr/kinds/LiveActivityDetailRenderer.tsx
index 187d373..bb3003c 100644
--- a/src/components/nostr/kinds/LiveActivityDetailRenderer.tsx
+++ b/src/components/nostr/kinds/LiveActivityDetailRenderer.tsx
@@ -6,11 +6,12 @@ import {
getLiveHost,
} from "@/lib/live-activity";
import { VideoPlayer } from "@/components/live/VideoPlayer";
-import { StreamChat } from "@/components/live/StreamChat";
+import { ChatView } from "@/components/nostr/ChatView";
import { StatusBadge } from "@/components/live/StatusBadge";
import { UserName } from "../UserName";
import { Calendar } from "lucide-react";
import { useOutboxRelays } from "@/hooks/useOutboxRelays";
+import { useLiveTimeline } from "@/hooks/useLiveTimeline";
interface LiveActivityDetailRendererProps {
event: NostrEvent;
@@ -28,6 +29,31 @@ export function LiveActivityDetailRenderer({
authors: [hostPubkey],
});
+ // Combine stream relays + host relays for chat events
+ const allRelays = useMemo(
+ () => Array.from(new Set([...activity.relays, ...hostRelays])),
+ [activity.relays, hostRelays],
+ );
+
+ // Fetch chat messages (kind 1311) and zaps (kind 9735) that a-tag this stream
+ const timelineFilter = useMemo(
+ () => ({
+ kinds: [1311, 9735],
+ "#a": [
+ `${event.kind}:${event.pubkey}:${event.tags.find((t) => t[0] === "d")?.[1] || ""}`,
+ ],
+ limit: 100,
+ }),
+ [event],
+ );
+
+ const { events: chatEvents } = useLiveTimeline(
+ `stream-feed-${event.id}`,
+ timelineFilter,
+ allRelays,
+ { stream: true },
+ );
+
const videoUrl =
status === "live" && activity.streaming
? activity.streaming
@@ -80,17 +106,15 @@ export function LiveActivityDetailRenderer({
{activity.title || "Untitled Live Activity"}
-
+
{/* Chat Section */}
-
+
);
diff --git a/src/data/nostr-kinds-schema.yaml b/src/data/nostr-kinds-schema.yaml
new file mode 100644
index 0000000..7f172c2
--- /dev/null
+++ b/src/data/nostr-kinds-schema.yaml
@@ -0,0 +1,2062 @@
+_profile: &profile
+ type: pubkey
+ required: true
+ next:
+ type: relay
+ next:
+ type: free # petname
+
+_external: &external
+ type: free
+ required: true
+ next:
+ type: url
+
+_event: &event
+ type: id
+ required: true
+ next:
+ type: relay
+ next:
+ type: pubkey
+
+_addr: &addr
+ type: addr
+ required: true
+ next:
+ type: relay
+
+_kind: &kind
+ type: kind
+ required: true
+
+_rtag: &rtag
+ name: r
+ next:
+ type: url
+ required: true
+
+_relaytag: &relaytag
+ name: relay
+ next:
+ type: relay
+ required: true
+
+_imetatag: &imetatag
+ name: imeta
+ next:
+ type: imeta
+ required: true
+ variadic: true
+
+_emojitag: &emojitag
+ name: emoji
+ next:
+ type: free
+ required: true
+ next:
+ type: url
+ required: true
+
+_title: &titletag
+ name: title
+ next:
+ type: free
+
+_image: &imagetag
+ name: image
+ next:
+ type: url
+ required: true
+
+_description: &descriptiontag
+ name: description
+ next:
+ type: free
+ required: true
+
+_summary: &summarytag
+ name: summary
+ next:
+ type: free
+ required: true
+
+_publishedattag: &publishedattag
+ name: published_at
+ next:
+ type: timestamp
+ required: true
+
+_atag: &atag
+ name: a
+ next: *addr
+
+_ptag: &ptag
+ name: p
+ next: *profile
+
+_etag: &etag
+ name: e
+ next: *event
+
+_ktag: &ktag
+ name: k
+ next: *kind
+
+_gtag: >ag
+ name: g
+ next:
+ type: geohash
+ required: true
+
+_ptag-bare: &ptag-bare
+ name: p
+ next:
+ type: pubkey
+ required: true
+
+_etag-bare: &etag-bare
+ name: e
+ next:
+ type: id
+ required: true
+
+_atag-bare: &atag-bare
+ name: a
+ next:
+ type: addr
+ required: true
+
+generic_tags:
+ t:
+ type: lowercase
+ required: true
+ L:
+ type: free
+ required: true
+ l:
+ type: lowercase
+ required: true
+ next:
+ type: free
+ required: true
+ expiration:
+ type: timestamp
+ required: true
+ h:
+ type: free
+ required: true
+
+kinds:
+ 0:
+ description: User metadata
+ in_use: true
+ content:
+ type: json
+ tags:
+ - *emojitag
+
+ 1:
+ description: Short text note
+ in_use: true
+ content:
+ type: free
+ tags:
+ -
+ name: e
+ next:
+ type: id
+ required: true
+ next:
+ type: relay
+ next:
+ type: constrained
+ either:
+ - reply
+ - root
+ next:
+ type: pubkey
+ -
+ name: q
+ next:
+ type: id
+ required: true
+ next:
+ type: relay
+ next:
+ type: pubkey
+ -
+ name: q
+ next:
+ type: addr
+ required: true
+ next:
+ type: relay
+ -
+ name: p
+ next: *profile
+ -
+ name: a
+ next: *addr
+ -
+ name: subject
+ next:
+ type: free
+ required: true
+ - *emojitag
+
+ 3:
+ description: Follows
+ in_use: true
+ content:
+ type: free
+ tags:
+ - *ptag
+
+ 4:
+ description: Encrypted Direct Messages
+ content:
+ type: free
+ tags:
+ - *ptag
+
+ 5:
+ description: Event Deletion Request
+ in_use: true
+ content:
+ type: empty
+ tags:
+ - *etag-bare
+ - *atag-bare
+
+ 6:
+ description: Repost
+ in_use: true
+ content:
+ type: json
+ tags:
+ - *etag
+ - *ptag
+
+ 7:
+ description: Reaction
+ in_use: true
+ content:
+ type: free
+ tags:
+ - *etag
+ - *ptag
+ - *emojitag
+
+ 8:
+ description: Badge Award
+ in_use: true
+ content:
+ type: empty
+ tags:
+ - *atag
+ - *ptag
+
+ 9:
+ description: Chat Message
+ in_use: true
+ content:
+ type: free
+ tags:
+ -
+ name: e
+ next:
+ type: id
+ required: true
+ next:
+ type: relay
+ next:
+ type: constrained
+ either:
+ - root
+ next:
+ type: pubkey
+ -
+ name: q
+ next:
+ type: id
+ required: true
+ next:
+ type: relay
+ next:
+ type: pubkey
+ -
+ name: q
+ next:
+ type: addr
+ required: true
+ next:
+ type: relay
+ - *ptag
+
+ 11:
+ description: Forum Thread
+ in_use: true
+ content:
+ type: free
+ tags:
+ - *titletag
+
+ 13:
+ description: Seal
+ in_use: true
+ content:
+ type: free
+ tags: []
+
+ 14:
+ description: Direct Message
+ in_use: true
+ content:
+ type: free
+ tags:
+ - *ptag
+ - *etag
+ -
+ name: subject
+ next:
+ type: free
+ required: true
+
+ 15:
+ description: File Message
+ in_use: true
+ content:
+ type: free
+ tags:
+ - *ptag
+ -
+ name: e
+ next:
+ type: id
+ required: true
+ next:
+ type: relay
+ next:
+ type: constrained
+ either:
+ - reply
+ -
+ name: subject
+ next:
+ type: free
+ required: true
+ -
+ name: file-type
+ next:
+ type: free
+ required: true
+ -
+ name: encryption-algorithm
+ next:
+ type: free
+ required: true
+ -
+ name: decryption-key
+ next:
+ type: free
+ required: true
+ -
+ name: decryption-nonce
+ next:
+ type: free
+ required: true
+ -
+ name: x
+ next:
+ type: free
+ required: true
+
+ 16:
+ description: Generic Repost
+ in_use: true
+ content:
+ type: json
+ tags:
+ - *etag
+ - *ptag
+ - *ktag
+
+ 17:
+ description: Reaction to a website
+ content:
+ type: free
+ required:
+ - r
+ tags:
+ - *rtag
+
+ 20:
+ description: Photo
+ in_use: true
+ content:
+ type: free
+ required:
+ - imeta
+ tags:
+ - *titletag
+ - *imetatag
+ -
+ name: content-warning
+ next:
+ type: free
+ required: true
+ - *ptag
+ -
+ name: m
+ next:
+ type: constrained
+ either:
+ - image/apng
+ - image/avif
+ - image/gif
+ - image/jpeg
+ - image/png
+ - image/webp
+ required: true
+ -
+ name: x
+ next:
+ type: hex
+ min: 64
+ max: 64
+ required: true
+ -
+ name: location
+ next:
+ type: free
+ required: true
+ - *gtag
+
+ 21:
+ description: Normal Video Event
+ in_use: true
+ content:
+ type: free
+ required:
+ - imeta
+ tags:
+ - *titletag
+ - *publishedattag
+ - *imetatag
+ -
+ name: content-warning
+ next:
+ type: free
+ required: true
+ -
+ name: segment
+ next:
+ type: free
+ required: true
+ next:
+ type: free
+ required: true
+ next:
+ type: free
+ required: true
+ next:
+ type: url
+ - *ptag
+ - *rtag
+ -
+ name: text-track
+ next:
+ type: json
+ required: true
+ next:
+ type: relay
+
+ 22:
+ description: Short Video Event
+ in_use: true
+ content:
+ type: free
+ required:
+ - imeta
+ tags:
+ - *titletag
+ - *publishedattag
+ - *imetatag
+ -
+ name: content-warning
+ next:
+ type: free
+ required: true
+ -
+ name: segment
+ next:
+ type: free
+ required: true
+ next:
+ type: free
+ required: true
+ next:
+ type: free
+ required: true
+ next:
+ type: url
+ - *ptag
+ - *rtag
+ -
+ name: text-track
+ next:
+ type: json
+ required: true
+ next:
+ type: relay
+
+ 40:
+ description: Channel Creation
+ content:
+ type: json
+ tags: []
+
+ 41:
+ description: Channel Metadata
+ content:
+ type: json
+ tags:
+ -
+ name: e
+ next:
+ type: id
+ required: true
+ next:
+ type: relay
+ next:
+ type: constrained
+ either:
+ - root
+
+ 42:
+ description: Channel Message
+ content:
+ type: free
+ tags:
+ -
+ name: e
+ next:
+ type: id
+ required: true
+ next:
+ type: relay
+ next:
+ type: constrained
+ either:
+ - root
+ - reply
+ - *ptag
+
+ 43:
+ description: Channel Hide Message
+ content:
+ type: json
+ tags:
+ -
+ name: e
+ next:
+ type: id
+ required: true
+
+ 44:
+ description: Channel Mute User
+ content:
+ type: json
+ tags:
+ - *ptag
+
+ 64:
+ description: Chess (PGN)
+ content:
+ type: free
+ tags:
+ - *ptag
+
+ 818:
+ description: Wiki merge requests
+ in_use: true
+ content:
+ type: free
+ tags:
+ - *atag
+ - *ptag
+ - *etag
+
+ 1018:
+ description: Poll Response
+ in_use: true
+ content:
+ type: empty
+ tags:
+ - *etag
+ -
+ name: response
+ next:
+ type: free
+ required: true
+
+ 1021:
+ description: Bid
+ content:
+ type: free
+ tags:
+ - *etag
+
+ 1022:
+ description: Bid confirmation
+ content:
+ type: json
+ tags:
+ - *etag
+
+ 1040:
+ description: OpenTimestamps
+ in_use: true
+ content:
+ type: free
+ tags:
+ - *etag
+ - *ktag
+
+ 1222:
+ description: Voice Message
+ in_use: true
+ content:
+ type: free
+ tags: []
+
+ 1244:
+ description: Voice Message Comment
+ in_use: true
+ content:
+ type: free
+ tags:
+ -
+ name: A
+ next: *addr
+ -
+ name: a
+ next: *addr
+ -
+ name: E
+ next: *event
+ -
+ name: e
+ next: *event
+ -
+ name: I
+ next: *external
+ -
+ name: i
+ next: *external
+ -
+ name: K
+ next: *kind
+ -
+ name: K
+ next:
+ type: free
+ required: true
+ - *ktag
+ -
+ name: P
+ next: *profile
+ - *ptag
+
+ 1311:
+ description: Live Chat Message
+ in_use: true
+ content:
+ type: free
+ tags: []
+
+ 1337:
+ description: Code Snippet
+ in_use: true
+ content:
+ type: free
+ tags: []
+
+ 1971:
+ description: Problem Tracker
+ content:
+ type: free
+ tags: []
+
+ 1986:
+ description: Relay reviews
+ content:
+ type: free
+ tags: []
+
+ 1987:
+ description: AI Embeddings / Vector lists
+ content:
+ type: free
+ tags: []
+
+ 2003:
+ description: Torrent
+ in_use: true
+ content:
+ type: free
+ tags:
+ - *titletag
+ -
+ name: x
+ next:
+ type: free
+ required: true
+ -
+ name: file
+ next:
+ type: free
+ required: true
+ next:
+ type: free
+ -
+ name: tracker
+ next:
+ type: url
+ -
+ name: i
+ next:
+ type: free
+
+ 2004:
+ description: Torrent Comment
+ content:
+ type: free
+ tags:
+ - *etag
+
+ 2022:
+ description: Coinjoin Pool
+ content:
+ type: free
+ tags: []
+
+ 4550:
+ description: Community Post Approval
+ content:
+ type: free
+ tags: []
+
+ 7374:
+ description: Reserved Cashu Wallet Tokens
+ in_use: true
+ content:
+ type: free
+ tags: []
+
+ 7375:
+ description: Cashu Wallet Tokens
+ in_use: true
+ content:
+ type: free
+ tags: []
+
+ 7376:
+ description: Cashu Wallet History
+ in_use: true
+ content:
+ type: free
+ tags: []
+
+ 7516:
+ description: Geocache log
+ content:
+ type: free
+ tags: []
+
+ 7517:
+ description: Geocache proof of find
+ content:
+ type: free
+ tags: []
+
+ 9321:
+ description: Nutzap
+ in_use: true
+ content:
+ type: free
+ tags:
+ -
+ name: proof
+ next:
+ type: free
+ required: true
+ variadic: true
+ -
+ name: u
+ next:
+ type: url
+ required: true
+ - *etag-bare
+ - *ktag
+ - *ptag
+
+ 9467:
+ description: Tidal login
+ content:
+ type: free
+ tags: []
+
+ 1059:
+ description: Gift Wrap
+ in_use: true
+ content:
+ type: free
+ tags:
+ - *ptag
+
+ 1063:
+ description: File Metadata
+ content:
+ type: free
+ tags:
+ -
+ name: url
+ next:
+ type: url
+ required: true
+ -
+ name: m
+ next:
+ type: free
+ required: true
+ -
+ name: x
+ next:
+ type: free
+ required: true
+ -
+ name: ox
+ next:
+ type: free
+ required: true
+ -
+ name: size
+ next:
+ type: free
+ required: true
+ -
+ name: dim
+ next:
+ type: free
+ required: true
+ -
+ name: magnet
+ next:
+ type: url
+ required: true
+ -
+ name: i
+ next:
+ type: free
+ required: true
+ -
+ name: blurhash
+ next:
+ type: free
+ required: true
+ -
+ name: thumb
+ next:
+ type: url
+ required: true
+ - *imagetag
+ - *summarytag
+ -
+ name: fallback
+ next:
+ type: url
+ required: true
+ -
+ name: service
+ next:
+ type: free
+ required: true
+
+ 1068:
+ description: Poll
+ in_use: true
+ content:
+ type: free
+ tags:
+ -
+ name: option
+ next:
+ type: free
+ required: true
+ next:
+ type: free
+ required: true
+ - *relaytag
+ -
+ name: polltype
+ next:
+ type: constrained
+ either:
+ - singlechoice
+ - multiplechoice
+ -
+ name: endsAt
+ next:
+ type: free
+ required: true
+
+ 1621:
+ description: Issues
+ content:
+ type: free
+ tags:
+ - *atag
+ - *ptag
+ -
+ name: subject
+ next:
+ type: free
+ required: true
+
+ 1984:
+ description: Reporting
+ in_use: true
+ content:
+ type: free
+ tags:
+ - *ptag
+ - *etag
+ - *atag
+
+ 1985:
+ description: Label
+ content:
+ type: free
+ tags:
+ - *ptag
+ - *etag
+ - *atag
+
+ 9041:
+ description: Zap Goal
+ content:
+ type: free
+ tags:
+ -
+ name: amount
+ next:
+ type: free
+ required: true
+ -
+ name: relays
+ next:
+ type: relay
+ variadic: true
+ required: true
+ -
+ name: closed_at
+ next:
+ type: free
+ - *imagetag
+ - *summarytag
+ - *rtag
+ - *atag
+ -
+ name: zap
+ next:
+ type: pubkey
+ required: true
+ next:
+ type: relay
+ next:
+ type: free
+
+ 9734:
+ description: Zap Request
+ in_use: true
+ content:
+ type: free
+ tags:
+ -
+ name: relays
+ next:
+ type: relay
+ variadic: true
+ required: true
+ -
+ name: amount
+ next:
+ type: free
+ -
+ name: lnurl
+ next:
+ type: free
+ - *ptag
+ - *etag
+ - *atag
+ - *ktag
+
+ 9735:
+ description: Zap
+ in_use: true
+ content:
+ type: empty
+ tags:
+ - *ptag
+ -
+ name: P
+ next: *profile
+ - *etag
+ - *ktag
+ -
+ name: bolt11
+ next:
+ type: free
+ required: true
+ -
+ name: description
+ next:
+ type: json
+ required: true
+ -
+ name: preimage
+ next:
+ type: free
+
+ 1111:
+ description: Comment
+ in_use: true
+ content:
+ type: free
+ tags:
+ -
+ name: A
+ next: *addr
+ - *atag
+ -
+ name: E
+ next: *event
+ - *etag
+ -
+ name: I
+ next: *external
+ -
+ name: i
+ next: *external
+ -
+ name: K
+ next: *kind
+ -
+ name: K
+ next:
+ type: free
+ required: true
+ - *ktag
+ -
+ name: P
+ next: *profile
+ - *ptag
+ - *emojitag
+
+ 10002:
+ description: Relay List Metadata
+ in_use: true
+ content:
+ type: empty
+ tags:
+ -
+ name: r
+ next:
+ type: relay
+ required: true
+ next:
+ type: constrained
+ either:
+ - read
+ - write
+
+ 9802:
+ description: Highlights
+ in_use: true
+ content:
+ type: free
+ tags:
+ - *ptag
+ - *etag
+ - *atag
+ - *rtag
+ -
+ name: context
+ next:
+ type: free
+ -
+ name: comment
+ next:
+ type: free
+
+ 27235:
+ description: HTTP Auth
+ in_use: true
+ content:
+ type: free
+ tags:
+ -
+ name: u
+ next:
+ type: url
+ required: true
+ -
+ name: method
+ next:
+ type: constrained
+ either:
+ - GET
+ - POST
+ - PUT
+ - DELETE
+ - PATCH
+ required: true
+ -
+ name: payload
+ next:
+ type: free
+
+ 10000:
+ description: Mute list
+ in_use: true
+ content:
+ type: free
+ tags:
+ - *ptag
+ - name: word
+ next:
+ type: free
+ required: true
+ - *etag
+
+ 10001:
+ description: Pin list
+ in_use: true
+ content:
+ type: empty
+ tags:
+ - *etag
+
+ 10003:
+ description: Bookmark list
+ in_use: true
+ content:
+ type: free
+ tags:
+ - *etag
+ - *atag
+ - *rtag
+
+ 10004:
+ description: Communities list
+ content:
+ type: free
+ tags:
+ - *atag
+
+ 10005:
+ description: Public chats list
+ content:
+ type: free
+ tags:
+ - *etag
+
+ 10006:
+ description: Blocked relays list
+ content:
+ type: free
+ tags:
+ - *relaytag
+
+ 10007:
+ description: Search relays list
+ in_use: true
+ content:
+ type: free
+ tags:
+ - *relaytag
+
+ 10012:
+ description: Favorite relays list
+ in_use: true
+ content:
+ type: free
+ tags:
+ - *relaytag
+ - *atag
+
+ 10015:
+ description: Interests list
+ in_use: true
+ content:
+ type: free
+ tags:
+ - *atag
+
+ 10020:
+ description: Media follows
+ content:
+ type: free
+ tags:
+ - *ptag
+
+ 10030:
+ description: User emoji list
+ in_use: true
+ content:
+ type: free
+ tags:
+ - *emojitag
+ - *atag
+
+ 10050:
+ description: Relay list to receive DMs
+ in_use: true
+ content:
+ type: empty
+ tags:
+ - *relaytag
+
+ 10101:
+ description: Good wiki authors
+ content:
+ type: empty
+ tags:
+ - *ptag
+
+ 10102:
+ description: Good wiki relays
+ content:
+ type: empty
+ tags:
+ - *relaytag
+
+ 10009:
+ description: User groups
+ in_use: true
+ content:
+ type: empty
+ tags:
+ -
+ name: group
+ next:
+ type: free
+ required: true
+ next:
+ type: relay
+ next:
+ type: free
+
+ 10013:
+ description: Private event relay list
+ content:
+ type: empty
+ tags: []
+
+ 10019:
+ description: Nutzap Mint Recommendation
+ in_use: true
+ content:
+ type: empty
+ tags:
+ - *relaytag
+ -
+ name: mint
+ next:
+ type: url
+ required: true
+ -
+ name: pubkey
+ next:
+ type: pubkey
+ required: true
+
+ 10063:
+ description: Blossom server list
+ in_use: true
+ content:
+ type: empty
+ tags:
+ -
+ name: server
+ next:
+ type: url
+ required: true
+
+ 10096:
+ description: File storage server list
+ content:
+ type: empty
+ tags:
+ -
+ name: server
+ next:
+ type: url
+ required: true
+
+ 10166:
+ description: Relay Monitor Announcement
+ content:
+ type: empty
+ tags: []
+
+ 10312:
+ description: Room Presence
+ content:
+ type: empty
+ tags: []
+
+ 10377:
+ description: Proxy Announcement
+ content:
+ type: empty
+ tags: []
+
+ 11111:
+ description: Transport Method Announcement
+ content:
+ type: empty
+ tags: []
+
+ 13194:
+ description: Wallet Info
+ in_use: true
+ content:
+ type: free
+ tags: []
+
+ 17375:
+ description: Cashu Wallet Event
+ in_use: true
+ content:
+ type: free
+ tags: []
+
+ 21000:
+ description: Lightning Pub RPC
+ content:
+ type: free
+ tags: []
+
+ 22242:
+ description: Client Authentication
+ in_use: true
+ content:
+ type: free
+ tags: []
+
+ 23194:
+ description: Wallet Request
+ content:
+ type: free
+ tags: []
+
+ 23195:
+ description: Wallet Response
+ content:
+ type: free
+ tags: []
+
+ 24133:
+ description: Nostr Connect
+ content:
+ type: free
+ tags: []
+
+ 24242:
+ description: Blobs stored on mediaservers
+ content:
+ type: free
+ tags: []
+
+ 30008:
+ description: Profile Badges
+ in_use: true
+ content:
+ type: empty
+ tags:
+ - *atag
+ - *etag
+
+ 30009:
+ description: Badge Definition
+ in_use: true
+ content:
+ type: free
+ tags:
+ -
+ name: name
+ next:
+ type: free
+ required: true
+ - *descriptiontag
+ - *imagetag
+ -
+ name: thumb
+ next:
+ type: url
+ next:
+ type: free
+
+ 30017:
+ description: Create or update a stall
+ content:
+ type: json
+ tags: []
+
+ 30018:
+ description: Create or update a product
+ content:
+ type: json
+ tags: []
+
+ 30020:
+ description: Product sold as an auction
+ content:
+ type: json
+ tags: []
+
+ 30023:
+ description: Long-form Content
+ in_use: true
+ content:
+ type: free
+ tags:
+ - *titletag
+ - *imagetag
+ - *summarytag
+ - *publishedattag
+ - *etag
+ - *atag
+
+ 30024:
+ description: Draft Long-form Content
+ content:
+ type: free
+ tags:
+ - *titletag
+ - *imagetag
+ - *summarytag
+ - *publishedattag
+ - *etag
+ - *atag
+
+ 30078:
+ description: Application-specific Data
+ in_use: true
+ content:
+ type: free
+ tags: []
+
+ 30315:
+ description: User Statuses
+ content:
+ type: free
+ tags:
+ - *rtag
+ - *ptag
+ - *etag
+ - *atag
+ - *emojitag
+
+
+ 30402:
+ description: Classified Listing
+ content:
+ type: free
+ tags:
+ - *titletag
+ - *summarytag
+ - *publishedattag
+ -
+ name: location
+ next:
+ type: free
+ required: true
+ -
+ name: price
+ next:
+ type: free
+ required: true
+ next:
+ type: free
+ required: true
+ next:
+ type: free
+ -
+ name: status
+ next:
+ type: constrained
+ either:
+ - active
+ - sold
+ - *imagetag
+ -
+ name: g
+ next:
+ type: free
+ - *etag
+ - *atag
+
+ 30403:
+ description: Draft Classified Listing
+ content:
+ type: free
+ tags:
+ - *titletag
+ - *summarytag
+ - *publishedattag
+ -
+ name: location
+ next:
+ type: free
+ required: true
+ -
+ name: price
+ next:
+ type: free
+ required: true
+ next:
+ type: free
+ required: true
+ next:
+ type: free
+ -
+ name: status
+ next:
+ type: constrained
+ either:
+ - active
+ - sold
+ - *imagetag
+ - *gtag
+ - *etag
+ - *atag
+
+ 31922:
+ description: Date-Based Calendar Event
+ in_use: true
+ content:
+ type: free
+ required:
+ - title
+ - start
+ - location
+ tags:
+ - *titletag
+ - *summarytag
+ - *imagetag
+ -
+ name: location
+ next:
+ type: free
+ required: true
+ - *gtag
+ - *ptag
+ -
+ name: start
+ next:
+ type: date
+ required: true
+ -
+ name: end
+ next:
+ type: date
+ required: true
+
+ 31923:
+ description: Time-Based Calendar Event
+ in_use: true
+ content:
+ type: free
+ required:
+ - title
+ - start
+ - location
+ tags:
+ - *titletag
+ - *summarytag
+ - *imagetag
+ -
+ name: location
+ next:
+ type: free
+ - *gtag
+ - *ptag
+ -
+ name: start
+ next:
+ type: timestamp
+ required: true
+ -
+ name: end
+ next:
+ type: timestamp
+ required: true
+ -
+ name: start_tzid
+ next:
+ type: free
+ required: true
+ -
+ name: end_tzid
+ next:
+ type: free
+ required: true
+ 31924:
+ description: Calendar
+ in_use: true
+ content:
+ type: free
+ required:
+ - title
+ tags:
+ - *titletag
+ - *atag
+ - *imagetag
+ - *descriptiontag
+
+ 31925:
+ description: Calendar Event RSVP
+ in_use: true
+ content:
+ type: free
+ required:
+ - a
+ - status
+ tags:
+ - *etag
+ - *atag
+ -
+ name: status
+ next:
+ type: constrained
+ either:
+ - accepted
+ - declined
+ - tentative
+ required: true
+ -
+ name: fb
+ next:
+ type: constrained
+ either:
+ - free
+ - busy
+ - *ptag
+
+ 31989:
+ description: Handler recommendation
+ content:
+ type: empty
+ tags:
+ - *atag
+
+ 31990:
+ description: Handler information
+ content:
+ type: json
+ tags:
+ - *ktag
+ -
+ name: web
+ next:
+ type: url
+ required: true
+ next:
+ type: constrained
+ either:
+ - nevent
+ - nprofile
+ -
+ name: ios
+ next:
+ type: free
+ required: true
+
+ 30617:
+ description: Repository announcements
+ in_use: true
+ content:
+ type: empty
+ tags:
+ -
+ name: name
+ next:
+ type: free
+ required: true
+ - *descriptiontag
+ -
+ name: web
+ next:
+ type: url
+ required: true
+ -
+ name: clone
+ next:
+ type: giturl
+ required: true
+ -
+ name: relays
+ next:
+ type: relay
+ variadic: true
+ -
+ name: r
+ next:
+ type: hex
+ min: 40
+ max: 40
+ required: true
+ next:
+ type: constrained
+ either:
+ - euc
+ required: true
+ -
+ name: maintainers
+ next:
+ type: pubkey
+ variadic: true
+
+ 30618:
+ description: Repository state announcements
+ in_use: true
+ content:
+ type: empty
+ tags:
+ -
+ prefix: "refs/"
+ next:
+ type: hex
+ min: 40
+ max: 40
+ required: true
+ -
+ name: HEAD
+ next:
+ type: free
+
+ 1617:
+ description: Patches
+ in_use: true
+ content:
+ type: free
+ tags:
+ - *atag
+ -
+ name: r
+ next:
+ type: hex
+ min: 40
+ max: 40
+ required: true
+ - *ptag
+ -
+ name: t
+ next:
+ type: constrained
+ either:
+ - root
+ - root-revision
+ required: true
+ -
+ name: commit
+ next:
+ type: hex
+ min: 40
+ max: 40
+ required: true
+ -
+ name: parent-commit
+ next:
+ type: hex
+ min: 40
+ max: 40
+ required: true
+ -
+ name: commit-pgp-sig
+ next:
+ type: free
+ required: true
+ -
+ name: committer
+ next:
+ type: free
+ required: true
+ next:
+ type: free
+ required: true
+ next:
+ type: free
+ required: true
+ next:
+ type: free
+ required: true
+
+ 30000:
+ description: Follow sets
+ in_use: true
+ content:
+ type: empty
+ tags:
+ - *ptag
+ - *titletag
+ - *imagetag
+ - *descriptiontag
+
+ 30002:
+ description: Relay sets
+ in_use: true
+ content:
+ type: empty
+ tags:
+ - *relaytag
+ - *titletag
+ - *imagetag
+ - *descriptiontag
+
+ 30003:
+ description: Bookmark sets
+ content:
+ type: empty
+ tags:
+ - *etag
+ - *atag
+ - *rtag
+ - *titletag
+ - *imagetag
+ - *descriptiontag
+
+ 30004:
+ description: Curation sets
+ content:
+ type: empty
+ tags:
+ - *atag
+ - *etag
+ - *titletag
+ - *imagetag
+ - *descriptiontag
+
+ 30005:
+ description: Video sets
+ content:
+ type: empty
+ tags:
+ - *atag
+ - *titletag
+ - *imagetag
+ - *descriptiontag
+
+ 30007:
+ description: Kind mute sets
+ content:
+ type: empty
+ tags:
+ - *ptag
+ - *titletag
+ - *imagetag
+ - *descriptiontag
+
+ 30015:
+ description: Interest sets
+ content:
+ type: empty
+ tags:
+ - *titletag
+ - *imagetag
+ - *descriptiontag
+
+ 30019:
+ description: Marketplace UI/UX
+ content:
+ type: json
+ tags: []
+
+ 30030:
+ description: Emoji sets
+ in_use: true
+ content:
+ type: empty
+ tags:
+ - *emojitag
+ - *titletag
+ - *imagetag
+ - *descriptiontag
+
+ 30040:
+ description: Curated Publication Index
+ content:
+ type: free
+ tags: []
+
+ 30041:
+ description: Curated Publication Content
+ content:
+ type: free
+ tags: []
+
+ 30063:
+ description: Release artifact sets
+ content:
+ type: empty
+ tags:
+ - *etag
+ - *atag
+ - *titletag
+ - *imagetag
+ - *descriptiontag
+
+ 30166:
+ description: Relay Discovery
+ content:
+ type: empty
+ tags: []
+
+ 30267:
+ description: App curation sets
+ content:
+ type: empty
+ tags:
+ - *atag
+ - *titletag
+ - *imagetag
+ - *descriptiontag
+
+ 30311:
+ description: Live Event
+ in_use: true
+ content:
+ type: free
+ tags: []
+
+ 30312:
+ description: Interactive Room
+ content:
+ type: free
+ tags: []
+
+ 30313:
+ description: Conference Event
+ content:
+ type: free
+ tags: []
+
+ 30388:
+ description: Slide Set
+ content:
+ type: free
+ tags: []
+
+ 30818:
+ description: Wiki article
+ in_use: true
+ content:
+ type: free
+ tags:
+ - *titletag
+ - *summarytag
+ - *atag
+ - *etag
+
+ 30819:
+ description: Wiki Redirects
+ in_use: true
+ content:
+ type: free
+ tags: []
+
+ 31234:
+ description: Draft Event
+ content:
+ type: free
+ tags: []
+
+ 31388:
+ description: Link Set
+ content:
+ type: free
+ tags: []
+
+ 31890:
+ description: Feed
+ content:
+ type: free
+ tags: []
+
+ 32267:
+ description: Software Application
+ content:
+ type: free
+ tags: []
+
+ 34550:
+ description: Community Definition
+ content:
+ type: free
+ tags: []
+
+ 37516:
+ description: Geocache listing
+ content:
+ type: free
+ tags: []
+
+ 38172:
+ description: Cashu Mint Announcement
+ content:
+ type: free
+ tags: []
+
+ 38173:
+ description: Fedimint Announcement
+ content:
+ type: free
+ tags: []
+
+ 38383:
+ description: Peer-to-peer Order events
+ content:
+ type: free
+ tags: []
+
+ 39089:
+ description: Starter packs
+ in_use: true
+ content:
+ type: empty
+ tags:
+ - *ptag
+ - *titletag
+ - *imagetag
+ - *descriptiontag
+
+ 39092:
+ description: Media starter packs
+ content:
+ type: empty
+ tags:
+ - *ptag
+ - *titletag
+ - *imagetag
+ - *descriptiontag
+
+ 39701:
+ description: Web bookmarks
+ in_use: true
+ content:
+ type: free
+ tags: []
diff --git a/src/hooks/useLiveTimeline.ts b/src/hooks/useLiveTimeline.ts
index 8955815..d5664c5 100644
--- a/src/hooks/useLiveTimeline.ts
+++ b/src/hooks/useLiveTimeline.ts
@@ -5,15 +5,15 @@ import { useEventStore, useObservableMemo } from "applesauce-react/hooks";
import { isNostrEvent } from "@/lib/type-guards";
interface UseLiveTimelineOptions {
- limit?: number;
- stream?: boolean;
+ limit?: number;
+ stream?: boolean;
}
interface UseLiveTimelineReturn {
- events: NostrEvent[];
- loading: boolean;
- error: Error | null;
- eoseReceived: boolean;
+ events: NostrEvent[];
+ loading: boolean;
+ error: Error | null;
+ eoseReceived: boolean;
}
/**
@@ -27,103 +27,103 @@ interface UseLiveTimelineReturn {
* @returns Object containing events array (from store, sorted), loading state, and error
*/
export function useLiveTimeline(
- id: string,
- filters: Filter | Filter[],
- relays: string[],
- options: UseLiveTimelineOptions = { limit: 200 },
+ id: string,
+ filters: Filter | Filter[],
+ relays: string[],
+ options: UseLiveTimelineOptions = { limit: 1000 },
): UseLiveTimelineReturn {
- const eventStore = useEventStore();
- const { limit, stream = false } = options;
- const [loading, setLoading] = useState(false);
- const [error, setError] = useState
(null);
- const [eoseReceived, setEoseReceived] = useState(false);
+ const eventStore = useEventStore();
+ const { limit, stream = false } = options;
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState(null);
+ const [eoseReceived, setEoseReceived] = useState(false);
- // Stabilize filters and relays for dependency array
- // Using JSON.stringify and .join() for deep comparison - this is intentional
- // eslint-disable-next-line react-hooks/exhaustive-deps
- const stableFilters = useMemo(() => filters, [JSON.stringify(filters)]);
- // eslint-disable-next-line react-hooks/exhaustive-deps
- const stableRelays = useMemo(() => relays, [relays.join(",")]);
+ // Stabilize filters and relays for dependency array
+ // Using JSON.stringify and .join() for deep comparison - this is intentional
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ const stableFilters = useMemo(() => filters, [JSON.stringify(filters)]);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ const stableRelays = useMemo(() => relays, [relays.join(",")]);
- // 1. Subscription Effect - Fetch data and feed EventStore
- useEffect(() => {
- if (relays.length === 0) {
+ // 1. Subscription Effect - Fetch data and feed EventStore
+ useEffect(() => {
+ if (relays.length === 0) {
+ setLoading(false);
+ return;
+ }
+
+ console.log("LiveTimeline: Starting query", {
+ id,
+ relays,
+ filters,
+ limit,
+ stream,
+ });
+
+ setLoading(true);
+ setError(null);
+ setEoseReceived(false);
+
+ // Normalize filters to array
+ const filterArray = Array.isArray(filters) ? filters : [filters];
+
+ // Add limit to filters if specified
+ const filtersWithLimit = filterArray.map((f) => ({
+ ...f,
+ limit: limit || f.limit,
+ }));
+
+ const observable = pool.subscription(relays, filtersWithLimit, {
+ retries: 5,
+ reconnect: 5,
+ resubscribe: true,
+ eventStore, // Automatically add events to store
+ });
+
+ const subscription = observable.subscribe(
+ (response) => {
+ // Response can be an event or 'EOSE' string
+ if (typeof response === "string") {
+ console.log("LiveTimeline: EOSE received");
+ setEoseReceived(true);
+ if (!stream) {
setLoading(false);
- return;
+ }
+ } else if (isNostrEvent(response)) {
+ // Event automatically added to store by pool.subscription (via options.eventStore)
+ } else {
+ console.warn("LiveTimeline: Unexpected response type:", response);
}
+ },
+ (err: Error) => {
+ console.error("LiveTimeline: Error", err);
+ setError(err);
+ setLoading(false);
+ },
+ () => {
+ // Only set loading to false if not streaming
+ if (!stream) {
+ setLoading(false);
+ }
+ },
+ );
- console.log("LiveTimeline: Starting query", {
- id,
- relays,
- filters,
- limit,
- stream,
- });
-
- setLoading(true);
- setError(null);
- setEoseReceived(false);
-
- // Normalize filters to array
- const filterArray = Array.isArray(filters) ? filters : [filters];
-
- // Add limit to filters if specified
- const filtersWithLimit = filterArray.map((f) => ({
- ...f,
- limit: limit || f.limit,
- }));
-
- const observable = pool.subscription(relays, filtersWithLimit, {
- retries: 5,
- reconnect: 5,
- resubscribe: true,
- eventStore, // Automatically add events to store
- });
-
- const subscription = observable.subscribe(
- (response) => {
- // Response can be an event or 'EOSE' string
- if (typeof response === "string") {
- console.log("LiveTimeline: EOSE received");
- setEoseReceived(true);
- if (!stream) {
- setLoading(false);
- }
- } else if (isNostrEvent(response)) {
- // Event automatically added to store by pool.subscription (via options.eventStore)
- } else {
- console.warn("LiveTimeline: Unexpected response type:", response);
- }
- },
- (err: Error) => {
- console.error("LiveTimeline: Error", err);
- setError(err);
- setLoading(false);
- },
- () => {
- // Only set loading to false if not streaming
- if (!stream) {
- setLoading(false);
- }
- },
- );
-
- return () => {
- subscription.unsubscribe();
- };
- }, [id, stableFilters, stableRelays, limit, stream, eventStore]);
-
- // 2. Observable Effect - Read from EventStore
- const timelineEvents = useObservableMemo(() => {
- // eventStore.timeline returns an Observable that emits sorted array of events matching filter
- // It updates whenever relevant events are added/removed from store
- return eventStore.timeline(filters);
- }, [stableFilters]);
-
- return {
- events: timelineEvents || [],
- loading,
- error,
- eoseReceived,
+ return () => {
+ subscription.unsubscribe();
};
+ }, [id, stableFilters, stableRelays, limit, stream, eventStore]);
+
+ // 2. Observable Effect - Read from EventStore
+ const timelineEvents = useObservableMemo(() => {
+ // eventStore.timeline returns an Observable that emits sorted array of events matching filter
+ // It updates whenever relevant events are added/removed from store
+ return eventStore.timeline(filters);
+ }, [stableFilters]);
+
+ return {
+ events: timelineEvents || [],
+ loading,
+ error,
+ eoseReceived,
+ };
}
diff --git a/src/lib/nostr-schema.test.ts b/src/lib/nostr-schema.test.ts
new file mode 100644
index 0000000..1eb5352
--- /dev/null
+++ b/src/lib/nostr-schema.test.ts
@@ -0,0 +1,219 @@
+import { describe, it, expect } from "vitest";
+import {
+ loadSchema,
+ getKindSchema,
+ getAllKinds,
+ formatTag,
+ parseTagStructure,
+ getContentTypeDescription,
+} from "./nostr-schema";
+
+describe("nostr-schema", () => {
+ describe("loadSchema", () => {
+ it("should load and parse the schema", () => {
+ const schema = loadSchema();
+ expect(schema).toBeDefined();
+ expect(Object.keys(schema).length).toBeGreaterThan(0);
+ });
+
+ it("should parse kind numbers correctly", () => {
+ const schema = loadSchema();
+ expect(schema[0]).toBeDefined(); // Metadata
+ expect(schema[1]).toBeDefined(); // Note
+ expect(schema[3]).toBeDefined(); // Contacts
+ });
+ });
+
+ describe("getKindSchema", () => {
+ it("should get schema for kind 1", () => {
+ const schema = getKindSchema(1);
+ expect(schema).toBeDefined();
+ expect(schema?.description).toBe("Short text note");
+ expect(schema?.in_use).toBe(true);
+ expect(schema?.content?.type).toBe("free");
+ });
+
+ it("should return undefined for unknown kind", () => {
+ const schema = getKindSchema(999999);
+ expect(schema).toBeUndefined();
+ });
+
+ it("should have tags for kind 1", () => {
+ const schema = getKindSchema(1);
+ expect(schema?.tags).toBeDefined();
+ expect(Array.isArray(schema?.tags)).toBe(true);
+ expect(schema?.tags && schema.tags.length > 0).toBe(true);
+ });
+ });
+
+ describe("getAllKinds", () => {
+ it("should return sorted array of kind numbers", () => {
+ const kinds = getAllKinds();
+ expect(Array.isArray(kinds)).toBe(true);
+ expect(kinds.length).toBeGreaterThan(0);
+
+ // Check if sorted
+ for (let i = 1; i < kinds.length; i++) {
+ expect(kinds[i]).toBeGreaterThan(kinds[i - 1]);
+ }
+ });
+ });
+
+ describe("formatTag", () => {
+ it("should format simple tag", () => {
+ const result = formatTag({
+ name: "e",
+ next: {
+ type: "id",
+ required: true,
+ },
+ });
+ expect(result).toBe("#e ");
+ });
+
+ it("should format tag with multiple values", () => {
+ const result = formatTag({
+ name: "p",
+ next: {
+ type: "pubkey",
+ required: true,
+ next: {
+ type: "relay",
+ },
+ },
+ });
+ expect(result).toBe("#p ");
+ });
+
+ it("should indicate variadic tags", () => {
+ const result = formatTag({
+ name: "t",
+ variadic: true,
+ next: {
+ type: "free",
+ required: true,
+ },
+ });
+ expect(result).toBe("#t (multiple)");
+ });
+
+ it("should format constrained types", () => {
+ const result = formatTag({
+ name: "status",
+ next: {
+ type: "constrained",
+ either: ["accepted", "declined"],
+ },
+ });
+ expect(result).toBe("#status ");
+ });
+
+ it("should convert 'free' type to 'text'", () => {
+ const result = formatTag({
+ name: "subject",
+ next: {
+ type: "free",
+ required: true,
+ },
+ });
+ expect(result).toBe("#subject ");
+ });
+ });
+
+ describe("parseTagStructure", () => {
+ it("should parse single value tag", () => {
+ const result = parseTagStructure({
+ name: "e",
+ next: {
+ type: "id",
+ required: true,
+ },
+ });
+ expect(result.primaryValue).toBe("id");
+ expect(result.otherParameters).toEqual([]);
+ });
+
+ it("should parse tag with multiple parameters", () => {
+ const result = parseTagStructure({
+ name: "p",
+ next: {
+ type: "pubkey",
+ required: true,
+ next: {
+ type: "relay",
+ next: {
+ type: "free",
+ },
+ },
+ },
+ });
+ expect(result.primaryValue).toBe("pubkey");
+ expect(result.otherParameters).toEqual([
+ "relay (e.g. wss://grimoire.rocks)",
+ "text",
+ ]);
+ });
+
+ it("should parse tag with constrained values", () => {
+ const result = parseTagStructure({
+ name: "status",
+ next: {
+ type: "constrained",
+ either: ["accepted", "declined", "tentative"],
+ },
+ });
+ expect(result.primaryValue).toBe("accepted | declined | tentative");
+ expect(result.otherParameters).toEqual([]);
+ });
+
+ it("should handle tag with no parameters", () => {
+ const result = parseTagStructure({
+ name: "t",
+ });
+ expect(result.primaryValue).toBe("");
+ expect(result.otherParameters).toEqual([]);
+ });
+
+ it("should show grimoire.rocks example for url parameters", () => {
+ const result = parseTagStructure({
+ name: "r",
+ next: {
+ type: "url",
+ required: true,
+ },
+ });
+ expect(result.primaryValue).toBe("url (e.g. https://grimoire.rocks)");
+ expect(result.otherParameters).toEqual([]);
+ });
+
+ it("should show grimoire.rocks example for relay parameters", () => {
+ const result = parseTagStructure({
+ name: "relay",
+ next: {
+ type: "relay",
+ required: true,
+ },
+ });
+ expect(result.primaryValue).toBe("relay (e.g. wss://grimoire.rocks)");
+ expect(result.otherParameters).toEqual([]);
+ });
+ });
+
+ describe("getContentTypeDescription", () => {
+ it("should describe free content", () => {
+ expect(getContentTypeDescription("free")).toBe(
+ "Free-form text or markdown"
+ );
+ });
+
+ it("should describe json content", () => {
+ expect(getContentTypeDescription("json")).toBe("JSON object");
+ });
+
+ it("should describe empty content", () => {
+ expect(getContentTypeDescription("empty")).toBe(
+ "Empty (no content field)"
+ );
+ });
+ });
+});
diff --git a/src/lib/nostr-schema.ts b/src/lib/nostr-schema.ts
new file mode 100644
index 0000000..c106222
--- /dev/null
+++ b/src/lib/nostr-schema.ts
@@ -0,0 +1,154 @@
+import yaml from "js-yaml";
+import schemaYaml from "@/data/nostr-kinds-schema.yaml?raw";
+
+/**
+ * Nostr event schema types based on the official registry
+ */
+
+export interface TagDefinition {
+ name: string;
+ next?: TagValueDefinition;
+ variadic?: boolean;
+}
+
+export interface TagValueDefinition {
+ type: string;
+ required?: boolean;
+ next?: TagValueDefinition;
+ either?: string[]; // For constrained types
+ variadic?: boolean;
+}
+
+export interface KindSchema {
+ description: string;
+ in_use: boolean;
+ content?: {
+ type: "free" | "json" | "empty";
+ };
+ tags?: TagDefinition[];
+ required?: string[]; // List of required tag names
+}
+
+export type NostrSchema = Record;
+
+let parsedSchema: NostrSchema | null = null;
+
+/**
+ * Parse the YAML schema
+ */
+export function loadSchema(): NostrSchema {
+ if (parsedSchema) return parsedSchema;
+
+ try {
+ const data = yaml.load(schemaYaml) as any;
+ parsedSchema = {};
+
+ // The kinds are nested under a "kinds" key
+ const kindsData = data.kinds || data;
+
+ // Extract kind definitions (filter out anchor definitions starting with _)
+ for (const [key, value] of Object.entries(kindsData)) {
+ if (!key.startsWith("_") && !isNaN(Number(key))) {
+ parsedSchema[Number(key)] = value as KindSchema;
+ }
+ }
+
+ return parsedSchema;
+ } catch (error) {
+ console.error("Failed to parse Nostr schema:", error);
+ return {};
+ }
+}
+
+/**
+ * Get schema for a specific kind
+ */
+export function getKindSchema(kind: number): KindSchema | undefined {
+ const schema = loadSchema();
+ return schema[kind];
+}
+
+/**
+ * Get all available kinds from schema
+ */
+export function getAllKinds(): number[] {
+ const schema = loadSchema();
+ return Object.keys(schema)
+ .map(Number)
+ .sort((a, b) => a - b);
+}
+
+/**
+ * Format tag definition as a readable string
+ */
+export function formatTag(tag: TagDefinition): string {
+ let result = `#${tag.name}`;
+ let current = tag.next;
+ const parts: string[] = [];
+
+ while (current) {
+ if (current.either) {
+ parts.push(`<${current.either.join("|")}>`);
+ } else {
+ // Replace 'free' with 'text' for better readability
+ const type = current.type === "free" ? "text" : current.type;
+ parts.push(`<${type}>`);
+ }
+ current = current.next;
+ }
+
+ if (parts.length > 0) {
+ result += ` ${parts.join(" ")}`;
+ }
+
+ if (tag.variadic) {
+ result += " (multiple)";
+ }
+
+ return result;
+}
+
+/**
+ * Parse tag structure into primary value and other parameters
+ */
+export function parseTagStructure(tag: TagDefinition): {
+ primaryValue: string;
+ otherParameters: string[];
+} {
+ const parts: string[] = [];
+ let current = tag.next;
+
+ while (current) {
+ if (current.either) {
+ parts.push(`${current.either.join(" | ")}`);
+ } else {
+ // Replace 'free' with 'text' for better readability
+ const type = current.type === "free" ? "text" : current.type;
+ parts.push(type);
+ }
+ current = current.next;
+ }
+
+ return {
+ primaryValue: parts[0] || "",
+ otherParameters: parts.slice(1),
+ };
+}
+
+/**
+ * Get content type description
+ */
+export function getContentTypeDescription(
+ contentType: "free" | "json" | "empty"
+): string {
+ switch (contentType) {
+ case "free":
+ return "Free-form text or markdown";
+ case "json":
+ return "JSON object";
+ case "empty":
+ return "Empty (no content field)";
+ default:
+ return "Unknown";
+ }
+}
diff --git a/src/types/man.ts b/src/types/man.ts
index 938a9d2..45602df 100644
--- a/src/types/man.ts
+++ b/src/types/man.ts
@@ -39,7 +39,7 @@ export const manPages: Record = {
"nip 19 View the bech32 encoding specification",
"nip b0 View the NIP-B0 specification",
],
- seeAlso: ["feed", "kind"],
+ seeAlso: ["nips", "kind", "kinds"],
appId: "nip",
category: "Documentation",
argParser: (args: string[]) => {
@@ -67,7 +67,7 @@ export const manPages: Record = {
"kind 0 View metadata event kind",
"kind 1 View short text note kind",
],
- seeAlso: ["nip"],
+ seeAlso: ["kinds", "nip", "nips"],
appId: "kind",
category: "Documentation",
argParser: (args: string[]) => ({ number: args[0] || "1" }),
@@ -79,10 +79,6 @@ export const manPages: Record = {
synopsis: "help",
description:
"Display general help information about Grimoire and available commands.",
- examples: [
- "Use man to view detailed documentation",
- "Click any command to open its man page",
- ],
seeAlso: ["man", "nip", "kind"],
appId: "man",
category: "System",
@@ -137,7 +133,7 @@ export const manPages: Record = {
},
],
examples: [
- "man feed View the feed command manual",
+ "man req View the req command manual",
"man nip View the nip command manual",
],
seeAlso: ["help"],
@@ -223,33 +219,32 @@ export const manPages: Record = {
},
],
examples: [
- "req -k 1 -l 20 Get 20 recent notes (auto-selects optimal relays via NIP-65)",
- "req -k 1,3,7 -l 50 Get notes, contact lists, and reactions",
- "req -k 0 -a npub1... Get profile (queries author's outbox relays)",
- "req -k 1 -a user@domain.com Get notes from NIP-05 identifier",
- "req -k 1 -a dergigi.com Get notes from bare domain (resolves to _@dergigi.com)",
- "req -k 1 -a npub1...,npub2... Get notes from multiple authors (balances across outbox relays)",
- "req -a $me Get all your events (queries your outbox relays)",
- "req -k 1 -a $contacts --since 24h Get notes from contacts (queries their outbox relays)",
- "req -k 1 -a $contacts --since 7d Get notes from contacts in last week",
- "req -k 1 -a $contacts --since 3mo Get notes from contacts in last 3 months",
- "req -k 1 -a $contacts --since 1y Get notes from contacts in last year",
- "req -p $me -k 1,7 Get replies and reactions to you (queries your inbox relays)",
- "req -k 1 -a $me -a $contacts Get notes from you and contacts",
- "req -k 9735 -p $me --since 7d Get zaps you received (queries your inbox)",
- "req -k 9735 -P $me --since 7d Get zaps you sent",
- "req -k 9735 -P $contacts Get zaps sent by your contacts",
- "req -k 1 -p verbiricha@habla.news Get notes mentioning user (queries their inbox)",
- "req -k 1 --since 1h relay.damus.io Get notes from last hour (manual relay override)",
- "req -k 1 --since 7d --until now Get notes from last week up to now",
- "req -k 1 --close-on-eose Get recent notes and close after EOSE",
- "req -t nostr,bitcoin -l 50 Get 50 events tagged #nostr or #bitcoin",
- "req --tag a 30023:abc...:article Get events referencing addressable event (#a tag)",
- "req -T r https://example.com Get events referencing URL (#r tag)",
- "req -k 30023 --tag d article1,article2 Get specific replaceable events by d-tag",
- "req --tag g geohash123 -l 20 Get 20 events with geolocation tag",
- "req --search bitcoin -k 1 Search notes for 'bitcoin'",
- "req -k 1 relay1.com relay2.com Query specific relays (overrides auto-selection)",
+ "req -k 1 -l 20 Get 20 recent notes (auto-selects optimal relays via NIP-65)",
+ "req -k 1,3,7 -l 50 Get notes, contact lists, and reactions",
+ "req -k 0 -a fiatjaf.com Get profile (queries author's outbox relays)",
+ "req -k 1 -a verbiricha@habla.news Get notes from NIP-05 identifier",
+ "req -k 1 -a dergigi.com Get notes from bare domain (resolves to _@dergigi.com)",
+ "req -k 1 -a fiatjaf.com,dergigi.com Get notes from multiple authors (balances across outbox relays)",
+ "req -a $me Get all your events (queries your outbox relays)",
+ "req -k 1 -a $contacts --since 24h Get notes from contacts (queries their outbox relays)",
+ "req -k 1 -a $contacts --since 7d Get notes from contacts in last week",
+ "req -k 1 -a $contacts --since 3mo Get notes from contacts in last 3 months",
+ "req -k 1 -a $contacts --since 1y Get notes from contacts in last year",
+ "req -p $me -k 1,7 Get replies and reactions to you (queries your inbox relays)",
+ "req -k 1 -a $me -a $contacts Get notes from you and contacts",
+ "req -k 9735 -p $me --since 7d Get zaps you received (queries your inbox)",
+ "req -k 9735 -P $me --since 7d Get zaps you sent",
+ "req -k 9735 -P $contacts Get zaps sent by your contacts",
+ "req -k 1 -p verbiricha@habla.news Get notes mentioning user (queries their inbox)",
+ "req -k 1 --since 1h relay.damus.io Get notes from last hour (manual relay override)",
+ "req -k 1 --since 7d --until now Get notes from last week up to now",
+ "req -k 1 --close-on-eose Get recent notes and close after EOSE",
+ "req -t nostr,grimoire,bitcoin -l 50 Get 50 events tagged #nostr, #grimoire, or #bitcoin",
+ "req --tag a 30023:7fa56f5d6962ab1e3cd424e758c3002b8665f7b0d8dcee9fe9e288d7751ac194:grimoire Get events referencing addressable event (#a tag)",
+ "req -T r grimoire.rocks Get events referencing URL (#r tag)",
+ "req -k 30023 --tag d badges,grimoire Get specific replaceable events by d-tag",
+ "req --search bitcoin -k 1 Search notes for 'bitcoin'",
+ "req -k 1 theforest.nostr1.com relay.damus.io Query specific relays (overrides auto-selection)",
],
seeAlso: ["kind", "nip"],
appId: "req",
@@ -323,11 +318,9 @@ export const manPages: Record = {
},
],
examples: [
- "open note1abc... Open event by note1 ID",
- "open nevent1xyz... Open event with relay hints",
- "open naddr1def... Open addressable event",
- "open abc123... Open event by hex ID (64 chars)",
- "open 30023:abc123...:my-article Open by address pointer (kind:pubkey:d-tag)",
+ "open nevent1qgs8lft0t45k92c78n2zfe6ccvqzhpn977cd3h8wnl579zxhw5dvr9qqyz4nf2hlglhzhezygl5x2fdsg332fyd9q0p8ja7kvn0g53e0edzyxa32zg8 Open event with relay hints",
+ "open naddr1qvzqqqrkvupzpn6956apxcad0mfp8grcuugdysg44eepex68h50t73zcathmfs49qy88wumn8ghj7mn0wvhxcmmv9uq3wamnwvaz7tmjv4kxz7fwdehhxarj9e3xzmny9uq3wamnwvaz7tmjv4kxz7fwwpexjmtpdshxuet59uq3qamnwvaz7tmwdaehgu3wd4hk6tcpz9mhxue69uhkummnw3ezuamfdejj7qghwaehxw309a3xjarrda5kuetj9eek7cmfv9kz7qg4waehxw309aex2mrp0yhxgctdw4eju6t09uq3samnwvaz7tmxd9k8getj9ehx7um5wgh8w6twv5hszymhwden5te0danxvcmgv95kutnsw43z7qgawaehxw309ahx7um5wghxy6t5vdhkjmn9wgh8xmmrd9skctcpr9mhxue69uhhyetvv9ujuumwdae8gtnnda3kjctv9uqsuamnwvaz7tmev9382tndv5hsz9nhwden5te0wfjkccte9e3k76twdaeju6t09uq3vamnwvaz7tmjv4kxz7fwxvuns6np9eu8j730qqjr2vehvyenvdtr94nrzetr956rgctr94skvvfs95eryep3x3snwve389nxy97cjwx Open addressable event",
+ "open 30023:7fa56f5d6962ab1e3cd424e758c3002b8665f7b0d8dcee9fe9e288d7751ac194:grimoire Open by address pointer (kind:pubkey:d-tag)",
],
seeAlso: ["req", "kind"],
appId: "open",
@@ -350,12 +343,11 @@ export const manPages: Record = {
},
],
examples: [
- "profile npub1abc... Open profile by npub",
- "profile nprofile1xyz... Open profile with relay hints",
- "profile abc123... Open profile by hex pubkey (64 chars)",
- "profile user@domain.com Open profile by NIP-05 identifier",
- "profile jack@cash.app Open profile using NIP-05",
+ "profile fiatjaf.com Open profile by NIP-05 identifier",
+ "profile nprofile1qyd8wumn8ghj7urewfsk66ty9enxjct5dfskvtnrdakj7qgmwaehxw309a6xsetxdaex2um59ehx7um5wgcjucm0d5hsz9mhwden5te0veex2mnn9ehx7um5wgcjucm0d5hszxrhwden5te0ve5kcar9wghxummnw3ezuamfdejj7qpq07jk7htfv243u0x5ynn43scq9wrxtaasmrwwa8lfu2ydwag6cx2q0al9p4 Open profile with relay hints",
+ "profile 3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d Open profile by hex pubkey (64 chars)",
"profile dergigi.com Open profile by domain (resolves to _@dergigi.com)",
+ "profile jack@cash.app Open profile using NIP-05",
],
seeAlso: ["open", "req"],
appId: "profile",
@@ -391,11 +383,11 @@ export const manPages: Record = {
},
],
examples: [
- "encode npub abc123... Encode pubkey to npub",
- "encode nprofile abc123... --relay wss://relay.example.com",
- "encode note def456... Encode event ID to note",
- "encode nevent def456... --relay wss://relay.example.com --author abc123...",
- "encode naddr 30023:abc123...:article --relay wss://relay.example.com",
+ "encode npub 3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d Encode pubkey to npub",
+ "encode nprofile 3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d --relay wss://theforest.nostr1.com Encode profile with relay",
+ "encode note 5c83da77af1dec6d7289834998ad7aafbd9e2191396d75ec3cc27f5a77226f36 Encode event ID to note",
+ "encode nevent 5c83da77af1dec6d7289834998ad7aafbd9e2191396d75ec3cc27f5a77226f36 --relay wss://theforest.nostr1.com --author 3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d Encode event with metadata",
+ "encode naddr 30023:3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d:my-article --relay wss://theforest.nostr1.com Encode addressable event",
],
seeAlso: ["decode"],
appId: "encode",
@@ -417,10 +409,9 @@ export const manPages: Record = {
},
],
examples: [
- "decode npub1abc... Decode npub to hex pubkey",
- "decode nevent1xyz... Decode nevent showing ID, relays, author",
- "decode naddr1def... Decode naddr showing kind, pubkey, identifier",
- "decode nprofile1ghi... Decode nprofile with relay hints",
+ "decode nprofile1qyd8wumn8ghj7urewfsk66ty9enxjct5dfskvtnrdakj7qgmwaehxw309a6xsetxdaex2um59ehx7um5wgcjucm0d5hsz9mhwden5te0veex2mnn9ehx7um5wgcjucm0d5hszxrhwden5te0ve5kcar9wghxummnw3ezuamfdejj7qpq07jk7htfv243u0x5ynn43scq9wrxtaasmrwwa8lfu2ydwag6cx2q0al9p4 Decode nprofile with relay hints",
+ "decode nevent1qgs8lft0t45k92c78n2zfe6ccvqzhpn977cd3h8wnl579zxhw5dvr9qqyz4nf2hlglhzhezygl5x2fdsg332fyd9q0p8ja7kvn0g53e0edzyxa32zg8 Decode nevent showing ID, relays, author",
+ "decode naddr1qvzqqqrkvupzpn6956apxcad0mfp8grcuugdysg44eepex68h50t73zcathmfs49qy88wumn8ghj7mn0wvhxcmmv9uq3wamnwvaz7tmjv4kxz7fwdehhxarj9e3xzmny9uq3wamnwvaz7tmjv4kxz7fwwpexjmtpdshxuet59uq3qamnwvaz7tmwdaehgu3wd4hk6tcpz9mhxue69uhkummnw3ezuamfdejj7qghwaehxw309a3xjarrda5kuetj9eek7cmfv9kz7qg4waehxw309aex2mrp0yhxgctdw4eju6t09uq3samnwvaz7tmxd9k8getj9ehx7um5wgh8w6twv5hszymhwden5te0danxvcmgv95kutnsw43z7qgawaehxw309ahx7um5wghxy6t5vdhkjmn9wgh8xmmrd9skctcpr9mhxue69uhhyetvv9ujuumwdae8gtnnda3kjctv9uqsuamnwvaz7tmev9382tndv5hsz9nhwden5te0wfjkccte9e3k76twdaeju6t09uq3vamnwvaz7tmjv4kxz7fwxvuns6np9eu8j730qqjr2vehvyenvdtr94nrzetr956rgctr94skvvfs95eryep3x3snwve389nxy97cjwx Decode naddr showing kind, pubkey, identifier",
],
seeAlso: ["encode"],
appId: "decode",
@@ -444,7 +435,6 @@ export const manPages: Record = {
],
examples: [
"relay wss://relay.damus.io View relay information",
- "relay relay.primal.net Auto-adds wss:// protocol",
"relay nos.lol View relay capabilities",
],
seeAlso: ["req", "profile"],