Add ID filtering to REQ command (#202)

* feat: add -i/--id flag for direct event ID filtering in REQ command

Add a new -i/--id flag for direct event lookup via filter.ids, and clarify
-e flag behavior for tag-based filtering (#e/#a tags).

Changes:
- Add -i/--id flag: accepts note1, nevent, or hex event IDs for direct lookup
- Clarify -e flag: now always routes to #e/#a tags (including nevent)
- Update man page with new flag documentation and examples
- Add comprehensive tests for the new -i/--id flag

This aligns with nak's behavior where -i is for direct ID filtering and
-e is for tag-based event references.

* feat: support raw coordinate format (kind:pubkey:d) in -e flag

The -e flag now accepts raw a-tag coordinates like `30023:pubkey:article`
in addition to naddr bech32 encoding. Both route to #a tag filtering.

Examples:
- req -e 30023:abc123...:my-article    # Raw coordinate
- req -e naddr1...                      # Bech32 encoded (same effect)

* feat: add event ID previews in REQ viewer query dropdown

When using -i/--id flag for direct event lookup, the query dropdown now
shows clickable event ID previews (truncated hex).

Click any ID to open the event detail view.

Works in both accordion (complex queries) and simple card views.

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Alejandro
2026-01-22 16:14:55 +01:00
committed by GitHub
parent 93ffd365f5
commit 7c6014378b
4 changed files with 509 additions and 37 deletions

View File

@@ -18,6 +18,7 @@ import {
Sparkles,
Link as LinkIcon,
Check,
Target,
} from "lucide-react";
import { Virtuoso } from "react-virtuoso";
import { useReqTimelineEnhanced } from "@/hooks/useReqTimelineEnhanced";
@@ -90,6 +91,27 @@ const MemoizedFeedEvent = memo(
(prev, next) => prev.event.id === next.event.id,
);
/**
* Compact event ID display in query dropdown
* Shows truncated ID with click to open
*/
function EventIdPreview({ eventId }: { eventId: string }) {
const { addWindow } = useGrimoire();
const handleClick = useCallback(() => {
addWindow("open", { pointer: { id: eventId } });
}, [eventId, addWindow]);
return (
<code
className="text-xs font-mono cursor-crosshair hover:text-primary transition-colors"
onClick={handleClick}
>
{eventId.slice(0, 8)}...{eventId.slice(-4)}
</code>
);
}
interface ReqViewerProps {
filter: NostrFilter;
relays?: string[];
@@ -120,11 +142,15 @@ function QueryDropdown({
const { copy: handleCopy, copied } = useCopy();
// Expandable lists state
const [showAllIds, setShowAllIds] = useState(false);
const [showAllAuthors, setShowAllAuthors] = useState(false);
const [showAllPTags, setShowAllPTags] = useState(false);
const [showAllETags, setShowAllETags] = useState(false);
const [showAllTTags, setShowAllTTags] = useState(false);
// Get IDs for direct lookup (from -i flag)
const eventIds = filter.ids || [];
// Get pubkeys for authors and #p tags
const authorPubkeys = filter.authors || [];
const pTagPubkeys = filter["#p"] || [];
@@ -154,6 +180,7 @@ function QueryDropdown({
// Determine if we should use accordion for complex queries
const isComplexQuery =
(filter.kinds?.length || 0) +
eventIds.length +
authorPubkeys.length +
(filter.search ? 1 : 0) +
tagCount >
@@ -170,6 +197,7 @@ function QueryDropdown({
type="multiple"
defaultValue={[
"kinds",
"ids",
"authors",
"mentions",
"time",
@@ -203,6 +231,35 @@ function QueryDropdown({
</AccordionItem>
)}
{/* IDs Section (direct event lookup) */}
{eventIds.length > 0 && (
<AccordionItem value="ids" className="border-0">
<AccordionTrigger className="py-2 hover:no-underline">
<div className="flex items-center gap-2 text-xs font-semibold">
<Target className="size-3.5 text-muted-foreground" />
Event IDs ({eventIds.length})
</div>
</AccordionTrigger>
<AccordionContent>
<div className="space-y-1 ml-5">
{eventIds
.slice(0, showAllIds ? undefined : 3)
.map((eventId) => (
<EventIdPreview key={eventId} eventId={eventId} />
))}
{eventIds.length > 3 && (
<button
onClick={() => setShowAllIds(!showAllIds)}
className="text-xs text-primary hover:underline"
>
{showAllIds ? "Show less" : `Show all ${eventIds.length}`}
</button>
)}
</div>
</AccordionContent>
</AccordionItem>
)}
{/* Time Range Section */}
{(filter.since || filter.until) && (
<AccordionItem value="time" className="border-0">
@@ -469,6 +526,31 @@ function QueryDropdown({
</div>
)}
{/* Event IDs (direct lookup) */}
{eventIds.length > 0 && (
<div className="">
<div className="flex items-center gap-2 text-xs font-semibold mb-1.5">
<Target className="size-3.5 text-muted-foreground" />
Event IDs ({eventIds.length})
</div>
<div className="ml-5 space-y-1">
{eventIds
.slice(0, showAllIds ? undefined : 3)
.map((eventId) => (
<EventIdPreview key={eventId} eventId={eventId} />
))}
{eventIds.length > 3 && (
<button
onClick={() => setShowAllIds(!showAllIds)}
className="text-xs text-primary hover:underline"
>
{showAllIds ? "Show less" : `Show all ${eventIds.length}`}
</button>
)}
</div>
</div>
)}
{/* Time Range */}
{(filter.since || filter.until) && (
<div className="">

View File

@@ -216,8 +216,8 @@ describe("parseReqCommand", () => {
});
describe("event ID flag (-e) with nevent/naddr support", () => {
describe("nevent support", () => {
it("should parse nevent and populate filter.ids", () => {
describe("nevent support (tag filtering)", () => {
it("should parse nevent and populate filter['#e'] (tag filtering)", () => {
const eventId =
"3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d";
const nevent = nip19.neventEncode({
@@ -225,10 +225,10 @@ describe("parseReqCommand", () => {
});
const result = parseReqCommand(["-e", nevent]);
expect(result.filter.ids).toBeDefined();
expect(result.filter.ids).toHaveLength(1);
expect(result.filter.ids).toEqual([eventId]);
expect(result.filter["#e"]).toBeUndefined();
expect(result.filter["#e"]).toBeDefined();
expect(result.filter["#e"]).toHaveLength(1);
expect(result.filter["#e"]).toEqual([eventId]);
expect(result.filter.ids).toBeUndefined();
});
it("should extract relay hints from nevent", () => {
@@ -267,7 +267,7 @@ describe("parseReqCommand", () => {
});
const result = parseReqCommand(["-e", nevent]);
expect(result.filter.ids).toHaveLength(1);
expect(result.filter["#e"]).toHaveLength(1);
expect(result.relays).toBeUndefined();
});
});
@@ -363,8 +363,95 @@ describe("parseReqCommand", () => {
});
});
describe("raw coordinate support (kind:pubkey:d)", () => {
it("should parse raw coordinate and populate filter['#a']", () => {
const pubkey = "a".repeat(64);
const coordinate = `30023:${pubkey}:my-article`;
const result = parseReqCommand(["-e", coordinate]);
expect(result.filter["#a"]).toBeDefined();
expect(result.filter["#a"]).toHaveLength(1);
expect(result.filter["#a"]).toEqual([coordinate]);
expect(result.filter["#e"]).toBeUndefined();
});
it("should normalize pubkey to lowercase", () => {
const pubkey = "A".repeat(64);
const coordinate = `30023:${pubkey}:my-article`;
const result = parseReqCommand(["-e", coordinate]);
expect(result.filter["#a"]).toEqual([
`30023:${"a".repeat(64)}:my-article`,
]);
});
it("should handle empty d-tag identifier", () => {
const pubkey = "a".repeat(64);
const coordinate = `30023:${pubkey}:`;
const result = parseReqCommand(["-e", coordinate]);
expect(result.filter["#a"]).toEqual([coordinate]);
});
it("should handle d-tag with special characters", () => {
const pubkey = "a".repeat(64);
const coordinate = `30023:${pubkey}:my-article/with:special-chars`;
const result = parseReqCommand(["-e", coordinate]);
expect(result.filter["#a"]).toEqual([coordinate]);
});
it("should handle different kind numbers", () => {
const pubkey = "a".repeat(64);
const result = parseReqCommand([
"-e",
`0:${pubkey}:,30000:${pubkey}:list,30023:${pubkey}:article`,
]);
expect(result.filter["#a"]).toHaveLength(3);
expect(result.filter["#a"]).toContain(`0:${pubkey}:`);
expect(result.filter["#a"]).toContain(`30000:${pubkey}:list`);
expect(result.filter["#a"]).toContain(`30023:${pubkey}:article`);
});
it("should combine with naddr coordinates", () => {
const pubkey = "a".repeat(64);
const rawCoord = `30023:${pubkey}:raw-article`;
const naddr = nip19.naddrEncode({
kind: 30023,
pubkey: pubkey,
identifier: "encoded-article",
});
const result = parseReqCommand(["-e", `${rawCoord},${naddr}`]);
expect(result.filter["#a"]).toHaveLength(2);
expect(result.filter["#a"]).toContain(rawCoord);
expect(result.filter["#a"]).toContain(
`30023:${pubkey}:encoded-article`,
);
});
it("should ignore invalid coordinate formats", () => {
// Missing parts
const result1 = parseReqCommand(["-e", "30023:abc"]);
expect(result1.filter["#a"]).toBeUndefined();
// Invalid pubkey (not 64 hex chars)
const result2 = parseReqCommand(["-e", "30023:abc123:article"]);
expect(result2.filter["#a"]).toBeUndefined();
// Invalid kind (not a number)
const result3 = parseReqCommand([
"-e",
`abc:${"a".repeat(64)}:article`,
]);
expect(result3.filter["#a"]).toBeUndefined();
});
});
describe("mixed format support", () => {
it("should handle comma-separated mix of all formats", () => {
it("should handle comma-separated mix of all formats (all to tags)", () => {
const hex = "a".repeat(64);
const eventId =
"3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d";
@@ -383,14 +470,13 @@ describe("parseReqCommand", () => {
`${hex},${note},${nevent},${naddr}`,
]);
// hex and note should go to filter["#e"]
// hex, note, and nevent all go to filter["#e"] (deduplicated: eventId appears twice)
expect(result.filter["#e"]).toHaveLength(2);
expect(result.filter["#e"]).toContain(hex);
expect(result.filter["#e"]).toContain(eventId);
// nevent should go to filter.ids
expect(result.filter.ids).toHaveLength(1);
expect(result.filter.ids).toContain(eventId);
// No direct ID lookup for -e flag
expect(result.filter.ids).toBeUndefined();
// naddr should go to filter["#a"]
expect(result.filter["#a"]).toHaveLength(1);
@@ -408,8 +494,9 @@ describe("parseReqCommand", () => {
const result = parseReqCommand(["-e", `${nevent1},${nevent2}`]);
// Both nevent decode to same event ID, should deduplicate
expect(result.filter.ids).toHaveLength(1);
// Both nevent decode to same event ID, should deduplicate in #e
expect(result.filter["#e"]).toHaveLength(1);
expect(result.filter.ids).toBeUndefined();
});
it("should collect relay hints from mixed formats", () => {
@@ -447,10 +534,12 @@ describe("parseReqCommand", () => {
const result = parseReqCommand(["-e", `${nevent1},${hex}`]);
// nevent goes to filter.ids
expect(result.filter.ids).toHaveLength(1);
// hex goes to filter["#e"]
// Both nevent and hex go to filter["#e"]
expect(result.filter["#e"]).toHaveLength(2);
expect(result.filter["#e"]).toContain(eventId);
expect(result.filter["#e"]).toContain(hex);
// No direct ID lookup for -e flag
expect(result.filter.ids).toBeUndefined();
// relays extracted from nevent
expect(result.relays).toBeDefined();
expect(result.relays).toContain("wss://relay.damus.io/");
@@ -499,7 +588,8 @@ describe("parseReqCommand", () => {
const result = parseReqCommand(["-k", "1", "-e", nevent]);
expect(result.filter.kinds).toEqual([1]);
expect(result.filter.ids).toHaveLength(1);
expect(result.filter["#e"]).toHaveLength(1);
expect(result.filter.ids).toBeUndefined();
});
it("should work with explicit relays", () => {
@@ -539,13 +629,199 @@ describe("parseReqCommand", () => {
expect(result.filter.kinds).toEqual([1]);
expect(result.filter.authors).toEqual([hex]);
expect(result.filter.ids).toHaveLength(1);
expect(result.filter["#e"]).toHaveLength(1);
expect(result.filter.ids).toBeUndefined();
expect(result.filter.since).toBeDefined();
expect(result.filter.limit).toBe(50);
});
});
});
describe("direct ID lookup flag (-i, --id)", () => {
describe("basic parsing", () => {
it("should parse hex event ID to filter.ids", () => {
const hex = "a".repeat(64);
const result = parseReqCommand(["-i", hex]);
expect(result.filter.ids).toEqual([hex]);
});
it("should parse note to filter.ids", () => {
const eventId =
"3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d";
const note = nip19.noteEncode(eventId);
const result = parseReqCommand(["-i", note]);
expect(result.filter.ids).toEqual([eventId]);
});
it("should parse nevent to filter.ids", () => {
const eventId =
"3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d";
const nevent = nip19.neventEncode({ id: eventId });
const result = parseReqCommand(["-i", nevent]);
expect(result.filter.ids).toEqual([eventId]);
});
it("should handle --id long form", () => {
const hex = "a".repeat(64);
const result = parseReqCommand(["--id", hex]);
expect(result.filter.ids).toEqual([hex]);
});
});
describe("comma-separated values", () => {
it("should parse comma-separated hex IDs", () => {
const hex1 = "a".repeat(64);
const hex2 = "b".repeat(64);
const result = parseReqCommand(["-i", `${hex1},${hex2}`]);
expect(result.filter.ids).toEqual([hex1, hex2]);
});
it("should parse comma-separated mixed formats", () => {
const hex = "a".repeat(64);
const eventId =
"3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d";
const note = nip19.noteEncode(eventId);
const nevent = nip19.neventEncode({ id: eventId });
const result = parseReqCommand(["-i", `${hex},${note},${nevent}`]);
// hex is unique, note and nevent decode to same eventId (deduplicated)
expect(result.filter.ids).toHaveLength(2);
expect(result.filter.ids).toContain(hex);
expect(result.filter.ids).toContain(eventId);
});
it("should deduplicate IDs", () => {
const hex = "a".repeat(64);
const result = parseReqCommand(["-i", `${hex},${hex}`]);
expect(result.filter.ids).toEqual([hex]);
});
});
describe("relay hints", () => {
it("should extract relay hints from nevent", () => {
const eventId =
"3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d";
const nevent = nip19.neventEncode({
id: eventId,
relays: ["wss://relay.damus.io"],
});
const result = parseReqCommand(["-i", nevent]);
expect(result.filter.ids).toEqual([eventId]);
expect(result.relays).toBeDefined();
expect(result.relays).toContain("wss://relay.damus.io/");
});
it("should normalize relay URLs from nevent", () => {
const eventId =
"3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d";
const nevent = nip19.neventEncode({
id: eventId,
relays: ["wss://relay.damus.io"],
});
const result = parseReqCommand(["-i", nevent]);
result.relays?.forEach((url) => {
expect(url).toMatch(/^wss?:\/\//);
expect(url).toMatch(/\/$/);
});
});
it("should collect relay hints from multiple nevents", () => {
const eventId1 =
"3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d";
const eventId2 = "b".repeat(64);
const nevent1 = nip19.neventEncode({
id: eventId1,
relays: ["wss://relay.damus.io"],
});
const nevent2 = nip19.neventEncode({
id: eventId2,
relays: ["wss://nos.lol"],
});
const result = parseReqCommand(["-i", `${nevent1},${nevent2}`]);
expect(result.relays).toBeDefined();
expect(result.relays).toContain("wss://relay.damus.io/");
expect(result.relays).toContain("wss://nos.lol/");
});
});
describe("error handling", () => {
it("should ignore invalid bech32", () => {
const result = parseReqCommand(["-i", "note1invalid"]);
expect(result.filter.ids).toBeUndefined();
});
it("should ignore invalid nevent", () => {
const result = parseReqCommand(["-i", "nevent1invalid"]);
expect(result.filter.ids).toBeUndefined();
});
it("should ignore naddr (not valid for direct ID lookup)", () => {
const pubkey = "b".repeat(64);
const naddr = nip19.naddrEncode({
kind: 30023,
pubkey: pubkey,
identifier: "test-article",
});
const result = parseReqCommand(["-i", naddr]);
expect(result.filter.ids).toBeUndefined();
});
it("should skip empty values", () => {
const hex = "a".repeat(64);
const result = parseReqCommand(["-i", `${hex},,`]);
expect(result.filter.ids).toEqual([hex]);
});
it("should continue parsing after invalid values", () => {
const hex1 = "a".repeat(64);
const hex2 = "b".repeat(64);
const result = parseReqCommand(["-i", `${hex1},invalid,${hex2}`]);
expect(result.filter.ids).toEqual([hex1, hex2]);
});
});
describe("integration with other flags", () => {
it("should work alongside -e flag (both IDs and tags)", () => {
const directId = "a".repeat(64);
const tagEventId = "b".repeat(64);
const result = parseReqCommand(["-i", directId, "-e", tagEventId]);
expect(result.filter.ids).toEqual([directId]);
expect(result.filter["#e"]).toEqual([tagEventId]);
});
it("should work with kind and limit", () => {
const hex = "a".repeat(64);
const result = parseReqCommand(["-i", hex, "-k", "1", "-l", "10"]);
expect(result.filter.ids).toEqual([hex]);
expect(result.filter.kinds).toEqual([1]);
expect(result.filter.limit).toBe(10);
});
it("should work with relays", () => {
const hex = "a".repeat(64);
const result = parseReqCommand(["-i", hex, "wss://relay.example.com"]);
expect(result.filter.ids).toEqual([hex]);
expect(result.relays).toContain("wss://relay.example.com/");
});
it("should accumulate across multiple -i flags", () => {
const hex1 = "a".repeat(64);
const hex2 = "b".repeat(64);
const result = parseReqCommand(["-i", hex1, "-i", hex2]);
expect(result.filter.ids).toEqual([hex1, hex2]);
});
});
});
describe("pubkey tag flag (-p)", () => {
it("should parse hex pubkey for #p tag", () => {
const hex = "a".repeat(64);

View File

@@ -51,7 +51,7 @@ function parseCommaSeparated<T>(
/**
* Parse REQ command arguments into a Nostr filter
* Supports:
* - Filters: -k (kinds), -a (authors: hex/npub/nprofile/NIP-05), -l (limit), -e (note/nevent/naddr/hex), -p (#p: hex/npub/nprofile/NIP-05), -P (#P: hex/npub/nprofile/NIP-05), -t (#t), -d (#d), --tag/-T (any #tag)
* - Filters: -k (kinds), -a (authors: hex/npub/nprofile/NIP-05), -l (limit), -i/--id (direct event lookup), -e (tag filtering: #e/#a), -p (#p: hex/npub/nprofile/NIP-05), -P (#P: hex/npub/nprofile/NIP-05), -t (#t), -d (#d), --tag/-T (any #tag)
* - Time: --since, --until
* - Search: --search
* - Relays: wss://relay.com or relay.com (auto-adds wss://), relay hints from nprofile/nevent/naddr are automatically extracted
@@ -183,8 +183,40 @@ export function parseReqCommand(args: string[]): ParsedReqCommand {
break;
}
case "-i":
case "--id": {
// Direct event lookup via filter.ids
// Support comma-separated: -i note1...,nevent1...,hex
if (!nextArg) {
i++;
break;
}
let addedAny = false;
const values = nextArg.split(",").map((v) => v.trim());
for (const val of values) {
if (!val) continue;
const parsed = parseIdIdentifier(val);
if (parsed) {
ids.add(parsed.id);
addedAny = true;
// Collect relay hints from nevent
if (parsed.relays) {
relays.push(...parsed.relays);
}
}
}
i += addedAny ? 2 : 1;
break;
}
case "-e": {
// Support comma-separated event identifiers: -e note1...,nevent1...,naddr1...,hex
// Tag-based filtering: -e note1...,nevent1...,naddr1...,hex
// Events go to #e tag, addresses go to #a tag
if (!nextArg) {
i++;
break;
@@ -198,13 +230,11 @@ export function parseReqCommand(args: string[]): ParsedReqCommand {
const parsed = parseEventIdentifier(val);
if (parsed) {
// Route to appropriate filter field based on type
if (parsed.type === "direct-event") {
ids.add(parsed.value);
} else if (parsed.type === "direct-address") {
aTags.add(parsed.value);
} else if (parsed.type === "tag-event") {
// Route to appropriate tag filter based on type
if (parsed.type === "tag-event") {
eventIds.add(parsed.value);
} else if (parsed.type === "tag-address") {
aTags.add(parsed.value);
}
// Collect relay hints
@@ -564,24 +594,26 @@ function parseNpubOrHex(value: string): {
}
interface ParsedEventIdentifier {
type: "direct-event" | "direct-address" | "tag-event";
type: "tag-event" | "tag-address";
value: string;
relays?: string[];
}
/**
* Parse event identifier - supports note, nevent, naddr, and hex event ID
* Parse event identifier for -e flag (tag filtering)
* All event IDs go to #e, addresses go to #a
* Supports: note, nevent, naddr, and hex event ID
*/
function parseEventIdentifier(value: string): ParsedEventIdentifier | null {
if (!value) return null;
// nevent: direct event lookup with relay hints
// nevent: decode and route to #e tag
if (value.startsWith("nevent")) {
try {
const decoded = nip19.decode(value);
if (decoded.type === "nevent") {
return {
type: "direct-event",
type: "tag-event",
value: decoded.data.id,
relays: decoded.data.relays
?.map((url) => {
@@ -599,14 +631,14 @@ function parseEventIdentifier(value: string): ParsedEventIdentifier | null {
}
}
// naddr: coordinate-based lookup with relay hints
// naddr: coordinate-based lookup with relay hints → #a tag
if (value.startsWith("naddr")) {
try {
const decoded = nip19.decode(value);
if (decoded.type === "naddr") {
const coordinate = `${decoded.data.kind}:${decoded.data.pubkey}:${decoded.data.identifier}`;
return {
type: "direct-address",
type: "tag-address",
value: coordinate,
relays: decoded.data.relays
?.map((url) => {
@@ -624,7 +656,7 @@ function parseEventIdentifier(value: string): ParsedEventIdentifier | null {
}
}
// note1: tag-based filtering (existing behavior)
// note1: decode to event ID → #e tag
if (value.startsWith("note")) {
try {
const decoded = nip19.decode(value);
@@ -639,7 +671,7 @@ function parseEventIdentifier(value: string): ParsedEventIdentifier | null {
}
}
// Hex: tag-based filtering (existing behavior)
// Hex: → #e tag
if (isValidHexEventId(value)) {
return {
type: "tag-event",
@@ -647,5 +679,78 @@ function parseEventIdentifier(value: string): ParsedEventIdentifier | null {
};
}
// Raw coordinate: kind:pubkey:identifier → #a tag
// Format: <kind>:<pubkey>:<d-tag> (e.g., 30023:abc123...:article-name)
const coordinateMatch = value.match(/^(\d+):([a-fA-F0-9]{64}):(.*)$/);
if (coordinateMatch) {
const [, kindStr, pubkey, identifier] = coordinateMatch;
const kind = parseInt(kindStr, 10);
if (!isNaN(kind)) {
return {
type: "tag-address",
value: `${kind}:${pubkey.toLowerCase()}:${identifier}`,
};
}
}
return null;
}
interface ParsedIdIdentifier {
id: string;
relays?: string[];
}
/**
* Parse event identifier for -i/--id flag (direct ID lookup via filter.ids)
* Supports: note, nevent, and hex event ID
*/
function parseIdIdentifier(value: string): ParsedIdIdentifier | null {
if (!value) return null;
// nevent: decode and extract event ID
if (value.startsWith("nevent")) {
try {
const decoded = nip19.decode(value);
if (decoded.type === "nevent") {
return {
id: decoded.data.id,
relays: decoded.data.relays
?.map((url) => {
try {
return normalizeRelayURL(url);
} catch {
return null;
}
})
.filter((url): url is string => url !== null),
};
}
} catch {
// Not valid nevent, continue
}
}
// note1: decode to event ID
if (value.startsWith("note")) {
try {
const decoded = nip19.decode(value);
if (decoded.type === "note") {
return {
id: decoded.data,
};
}
} catch {
// Not valid note, continue
}
}
// Hex event ID
if (isValidHexEventId(value)) {
return {
id: normalizeHex(value),
};
}
return null;
}

View File

@@ -176,9 +176,14 @@ export const manPages: Record<string, ManPageEntry> = {
description: "Maximum number of events to return",
},
{
flag: "-e <note|nevent|naddr|hex>",
flag: "-i, --id <note|nevent|hex>",
description:
"Filter by event ID or coordinate. Supports note1 (bare event ID), nevent1 (event with relay hints), naddr1 (addressable event coordinate), or raw hex. Comma-separated values supported: -e note1...,nevent1...,naddr1...",
"Direct event lookup by ID (filter.ids). Fetch specific events by their ID. Supports note1, nevent1 (with relay hints), or raw hex. Comma-separated values supported: -i note1...,nevent1...,abc123...",
},
{
flag: "-e <note|nevent|naddr|coordinate|hex>",
description:
"Tag-based filtering (#e/#a tags). Find events that reference the specified events or addresses. Supports note1, nevent1, naddr1, raw coordinates (kind:pubkey:d-tag), or hex. Comma-separated values supported: -e note1...,30023:pubkey:article",
},
{
flag: "-p <npub|hex|nip05|$me|$contacts>",
@@ -256,6 +261,10 @@ export const manPages: Record<string, ManPageEntry> = {
"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 -i note1abc123... Direct lookup: fetch event by ID",
"req -i nevent1... Direct lookup: fetch event by nevent (uses relay hints)",
"req -e note1abc123... -k 1 Tag filtering: find notes that reply to or reference event",
"req -e 30023:pubkey...:article-name -k 1,7 Tag filtering: find events referencing addressable event",
"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)",