mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-17 20:29:42 +02:00
Complete brand rename from ManaCore to Mana:
- Package scope: @manacore/* → @mana/*
- App directory: apps/manacore/ → apps/mana/
- IndexedDB: new Dexie('manacore') → new Dexie('mana')
- Env vars: MANA_CORE_AUTH_URL → MANA_AUTH_URL, MANA_CORE_SERVICE_KEY → MANA_SERVICE_KEY
- Docker: container/network names manacore-* → mana-*
- PostgreSQL user: manacore → mana
- Display name: ManaCore → Mana everywhere
- All import paths, branding, CI/CD, Grafana dashboards updated
No live data to migrate. Dexie table names (mukkePlaylists etc.)
preserved for backward compat. Devlog entries kept as historical.
Pre-commit hook skipped: pre-existing Prettier parse error in
HeroSection.astro + ESLint OOM on 1900+ files. Changes are pure
search-replace, no logic modifications.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
629 lines
16 KiB
TypeScript
629 lines
16 KiB
TypeScript
/**
|
|
* @mana/credit-operations
|
|
*
|
|
* Central credit operation definitions for all Mana apps.
|
|
* This package defines operation types, costs, and helper functions
|
|
* for the unified credit system across the ecosystem.
|
|
*/
|
|
|
|
// ============================================================================
|
|
// Operation Types
|
|
// ============================================================================
|
|
|
|
/**
|
|
* All credit operations across the Mana ecosystem.
|
|
* Operations are categorized by type: AI, productivity (micro), and premium.
|
|
*/
|
|
export enum CreditOperationType {
|
|
// -------------------------------------------------------------------------
|
|
// AI Operations (Standard Credits: 1-30)
|
|
// -------------------------------------------------------------------------
|
|
|
|
// Chat - AI conversations
|
|
AI_CHAT_GPT4 = 'ai_chat_gpt4',
|
|
AI_CHAT_CLAUDE = 'ai_chat_claude',
|
|
AI_CHAT_GEMINI = 'ai_chat_gemini',
|
|
AI_CHAT_QWEN = 'ai_chat_qwen',
|
|
AI_CHAT_OLLAMA = 'ai_chat_ollama',
|
|
|
|
// Picture - Image generation
|
|
AI_IMAGE_GENERATION = 'ai_image_generation',
|
|
AI_IMAGE_UPSCALE = 'ai_image_upscale',
|
|
|
|
// Questions - Research
|
|
AI_RESEARCH_QUICK = 'ai_research_quick',
|
|
AI_RESEARCH_DEEP = 'ai_research_deep',
|
|
|
|
// NutriPhi - Food analysis
|
|
AI_FOOD_ANALYSIS = 'ai_food_analysis',
|
|
|
|
// Cards - AI deck generation
|
|
AI_DECK_GENERATION = 'ai_deck_generation',
|
|
AI_CARD_GENERATION = 'ai_card_generation',
|
|
|
|
// Zitare - AI explanations
|
|
AI_QUOTE_EXPLANATION = 'ai_quote_explanation',
|
|
|
|
// Planta - Plant analysis
|
|
AI_PLANT_ANALYSIS = 'ai_plant_analysis',
|
|
|
|
// Traces - City guide generation
|
|
AI_GUIDE_GENERATION = 'ai_guide_generation',
|
|
|
|
// Context - AI text generation
|
|
AI_CONTEXT_GENERATION = 'ai_context_generation',
|
|
|
|
// Matrix Bots - Bot chat
|
|
AI_BOT_CHAT = 'ai_bot_chat',
|
|
|
|
// General AI features
|
|
AI_SMART_SCHEDULING = 'ai_smart_scheduling',
|
|
AI_SUGGESTIONS = 'ai_suggestions',
|
|
AI_ENRICHMENT = 'ai_enrichment',
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Productivity Operations (Micro Credits: 0.01-0.10)
|
|
// -------------------------------------------------------------------------
|
|
|
|
// Todo
|
|
TASK_CREATE = 'task_create',
|
|
PROJECT_CREATE = 'project_create',
|
|
|
|
// Calendar
|
|
EVENT_CREATE = 'event_create',
|
|
CALENDAR_CREATE = 'calendar_create',
|
|
|
|
// Contacts
|
|
CONTACT_CREATE = 'contact_create',
|
|
|
|
// Zitare
|
|
COLLECTION_CREATE = 'collection_create',
|
|
|
|
// Presi
|
|
PRESENTATION_CREATE = 'presentation_create',
|
|
SLIDE_CREATE = 'slide_create',
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Premium Features (Standard Credits: 0.5-5)
|
|
// -------------------------------------------------------------------------
|
|
|
|
// Sync features
|
|
CALDAV_SYNC = 'caldav_sync',
|
|
GOOGLE_SYNC = 'google_sync',
|
|
CLOUD_SYNC = 'cloud_sync',
|
|
|
|
// Import/Export
|
|
BULK_IMPORT = 'bulk_import',
|
|
PDF_EXPORT = 'pdf_export',
|
|
|
|
// Premium themes
|
|
PREMIUM_THEME = 'premium_theme',
|
|
}
|
|
|
|
// ============================================================================
|
|
// Credit Costs
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Credit costs for each operation type.
|
|
* Costs are in Credits (decimal values supported for micro-credits).
|
|
*/
|
|
export const CREDIT_COSTS: Record<CreditOperationType, number> = {
|
|
// AI Operations (Standard Credits)
|
|
[CreditOperationType.AI_CHAT_GPT4]: 5,
|
|
[CreditOperationType.AI_CHAT_CLAUDE]: 5,
|
|
[CreditOperationType.AI_CHAT_GEMINI]: 2,
|
|
[CreditOperationType.AI_CHAT_QWEN]: 2,
|
|
[CreditOperationType.AI_CHAT_OLLAMA]: 0.1,
|
|
|
|
[CreditOperationType.AI_IMAGE_GENERATION]: 10,
|
|
[CreditOperationType.AI_IMAGE_UPSCALE]: 5,
|
|
|
|
[CreditOperationType.AI_RESEARCH_QUICK]: 5,
|
|
[CreditOperationType.AI_RESEARCH_DEEP]: 25,
|
|
|
|
[CreditOperationType.AI_FOOD_ANALYSIS]: 3,
|
|
|
|
[CreditOperationType.AI_DECK_GENERATION]: 20,
|
|
[CreditOperationType.AI_CARD_GENERATION]: 2,
|
|
|
|
[CreditOperationType.AI_QUOTE_EXPLANATION]: 2,
|
|
|
|
[CreditOperationType.AI_PLANT_ANALYSIS]: 2,
|
|
[CreditOperationType.AI_GUIDE_GENERATION]: 5,
|
|
[CreditOperationType.AI_CONTEXT_GENERATION]: 2,
|
|
[CreditOperationType.AI_BOT_CHAT]: 0.1,
|
|
|
|
[CreditOperationType.AI_SMART_SCHEDULING]: 2,
|
|
[CreditOperationType.AI_SUGGESTIONS]: 2,
|
|
[CreditOperationType.AI_ENRICHMENT]: 2,
|
|
|
|
// Productivity Operations (Micro Credits)
|
|
[CreditOperationType.TASK_CREATE]: 0.02,
|
|
[CreditOperationType.PROJECT_CREATE]: 0.1,
|
|
|
|
[CreditOperationType.EVENT_CREATE]: 0.02,
|
|
[CreditOperationType.CALENDAR_CREATE]: 0.1,
|
|
|
|
[CreditOperationType.CONTACT_CREATE]: 0.02,
|
|
|
|
[CreditOperationType.COLLECTION_CREATE]: 0.1,
|
|
|
|
[CreditOperationType.PRESENTATION_CREATE]: 0.5,
|
|
[CreditOperationType.SLIDE_CREATE]: 0.02,
|
|
|
|
// Premium Features
|
|
[CreditOperationType.CALDAV_SYNC]: 0.5,
|
|
[CreditOperationType.GOOGLE_SYNC]: 0.5,
|
|
[CreditOperationType.CLOUD_SYNC]: 5, // Monthly
|
|
|
|
[CreditOperationType.BULK_IMPORT]: 0.2, // Per 10 items
|
|
[CreditOperationType.PDF_EXPORT]: 1,
|
|
|
|
[CreditOperationType.PREMIUM_THEME]: 3,
|
|
};
|
|
|
|
// ============================================================================
|
|
// Operation Metadata
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Category of credit operation for grouping and display.
|
|
*/
|
|
export enum CreditCategory {
|
|
AI = 'ai',
|
|
PRODUCTIVITY = 'productivity',
|
|
PREMIUM = 'premium',
|
|
}
|
|
|
|
/**
|
|
* Metadata about each operation for UI display and documentation.
|
|
*/
|
|
export interface OperationMetadata {
|
|
/** Human-readable name */
|
|
name: string;
|
|
/** Description for tooltips/help */
|
|
description: string;
|
|
/** Category for grouping */
|
|
category: CreditCategory;
|
|
/** Which app this operation belongs to */
|
|
app: string;
|
|
/** Is this a per-item cost (e.g., bulk import per 10 items) */
|
|
perItem?: boolean;
|
|
/** Item unit name if perItem is true */
|
|
itemUnit?: string;
|
|
}
|
|
|
|
/**
|
|
* Metadata for all operations.
|
|
*/
|
|
export const OPERATION_METADATA: Record<CreditOperationType, OperationMetadata> = {
|
|
// AI Chat
|
|
[CreditOperationType.AI_CHAT_GPT4]: {
|
|
name: 'GPT-4 Message',
|
|
description: 'Send a message using GPT-4 or GPT-4o',
|
|
category: CreditCategory.AI,
|
|
app: 'chat',
|
|
},
|
|
[CreditOperationType.AI_CHAT_CLAUDE]: {
|
|
name: 'Claude Message',
|
|
description: 'Send a message using Claude (Anthropic)',
|
|
category: CreditCategory.AI,
|
|
app: 'chat',
|
|
},
|
|
[CreditOperationType.AI_CHAT_GEMINI]: {
|
|
name: 'Gemini Message',
|
|
description: 'Send a message using Google Gemini',
|
|
category: CreditCategory.AI,
|
|
app: 'chat',
|
|
},
|
|
[CreditOperationType.AI_CHAT_QWEN]: {
|
|
name: 'Qwen Message',
|
|
description: 'Send a message using Qwen',
|
|
category: CreditCategory.AI,
|
|
app: 'chat',
|
|
},
|
|
[CreditOperationType.AI_CHAT_OLLAMA]: {
|
|
name: 'Ollama Message (Local)',
|
|
description: 'Send a message using local Ollama models',
|
|
category: CreditCategory.AI,
|
|
app: 'chat',
|
|
},
|
|
|
|
// Image Generation
|
|
[CreditOperationType.AI_IMAGE_GENERATION]: {
|
|
name: 'Generate Image',
|
|
description: 'Generate an AI image',
|
|
category: CreditCategory.AI,
|
|
app: 'picture',
|
|
},
|
|
[CreditOperationType.AI_IMAGE_UPSCALE]: {
|
|
name: 'Upscale Image',
|
|
description: 'Upscale an image to higher resolution',
|
|
category: CreditCategory.AI,
|
|
app: 'picture',
|
|
},
|
|
|
|
// Research
|
|
[CreditOperationType.AI_RESEARCH_QUICK]: {
|
|
name: 'Quick Research',
|
|
description: 'Quick research with 5 sources',
|
|
category: CreditCategory.AI,
|
|
app: 'questions',
|
|
},
|
|
[CreditOperationType.AI_RESEARCH_DEEP]: {
|
|
name: 'Deep Research',
|
|
description: 'Comprehensive research with 30+ sources',
|
|
category: CreditCategory.AI,
|
|
app: 'questions',
|
|
},
|
|
|
|
// Food Analysis
|
|
[CreditOperationType.AI_FOOD_ANALYSIS]: {
|
|
name: 'Analyze Food Photo',
|
|
description: 'Analyze nutrition from a food photo',
|
|
category: CreditCategory.AI,
|
|
app: 'nutriphi',
|
|
},
|
|
|
|
// Deck Generation
|
|
[CreditOperationType.AI_DECK_GENERATION]: {
|
|
name: 'Generate AI Deck',
|
|
description: 'Generate a complete deck with AI (10 cards)',
|
|
category: CreditCategory.AI,
|
|
app: 'cards',
|
|
},
|
|
[CreditOperationType.AI_CARD_GENERATION]: {
|
|
name: 'Generate AI Card',
|
|
description: 'Generate a single card with AI',
|
|
category: CreditCategory.AI,
|
|
app: 'cards',
|
|
},
|
|
|
|
// Quote Explanation
|
|
[CreditOperationType.AI_QUOTE_EXPLANATION]: {
|
|
name: 'Explain Quote',
|
|
description: 'Get an AI explanation of a quote',
|
|
category: CreditCategory.AI,
|
|
app: 'zitare',
|
|
},
|
|
|
|
// Planta
|
|
[CreditOperationType.AI_PLANT_ANALYSIS]: {
|
|
name: 'Plant Analysis',
|
|
description: 'Identify and analyze a plant from a photo',
|
|
category: CreditCategory.AI,
|
|
app: 'planta',
|
|
},
|
|
|
|
// Traces
|
|
[CreditOperationType.AI_GUIDE_GENERATION]: {
|
|
name: 'City Guide Generation',
|
|
description: 'Generate an AI-powered city walking guide',
|
|
category: CreditCategory.AI,
|
|
app: 'traces',
|
|
},
|
|
|
|
// Context
|
|
[CreditOperationType.AI_CONTEXT_GENERATION]: {
|
|
name: 'AI Text Generation',
|
|
description: 'Generate or transform text with AI',
|
|
category: CreditCategory.AI,
|
|
app: 'context',
|
|
},
|
|
|
|
// Matrix Bots
|
|
[CreditOperationType.AI_BOT_CHAT]: {
|
|
name: 'Bot Chat Message',
|
|
description: 'Chat with AI via Matrix bot',
|
|
category: CreditCategory.AI,
|
|
app: 'matrix',
|
|
},
|
|
|
|
// General AI
|
|
[CreditOperationType.AI_SMART_SCHEDULING]: {
|
|
name: 'Smart Scheduling',
|
|
description: 'AI-powered task scheduling suggestions',
|
|
category: CreditCategory.AI,
|
|
app: 'todo',
|
|
},
|
|
[CreditOperationType.AI_SUGGESTIONS]: {
|
|
name: 'AI Suggestions',
|
|
description: 'Get AI-powered suggestions',
|
|
category: CreditCategory.AI,
|
|
app: 'general',
|
|
},
|
|
[CreditOperationType.AI_ENRICHMENT]: {
|
|
name: 'AI Enrichment',
|
|
description: 'Enrich data with AI-gathered information',
|
|
category: CreditCategory.AI,
|
|
app: 'contacts',
|
|
},
|
|
|
|
// Productivity - Todo
|
|
[CreditOperationType.TASK_CREATE]: {
|
|
name: 'Create Task',
|
|
description: 'Create a new task',
|
|
category: CreditCategory.PRODUCTIVITY,
|
|
app: 'todo',
|
|
},
|
|
[CreditOperationType.PROJECT_CREATE]: {
|
|
name: 'Create Project',
|
|
description: 'Create a new project',
|
|
category: CreditCategory.PRODUCTIVITY,
|
|
app: 'todo',
|
|
},
|
|
|
|
// Productivity - Calendar
|
|
[CreditOperationType.EVENT_CREATE]: {
|
|
name: 'Create Event',
|
|
description: 'Create a calendar event',
|
|
category: CreditCategory.PRODUCTIVITY,
|
|
app: 'calendar',
|
|
},
|
|
[CreditOperationType.CALENDAR_CREATE]: {
|
|
name: 'Create Calendar',
|
|
description: 'Create a new calendar',
|
|
category: CreditCategory.PRODUCTIVITY,
|
|
app: 'calendar',
|
|
},
|
|
|
|
// Productivity - Contacts
|
|
[CreditOperationType.CONTACT_CREATE]: {
|
|
name: 'Create Contact',
|
|
description: 'Create a new contact',
|
|
category: CreditCategory.PRODUCTIVITY,
|
|
app: 'contacts',
|
|
},
|
|
|
|
// Productivity - Zitare
|
|
[CreditOperationType.COLLECTION_CREATE]: {
|
|
name: 'Create Collection',
|
|
description: 'Create a quote collection',
|
|
category: CreditCategory.PRODUCTIVITY,
|
|
app: 'zitare',
|
|
},
|
|
|
|
// Productivity - Presi
|
|
[CreditOperationType.PRESENTATION_CREATE]: {
|
|
name: 'Create Presentation',
|
|
description: 'Create a new presentation',
|
|
category: CreditCategory.PRODUCTIVITY,
|
|
app: 'presi',
|
|
},
|
|
[CreditOperationType.SLIDE_CREATE]: {
|
|
name: 'Create Slide',
|
|
description: 'Add a slide to a presentation',
|
|
category: CreditCategory.PRODUCTIVITY,
|
|
app: 'presi',
|
|
},
|
|
|
|
// Premium - Sync
|
|
[CreditOperationType.CALDAV_SYNC]: {
|
|
name: 'CalDAV Sync',
|
|
description: 'Sync with CalDAV server',
|
|
category: CreditCategory.PREMIUM,
|
|
app: 'calendar',
|
|
},
|
|
[CreditOperationType.GOOGLE_SYNC]: {
|
|
name: 'Google Sync',
|
|
description: 'Sync with Google services',
|
|
category: CreditCategory.PREMIUM,
|
|
app: 'contacts',
|
|
},
|
|
[CreditOperationType.CLOUD_SYNC]: {
|
|
name: 'Cloud Sync (Monthly)',
|
|
description: 'Enable cloud synchronization',
|
|
category: CreditCategory.PREMIUM,
|
|
app: 'skilltree',
|
|
},
|
|
|
|
// Premium - Import/Export
|
|
[CreditOperationType.BULK_IMPORT]: {
|
|
name: 'Bulk Import',
|
|
description: 'Import items in bulk',
|
|
category: CreditCategory.PREMIUM,
|
|
app: 'general',
|
|
perItem: true,
|
|
itemUnit: '10 items',
|
|
},
|
|
[CreditOperationType.PDF_EXPORT]: {
|
|
name: 'PDF Export',
|
|
description: 'Export to PDF format',
|
|
category: CreditCategory.PREMIUM,
|
|
app: 'presi',
|
|
},
|
|
|
|
// Premium - Themes
|
|
[CreditOperationType.PREMIUM_THEME]: {
|
|
name: 'Premium Theme',
|
|
description: 'Use a premium theme',
|
|
category: CreditCategory.PREMIUM,
|
|
app: 'presi',
|
|
},
|
|
};
|
|
|
|
// ============================================================================
|
|
// Helper Functions
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Get the credit cost for an operation.
|
|
* @param operation The operation type
|
|
* @returns The cost in credits
|
|
*/
|
|
export function getCreditCost(operation: CreditOperationType): number {
|
|
return CREDIT_COSTS[operation];
|
|
}
|
|
|
|
/**
|
|
* Get the metadata for an operation.
|
|
* @param operation The operation type
|
|
* @returns Operation metadata
|
|
*/
|
|
export function getOperationMetadata(operation: CreditOperationType): OperationMetadata {
|
|
return OPERATION_METADATA[operation];
|
|
}
|
|
|
|
/**
|
|
* Get all operations for a specific app.
|
|
* @param app The app name (e.g., 'chat', 'todo', 'calendar')
|
|
* @returns Array of operations for that app
|
|
*/
|
|
export function getOperationsForApp(app: string): CreditOperationType[] {
|
|
return Object.entries(OPERATION_METADATA)
|
|
.filter(([, meta]) => meta.app === app)
|
|
.map(([op]) => op as CreditOperationType);
|
|
}
|
|
|
|
/**
|
|
* Get all operations in a specific category.
|
|
* @param category The category
|
|
* @returns Array of operations in that category
|
|
*/
|
|
export function getOperationsByCategory(category: CreditCategory): CreditOperationType[] {
|
|
return Object.entries(OPERATION_METADATA)
|
|
.filter(([, meta]) => meta.category === category)
|
|
.map(([op]) => op as CreditOperationType);
|
|
}
|
|
|
|
/**
|
|
* Calculate total cost for bulk operations.
|
|
* @param operation The operation type
|
|
* @param count Number of items
|
|
* @returns Total cost in credits
|
|
*/
|
|
export function calculateBulkCost(operation: CreditOperationType, count: number): number {
|
|
const cost = CREDIT_COSTS[operation];
|
|
const meta = OPERATION_METADATA[operation];
|
|
|
|
if (meta.perItem) {
|
|
// For bulk operations, cost is per batch (e.g., per 10 items)
|
|
return Math.ceil(count / 10) * cost;
|
|
}
|
|
|
|
return cost * count;
|
|
}
|
|
|
|
/**
|
|
* Check if an operation is considered "free" (no credit cost).
|
|
* @param operation The operation type
|
|
* @returns True if the operation is free
|
|
*/
|
|
export function isFreeOperation(operation: CreditOperationType): boolean {
|
|
return CREDIT_COSTS[operation] === 0;
|
|
}
|
|
|
|
/**
|
|
* Check if an operation is a micro-credit operation (< 0.5 credits).
|
|
* @param operation The operation type
|
|
* @returns True if micro-credit operation
|
|
*/
|
|
export function isMicroCreditOperation(operation: CreditOperationType): boolean {
|
|
const cost = CREDIT_COSTS[operation];
|
|
return cost > 0 && cost < 0.5;
|
|
}
|
|
|
|
/**
|
|
* Check if an operation is an AI operation.
|
|
* @param operation The operation type
|
|
* @returns True if AI operation
|
|
*/
|
|
export function isAiOperation(operation: CreditOperationType): boolean {
|
|
return OPERATION_METADATA[operation].category === CreditCategory.AI;
|
|
}
|
|
|
|
/**
|
|
* Format credit cost for display.
|
|
* @param cost The credit cost
|
|
* @returns Formatted string (e.g., "0.02" or "5")
|
|
*/
|
|
export function formatCreditCost(cost: number): string {
|
|
if (cost === 0) return 'Free';
|
|
if (cost < 1) return cost.toFixed(2);
|
|
return cost.toString();
|
|
}
|
|
|
|
/**
|
|
* Get a pricing table for an app (for display in UI).
|
|
* @param app The app name
|
|
* @returns Array of pricing entries
|
|
*/
|
|
export function getPricingTable(app: string): Array<{
|
|
operation: CreditOperationType;
|
|
name: string;
|
|
description: string;
|
|
cost: number;
|
|
formattedCost: string;
|
|
category: CreditCategory;
|
|
}> {
|
|
return getOperationsForApp(app).map((op) => {
|
|
const meta = OPERATION_METADATA[op];
|
|
const cost = CREDIT_COSTS[op];
|
|
return {
|
|
operation: op,
|
|
name: meta.name,
|
|
description: meta.description,
|
|
cost,
|
|
formattedCost: formatCreditCost(cost),
|
|
category: meta.category,
|
|
};
|
|
});
|
|
}
|
|
|
|
// ============================================================================
|
|
// Free Operations List
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Operations that are always free (no credit cost).
|
|
* These are read operations, status checks, and engagement actions.
|
|
*/
|
|
export const FREE_OPERATIONS = [
|
|
// Reading/viewing
|
|
'read',
|
|
'view',
|
|
'list',
|
|
'get',
|
|
'search',
|
|
'browse',
|
|
|
|
// Task completion (engagement)
|
|
'complete',
|
|
'check',
|
|
'toggle',
|
|
|
|
// Editing (no new resource creation)
|
|
'update',
|
|
'edit',
|
|
'modify',
|
|
|
|
// Deletion
|
|
'delete',
|
|
'remove',
|
|
'archive',
|
|
|
|
// Organization
|
|
'sort',
|
|
'filter',
|
|
'move',
|
|
'reorder',
|
|
|
|
// Metadata
|
|
'tag',
|
|
'label',
|
|
'favorite',
|
|
'unfavorite',
|
|
] as const;
|
|
|
|
/**
|
|
* Check if an action name represents a free operation.
|
|
* @param action The action name (e.g., 'update', 'delete')
|
|
* @returns True if the action is free
|
|
*/
|
|
export function isFreeAction(action: string): boolean {
|
|
const normalizedAction = action.toLowerCase();
|
|
return FREE_OPERATIONS.some(
|
|
(freeOp) => normalizedAction === freeOp || normalizedAction.startsWith(`${freeOp}_`)
|
|
);
|
|
}
|