Skip to main content
Glama
builder.ts11.1 kB
/** * Shortcut Builder - Creates and manages shortcut definitions */ import { ShortcutDefinition, ActionType, ShortcutInputType, ShortcutOutputType, ShortcutCreationRequest, ShortcutCreationResult, BuilderValidationResult, ShortcutTemplate } from '../types/shortcut-builder.js'; import { Logger } from '../utils/logger.js'; import { exec } from 'child_process'; import { promisify } from 'util'; import { writeFile, unlink } from 'fs/promises'; import { join } from 'path'; import { tmpdir } from 'os'; const execAsync = promisify(exec); export class ShortcutBuilder { private logger: Logger; private templates: Map<string, ShortcutTemplate> = new Map(); constructor(logger: Logger) { this.logger = logger; this.initializeTemplates(); } /** * Creates a new shortcut from a definition */ async createShortcut(request: ShortcutCreationRequest): Promise<ShortcutCreationResult> { try { this.logger.info('Creating shortcut:', { name: request.definition.name }); // Validate the definition if (request.validate !== false) { const validation = this.validateDefinition(request.definition); if (!validation.valid) { return { success: false, error: `Validation failed: ${validation.errors[0]?.message}`, validationResults: [validation] }; } } // Check if shortcut already exists if (!request.overwriteExisting) { const exists = await this.shortcutExists(request.definition.name); if (exists) { return { success: false, error: `Shortcut "${request.definition.name}" already exists. Use overwriteExisting=true to replace it.` }; } } // Generate shortcut file const shortcutData = this.generateShortcutData(request.definition); // Create temporary file const tempFile = join(tmpdir(), `shortcut_${Date.now()}.shortcut`); await writeFile(tempFile, JSON.stringify(shortcutData, null, 2)); // Import the shortcut await execAsync(`shortcuts import "${tempFile}"`); // Clean up temporary file await unlink(tempFile); this.logger.info('Shortcut created successfully:', { name: request.definition.name }); return { success: true, shortcutName: request.definition.name, warnings: [] }; } catch (error) { this.logger.error('Failed to create shortcut:', error); return { success: false, error: error instanceof Error ? error.message : String(error) }; } } /** * Creates a shortcut from a template */ async createFromTemplate(templateName: string, customizations: Partial<ShortcutDefinition> = {}): Promise<ShortcutCreationResult> { const template = this.templates.get(templateName); if (!template) { return { success: false, error: `Template "${templateName}" not found` }; } const definition: ShortcutDefinition = { ...template.definition, ...customizations, name: customizations.name || template.definition.name }; return this.createShortcut({ definition, validate: true }); } /** * Deletes a shortcut */ async deleteShortcut(name: string): Promise<boolean> { try { await execAsync(`shortcuts delete "${name}"`); this.logger.info('Shortcut deleted:', { name }); return true; } catch (error) { this.logger.error('Failed to delete shortcut:', { name, error }); return false; } } /** * Gets available templates */ getTemplates(): ShortcutTemplate[] { return Array.from(this.templates.values()); } /** * Adds a custom template */ addTemplate(template: ShortcutTemplate): void { this.templates.set(template.name, template); this.logger.debug('Template added:', { name: template.name }); } /** * Creates a simple text shortcut */ createSimpleTextShortcut(name: string, text: string, description?: string): ShortcutDefinition { return { name, description: description || `Returns the text: "${text}"`, actions: [ { type: ActionType.TEXT, identifier: 'text', parameters: { text } } ], outputType: ShortcutOutputType.TEXT }; } /** * Creates an input echo shortcut */ createEchoShortcut(name: string): ShortcutDefinition { return { name, description: 'Echoes back any input provided', inputType: ShortcutInputType.TEXT, outputType: ShortcutOutputType.TEXT, actions: [ { type: ActionType.TEXT, identifier: 'echo', parameters: { text: '{input}' } } ] }; } /** * Creates a current time shortcut */ createCurrentTimeShortcut(name: string, format?: string): ShortcutDefinition { return { name, description: 'Returns the current date and time', actions: [ { type: ActionType.DATE, identifier: 'current_date', parameters: {} }, { type: ActionType.FORMAT_DATE, identifier: 'format_date', parameters: { format: format || 'yyyy-MM-dd HH:mm:ss' } } ], outputType: ShortcutOutputType.TEXT }; } /** * Creates a weather shortcut */ createWeatherShortcut(name: string): ShortcutDefinition { return { name, description: 'Gets current weather for a location', inputType: ShortcutInputType.TEXT, actions: [ { type: ActionType.GET_CURRENT_WEATHER, identifier: 'get_weather', parameters: { location: '{input}' } } ], outputType: ShortcutOutputType.TEXT }; } /** * Creates a shell script shortcut */ createShellScriptShortcut(name: string, script: string, description?: string): ShortcutDefinition { return { name, description: description || 'Runs a shell script', actions: [ { type: ActionType.RUN_SHELL_SCRIPT, identifier: 'shell_script', parameters: { script, shell: '/bin/bash' } } ], outputType: ShortcutOutputType.TEXT }; } private validateDefinition(definition: ShortcutDefinition): BuilderValidationResult { const errors: any[] = []; const warnings: any[] = []; // Check required fields if (!definition.name || definition.name.trim().length === 0) { errors.push({ code: 'MISSING_NAME', message: 'Shortcut name is required' }); } if (!definition.actions || definition.actions.length === 0) { errors.push({ code: 'NO_ACTIONS', message: 'Shortcut must have at least one action' }); } // Validate actions definition.actions?.forEach((action, index) => { if (!action.type) { errors.push({ code: 'MISSING_ACTION_TYPE', message: 'Action type is required', actionIndex: index }); } if (!action.identifier) { errors.push({ code: 'MISSING_ACTION_IDENTIFIER', message: 'Action identifier is required', actionIndex: index }); } }); // Check for duplicate action identifiers const identifiers = new Set(); definition.actions?.forEach((action, index) => { if (identifiers.has(action.identifier)) { warnings.push({ code: 'DUPLICATE_IDENTIFIER', message: `Duplicate action identifier: ${action.identifier}`, suggestion: 'Use unique identifiers for each action', actionIndex: index }); } identifiers.add(action.identifier); }); return { valid: errors.length === 0, errors, warnings }; } private async shortcutExists(name: string): Promise<boolean> { try { const { stdout } = await execAsync('shortcuts list'); const shortcuts = stdout.split('\n').map(line => line.trim()).filter(Boolean); return shortcuts.includes(name); } catch { return false; } } private generateShortcutData(definition: ShortcutDefinition): any { // This generates the shortcut file format // The actual format is quite complex - this is a simplified version return { WFWorkflowName: definition.name, WFWorkflowDescription: definition.description || '', WFWorkflowIcon: { WFWorkflowIconImageData: null, WFWorkflowIconGlyphCharacter: definition.icon || '⚡' }, WFWorkflowInputContentItemClasses: definition.inputType ? [definition.inputType] : [], WFWorkflowOutputContentItemClasses: definition.outputType ? [definition.outputType] : [], WFWorkflowActions: definition.actions.map(action => ({ WFWorkflowActionIdentifier: action.type, WFWorkflowActionParameters: action.parameters || {}, UUID: action.uuid || this.generateUUID() })), WFWorkflowImportQuestions: [], WFWorkflowTypes: ['NCWidget', 'WatchKit'], WFWorkflowHasShortcutInputVariables: !!definition.inputType }; } private generateUUID(): string { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { const r = Math.random() * 16 | 0; const v = c === 'x' ? r : (r & 0x3 | 0x8); return v.toString(16); }); } private initializeTemplates(): void { // Initialize built-in templates const templates: ShortcutTemplate[] = [ { name: 'hello-world', description: 'Simple hello world shortcut', category: 'basic', definition: this.createSimpleTextShortcut('Hello World', 'Hello from Shortcuts MCP!') }, { name: 'echo-input', description: 'Echoes back any input', category: 'basic', definition: this.createEchoShortcut('Echo Input') }, { name: 'current-time', description: 'Shows current date and time', category: 'utilities', definition: this.createCurrentTimeShortcut('Current Time') }, { name: 'weather-report', description: 'Gets weather for a location', category: 'information', definition: this.createWeatherShortcut('Weather Report') }, { name: 'system-info', description: 'Gets basic system information', category: 'system', definition: { name: 'System Info', description: 'Returns basic system information', actions: [ { type: ActionType.GET_DEVICE_DETAILS, identifier: 'device_details', parameters: {} } ], outputType: ShortcutOutputType.TEXT } } ]; templates.forEach(template => { this.templates.set(template.name, template); }); this.logger.debug('Initialized shortcut templates:', { count: templates.length }); } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/aezizhu/shortcut-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server