mirror of
https://github.com/purrgrammer/grimoire.git
synced 2026-04-13 00:46:54 +02:00
Add spell support for COUNT commands
- Extend spell system to support both REQ and COUNT commands - Add detectCommandType() to identify command type from string - Update encodeSpell to use ["cmd", "COUNT"] tag for count commands - Update decodeSpell to handle COUNT spells - Update reconstructCommand to accept cmdType parameter - Add "Save as spell" option to COUNT windows in WindowToolbar - Update SpellDialog to handle both REQ and COUNT commands
This commit is contained in:
@@ -83,9 +83,9 @@ export function WindowToolbar({
|
||||
const handleTurnIntoSpell = () => {
|
||||
if (!window) return;
|
||||
|
||||
// Only available for REQ windows
|
||||
if (window.appId !== "req") {
|
||||
toast.error("Only REQ windows can be turned into spells");
|
||||
// Only available for REQ and COUNT windows
|
||||
if (window.appId !== "req" && window.appId !== "count") {
|
||||
toast.error("Only REQ and COUNT windows can be turned into spells");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -123,12 +123,14 @@ export function WindowToolbar({
|
||||
toast.success("NIP markdown copied to clipboard");
|
||||
};
|
||||
|
||||
// Check if this is a REQ window for spell creation
|
||||
// Check if this is a REQ or COUNT window for spell creation
|
||||
const isReqWindow = window?.appId === "req";
|
||||
const isCountWindow = window?.appId === "count";
|
||||
const isSpellableWindow = isReqWindow || isCountWindow;
|
||||
|
||||
// Get REQ command for spell dialog
|
||||
const reqCommand =
|
||||
isReqWindow && window
|
||||
// Get command for spell dialog
|
||||
const spellCommand =
|
||||
isSpellableWindow && window
|
||||
? window.commandString ||
|
||||
reconstructReqCommand(
|
||||
window.props?.filter || {},
|
||||
@@ -136,6 +138,7 @@ export function WindowToolbar({
|
||||
undefined,
|
||||
undefined,
|
||||
window.props?.closeOnEose,
|
||||
isCountWindow ? "COUNT" : "REQ",
|
||||
)
|
||||
: "";
|
||||
|
||||
@@ -216,8 +219,8 @@ export function WindowToolbar({
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* REQ-specific actions */}
|
||||
{isReqWindow && (
|
||||
{/* REQ/COUNT-specific actions */}
|
||||
{isSpellableWindow && (
|
||||
<>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={handleTurnIntoSpell}>
|
||||
@@ -230,12 +233,12 @@ export function WindowToolbar({
|
||||
</DropdownMenu>
|
||||
|
||||
{/* Spell Dialog */}
|
||||
{isReqWindow && (
|
||||
{isSpellableWindow && (
|
||||
<SpellDialog
|
||||
open={showSpellDialog}
|
||||
onOpenChange={setShowSpellDialog}
|
||||
mode="create"
|
||||
initialCommand={reqCommand}
|
||||
initialCommand={spellCommand}
|
||||
onSuccess={() => {
|
||||
toast.success("Spell published successfully!");
|
||||
}}
|
||||
|
||||
@@ -14,7 +14,7 @@ import { toast } from "sonner";
|
||||
import { use$ } from "applesauce-react/hooks";
|
||||
import accounts from "@/services/accounts";
|
||||
import { parseReqCommand } from "@/lib/req-parser";
|
||||
import { reconstructCommand } from "@/lib/spell-conversion";
|
||||
import { reconstructCommand, detectCommandType } from "@/lib/spell-conversion";
|
||||
import type { ParsedSpell, SpellEvent } from "@/types/spell";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { saveSpell } from "@/services/spell-storage";
|
||||
@@ -29,9 +29,12 @@ function filterSpellCommand(command: string): string {
|
||||
if (!command) return "";
|
||||
|
||||
try {
|
||||
// Parse the command
|
||||
const commandWithoutReq = command.replace(/^\s*req\s+/, "");
|
||||
const tokens = commandWithoutReq.split(/\s+/);
|
||||
// Detect command type (REQ or COUNT)
|
||||
const cmdType = detectCommandType(command);
|
||||
|
||||
// Parse the command - remove prefix first
|
||||
const commandWithoutPrefix = command.replace(/^\s*(req|count)\s+/i, "");
|
||||
const tokens = commandWithoutPrefix.split(/\s+/);
|
||||
|
||||
// Parse to get filter and relays
|
||||
const parsed = parseReqCommand(tokens);
|
||||
@@ -43,6 +46,7 @@ function filterSpellCommand(command: string): string {
|
||||
undefined,
|
||||
undefined,
|
||||
parsed.closeOnEose,
|
||||
cmdType,
|
||||
);
|
||||
} catch {
|
||||
// If parsing fails, return original
|
||||
@@ -245,7 +249,7 @@ export function SpellDialog({
|
||||
setErrorMessage("Signing was rejected. Please try again.");
|
||||
} else if (error.message.includes("No command provided")) {
|
||||
setErrorMessage(
|
||||
"No command to save. Please try again from a REQ window.",
|
||||
"No command to save. Please try again from a REQ or COUNT window.",
|
||||
);
|
||||
} else {
|
||||
setErrorMessage(error.message);
|
||||
@@ -274,7 +278,7 @@ export function SpellDialog({
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{mode === "create"
|
||||
? "Save this REQ command as a spell. You can save it locally or publish it to Nostr relays."
|
||||
? "Save this command as a spell. You can save it locally or publish it to Nostr relays."
|
||||
: "Edit your spell and republish it to relays."}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
@@ -49,7 +49,19 @@ function tokenizeCommand(command: string): string[] {
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode a REQ command as spell event tags
|
||||
* Detect command type from a command string
|
||||
* Returns "REQ" or "COUNT" based on the command prefix
|
||||
*/
|
||||
export function detectCommandType(command: string): "REQ" | "COUNT" {
|
||||
const trimmed = command.trim().toLowerCase();
|
||||
if (trimmed.startsWith("count ") || trimmed === "count") {
|
||||
return "COUNT";
|
||||
}
|
||||
return "REQ";
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode a REQ or COUNT command as spell event tags
|
||||
*
|
||||
* Parses the command and extracts filter parameters into Nostr tags.
|
||||
* Preserves relative timestamps (7d, now) for dynamic spell behavior.
|
||||
@@ -66,10 +78,15 @@ export function encodeSpell(options: CreateSpellOptions): EncodedSpell {
|
||||
throw new Error("Spell command is required");
|
||||
}
|
||||
|
||||
// Detect command type (REQ or COUNT)
|
||||
const cmdType = detectCommandType(command);
|
||||
|
||||
// Parse the command to extract filter components
|
||||
// Remove "req" prefix if present and tokenize
|
||||
const commandWithoutReq = command.replace(/^\s*req\s+/, "");
|
||||
const tokens = tokenizeCommand(commandWithoutReq);
|
||||
// Remove "req" or "count" prefix if present and tokenize
|
||||
const commandWithoutPrefix = command
|
||||
.replace(/^\s*(req|count)\s+/i, "")
|
||||
.trim();
|
||||
const tokens = tokenizeCommand(commandWithoutPrefix);
|
||||
|
||||
// Validate we have tokens to parse
|
||||
if (tokens.length === 0) {
|
||||
@@ -98,7 +115,7 @@ export function encodeSpell(options: CreateSpellOptions): EncodedSpell {
|
||||
|
||||
// Start with required tags
|
||||
const tags: [string, string, ...string[]][] = [
|
||||
["cmd", "REQ"],
|
||||
["cmd", cmdType],
|
||||
["client", "grimoire"],
|
||||
];
|
||||
|
||||
@@ -109,8 +126,8 @@ export function encodeSpell(options: CreateSpellOptions): EncodedSpell {
|
||||
|
||||
// Add alt tag for NIP-31 compatibility
|
||||
const altText = description
|
||||
? `Grimoire REQ spell: ${description.substring(0, 100)}`
|
||||
: "Grimoire REQ spell";
|
||||
? `Grimoire ${cmdType} spell: ${description.substring(0, 100)}`
|
||||
: `Grimoire ${cmdType} spell`;
|
||||
tags.push(["alt", altText]);
|
||||
|
||||
// Add provenance if forked
|
||||
@@ -246,7 +263,7 @@ export function decodeSpell(event: SpellEvent): ParsedSpell {
|
||||
|
||||
// Validate cmd tag
|
||||
const cmd = tagMap.get("cmd")?.[0];
|
||||
if (cmd !== "REQ") {
|
||||
if (cmd !== "REQ" && cmd !== "COUNT") {
|
||||
throw new Error(`Invalid spell command type: ${cmd}`);
|
||||
}
|
||||
|
||||
@@ -326,8 +343,15 @@ export function decodeSpell(event: SpellEvent): ParsedSpell {
|
||||
const relays = tagMap.get("relays");
|
||||
const closeOnEose = tagMap.has("close-on-eose");
|
||||
|
||||
// Reconstruct command string
|
||||
const command = reconstructCommand(filter, relays, since, until, closeOnEose);
|
||||
// Reconstruct command string with appropriate command type
|
||||
const command = reconstructCommand(
|
||||
filter,
|
||||
relays,
|
||||
since,
|
||||
until,
|
||||
closeOnEose,
|
||||
cmd as "REQ" | "COUNT",
|
||||
);
|
||||
|
||||
return {
|
||||
name,
|
||||
@@ -343,7 +367,7 @@ export function decodeSpell(event: SpellEvent): ParsedSpell {
|
||||
}
|
||||
|
||||
/**
|
||||
* Reconstruct a canonical REQ command string from filter components
|
||||
* Reconstruct a canonical command string from filter components
|
||||
*/
|
||||
export function reconstructCommand(
|
||||
filter: NostrFilter,
|
||||
@@ -351,8 +375,9 @@ export function reconstructCommand(
|
||||
since?: string,
|
||||
until?: string,
|
||||
closeOnEose?: boolean,
|
||||
cmdType: "REQ" | "COUNT" = "REQ",
|
||||
): string {
|
||||
const parts: string[] = ["req"];
|
||||
const parts: string[] = [cmdType.toLowerCase()];
|
||||
|
||||
// Kinds
|
||||
if (filter.kinds && filter.kinds.length > 0) {
|
||||
|
||||
Reference in New Issue
Block a user