From 17840c2028d183927843ffe64a553ccfd2193d45 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 22 Jan 2026 13:36:02 +0000 Subject: [PATCH] feat: include non-parameterized spells with $me/$contacts as $pubkey lenses When querying for $pubkey parameter type spells, now includes existing non-parameterized spells that use $me or $contacts (single argument). These spells can be applied to any pubkey, making them reusable across profiles. Changes: - Update useParameterizedSpells to query all spells when type=$pubkey - Detect and include spells with $me or $contacts in command - Treat these spells as implicitly parameterized with default value - Update applySpellParameters to handle implicit parameterization - Substitute $me/$contacts with target pubkey when applying to profiles This allows users to use existing spells like 'req -k 1 -a $me' on any profile without explicitly marking them as parameterized. All tests passing (1015 tests). --- src/hooks/useParameterizedSpells.ts | 169 +++++++++++++++++++++------- src/lib/spell-conversion.ts | 29 ++++- 2 files changed, 155 insertions(+), 43 deletions(-) diff --git a/src/hooks/useParameterizedSpells.ts b/src/hooks/useParameterizedSpells.ts index 82bf7b7..a160d7d 100644 --- a/src/hooks/useParameterizedSpells.ts +++ b/src/hooks/useParameterizedSpells.ts @@ -88,8 +88,32 @@ export function useParameterizedSpells( const stableAuthor = useStableValue(author); const stableRelays = useStableValue(relays); - // Query local spells with parameterType + // Query local spells with parameterType or convertible spells const localSpells = useLiveQuery(async () => { + // For $pubkey type, also include non-parameterized spells with $me or $contacts + if (stableType === "$pubkey") { + // Get all spells (parameterized and non-parameterized) + const allSpells = await db.spells.toArray(); + + return allSpells.filter((spell) => { + // Skip soft-deleted + if (spell.deletedAt) return false; + + // Include explicitly parameterized $pubkey spells + if (spell.parameterType === "$pubkey") return true; + + // Include spells with $me or $contacts that could be parameterized + if (!spell.parameterType) { + const cmd = spell.command.toLowerCase(); + // Check if command uses $me or $contacts (single arg that can become $pubkey) + return cmd.includes("$me") || cmd.includes("$contacts"); + } + + return false; + }); + } + + // For other types or no type filter, use the original indexed query let query = db.spells.where("parameterType").notEqual(undefined as any); // Filter by type if specified @@ -114,8 +138,9 @@ export function useParameterizedSpells( filter.authors = [stableAuthor]; } - // Add tag filter for parameter type if specified - if (stableType) { + // For $pubkey type, load all spells (will filter in merge logic) + // For other types, filter by parameter tag + if (stableType && stableType !== "$pubkey") { filter["#l"] = [stableType]; } @@ -151,7 +176,9 @@ export function useParameterizedSpells( filter.authors = [stableAuthor]; } - if (stableType) { + // For $pubkey type, load all spells (will filter in merge logic) + // For other types, filter by parameter tag + if (stableType && stableType !== "$pubkey") { filter["#l"] = [stableType]; } @@ -164,26 +191,55 @@ export function useParameterizedSpells( // Add local spells for (const localSpell of localSpells || []) { - // Skip if no parameter type (should be filtered by query, but double-check) - if (!localSpell.parameterType) continue; + // Handle explicitly parameterized spells + if (localSpell.parameterType) { + // Skip if type filter doesn't match + if (stableType && localSpell.parameterType !== stableType) continue; - // Skip if type filter doesn't match - if (stableType && localSpell.parameterType !== stableType) continue; + spellsMap.set(localSpell.id, { + id: localSpell.id, + name: localSpell.name, + alias: localSpell.alias, + command: localSpell.command, + description: localSpell.description, + parameterType: localSpell.parameterType, + parameterDefault: localSpell.parameterDefault, + isPublished: localSpell.isPublished, + eventId: localSpell.eventId, + event: localSpell.event, + createdAt: localSpell.createdAt, + source: "local" as const, + }); + continue; + } - spellsMap.set(localSpell.id, { - id: localSpell.id, - name: localSpell.name, - alias: localSpell.alias, - command: localSpell.command, - description: localSpell.description, - parameterType: localSpell.parameterType, - parameterDefault: localSpell.parameterDefault, - isPublished: localSpell.isPublished, - eventId: localSpell.eventId, - event: localSpell.event, - createdAt: localSpell.createdAt, - source: "local" as const, - }); + // Handle non-parameterized spells with $me or $contacts (treat as $pubkey) + if (stableType === "$pubkey") { + const cmd = localSpell.command.toLowerCase(); + if (cmd.includes("$me") || cmd.includes("$contacts")) { + // Detect default value from command + const defaultValue = cmd.includes("$me") + ? ["$me"] + : cmd.includes("$contacts") + ? ["$contacts"] + : undefined; + + spellsMap.set(localSpell.id, { + id: localSpell.id, + name: localSpell.name, + alias: localSpell.alias, + command: localSpell.command, + description: localSpell.description, + parameterType: "$pubkey" as const, + parameterDefault: defaultValue, + isPublished: localSpell.isPublished, + eventId: localSpell.eventId, + event: localSpell.event, + createdAt: localSpell.createdAt, + source: "local" as const, + }); + } + } } // Add network spells (skip if already in local) @@ -194,29 +250,58 @@ export function useParameterizedSpells( try { const parsed = decodeSpell(event as SpellEvent); - // Skip if not parameterized - if (!parsed.parameter) continue; - - // Skip if type filter doesn't match - if (stableType && parsed.parameter.type !== stableType) continue; - // Skip if author filter doesn't match if (stableAuthor && event.pubkey !== stableAuthor) continue; - spellsMap.set(event.id, { - id: event.id, - name: parsed.name, - command: parsed.command, - description: parsed.description, - parameterType: parsed.parameter.type, - parameterDefault: parsed.parameter.default, - isPublished: true, - eventId: event.id, - event: event as SpellEvent, - parsed, - createdAt: event.created_at * 1000, - source: "network" as const, - }); + // Handle explicitly parameterized spells + if (parsed.parameter) { + // Skip if type filter doesn't match + if (stableType && parsed.parameter.type !== stableType) continue; + + spellsMap.set(event.id, { + id: event.id, + name: parsed.name, + command: parsed.command, + description: parsed.description, + parameterType: parsed.parameter.type, + parameterDefault: parsed.parameter.default, + isPublished: true, + eventId: event.id, + event: event as SpellEvent, + parsed, + createdAt: event.created_at * 1000, + source: "network" as const, + }); + continue; + } + + // Handle non-parameterized spells with $me or $contacts (treat as $pubkey) + if (stableType === "$pubkey") { + const cmd = parsed.command.toLowerCase(); + if (cmd.includes("$me") || cmd.includes("$contacts")) { + // Detect default value from command + const defaultValue = cmd.includes("$me") + ? ["$me"] + : cmd.includes("$contacts") + ? ["$contacts"] + : undefined; + + spellsMap.set(event.id, { + id: event.id, + name: parsed.name, + command: parsed.command, + description: parsed.description, + parameterType: "$pubkey" as const, + parameterDefault: defaultValue, + isPublished: true, + eventId: event.id, + event: event as SpellEvent, + parsed, + createdAt: event.created_at * 1000, + source: "network" as const, + }); + } + } } catch (e) { console.warn("Failed to decode network spell", event.id, e); } diff --git a/src/lib/spell-conversion.ts b/src/lib/spell-conversion.ts index debd478..f7f528c 100644 --- a/src/lib/spell-conversion.ts +++ b/src/lib/spell-conversion.ts @@ -522,7 +522,34 @@ export function applySpellParameters( args: string[] = [], ): NostrFilter { if (!parsed.parameter) { - // Not a parameterized spell, return filter as-is + // Not an explicitly parameterized spell + // Check if we have args and the filter uses $me or $contacts (implicit parameterization) + if (args.length > 0) { + const filter: NostrFilter = { ...parsed.filter }; + + // Substitute $me and $contacts with provided pubkey(s) + if (filter.authors) { + filter.authors = filter.authors.flatMap((author) => + author === "$me" || author === "$contacts" ? args : [author], + ); + } + + if (filter["#p"]) { + filter["#p"] = filter["#p"].flatMap((p) => + p === "$me" || p === "$contacts" ? args : [p], + ); + } + + if (filter["#P"]) { + filter["#P"] = filter["#P"].flatMap((p) => + p === "$me" || p === "$contacts" ? args : [p], + ); + } + + return filter; + } + + // No parameter and no args, return filter as-is return parsed.filter; }