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).
This commit is contained in:
Claude
2026-01-22 13:36:02 +00:00
parent cf33e9a1d6
commit 17840c2028
2 changed files with 155 additions and 43 deletions

View File

@@ -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);
}

View File

@@ -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;
}