aimevzulari commited on
Commit
008543b
·
verified ·
1 Parent(s): 9581e53

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +1216 -0
app.py ADDED
@@ -0,0 +1,1216 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Cursor Rules Generator - Hugging Face Spaces App
3
+
4
+ This module implements the Gradio interface for Hugging Face Spaces deployment.
5
+ All code is self-contained in this file to avoid import issues.
6
+ """
7
+
8
+ import os
9
+ import gradio as gr
10
+ import json
11
+ import requests
12
+ import traceback
13
+ from dotenv import load_dotenv
14
+ from abc import ABC, abstractmethod
15
+ from typing import Dict, List, Optional, Any
16
+
17
+ # Load environment variables
18
+ load_dotenv()
19
+
20
+ # Configuration settings
21
+ class Settings:
22
+ """Application settings."""
23
+
24
+ # Application settings
25
+ APP_NAME = "Cursor Rules Generator"
26
+ DEBUG = os.getenv("DEBUG", "False").lower() == "true"
27
+
28
+ # API keys
29
+ GEMINI_API_KEY = os.getenv("GEMINI_API_KEY", "")
30
+ OPENAI_API_KEY = os.getenv("OPENAI_API_KEY", "")
31
+ OPENROUTER_API_KEY = os.getenv("OPENROUTER_API_KEY", "")
32
+
33
+ # Default settings
34
+ DEFAULT_PROVIDER = os.getenv("DEFAULT_PROVIDER", "gemini")
35
+ DEFAULT_RULE_TYPE = os.getenv("DEFAULT_RULE_TYPE", "Always")
36
+
37
+ # LLM provider settings
38
+ GEMINI_API_URL = "https://generativelanguage.googleapis.com/v1beta"
39
+ OPENAI_API_URL = "https://api.openai.com/v1"
40
+ OPENROUTER_API_URL = "https://openrouter.ai/api/v1"
41
+
42
+ # LLM model settings
43
+ DEFAULT_GEMINI_MODEL = os.getenv("DEFAULT_GEMINI_MODEL", "gemini-2.0-flash")
44
+ DEFAULT_OPENAI_MODEL = os.getenv("DEFAULT_OPENAI_MODEL", "gpt-4o")
45
+ DEFAULT_OPENROUTER_MODEL = os.getenv("DEFAULT_OPENROUTER_MODEL", "openai/gpt-4o")
46
+
47
+ # Rule generation settings
48
+ MAX_RULE_LENGTH = int(os.getenv("MAX_RULE_LENGTH", "10000"))
49
+ DEFAULT_TEMPERATURE = float(os.getenv("DEFAULT_TEMPERATURE", "0.7"))
50
+
51
+ # LLM Adapter Interface
52
+ class LLMAdapter(ABC):
53
+ """Base adapter interface for LLM providers."""
54
+
55
+ @abstractmethod
56
+ def initialize(self, api_key: str, **kwargs) -> None:
57
+ """Initialize the adapter with API key and optional parameters."""
58
+ pass
59
+
60
+ @abstractmethod
61
+ def validate_api_key(self, api_key: str) -> bool:
62
+ """Validate the API key."""
63
+ pass
64
+
65
+ @abstractmethod
66
+ def get_available_models(self) -> List[Dict[str, str]]:
67
+ """Get a list of available models from the provider."""
68
+ pass
69
+
70
+ @abstractmethod
71
+ def generate_rule(
72
+ self,
73
+ model: str,
74
+ rule_type: str,
75
+ description: str,
76
+ content: str,
77
+ parameters: Optional[Dict[str, Any]] = None
78
+ ) -> str:
79
+ """Generate a Cursor Rule using the LLM provider."""
80
+ pass
81
+
82
+ # Gemini Adapter
83
+ class GeminiAdapter(LLMAdapter):
84
+ """Adapter for Google's Gemini API."""
85
+
86
+ def __init__(self):
87
+ """Initialize the Gemini adapter."""
88
+ self.api_key = None
89
+ self.api_url = Settings.GEMINI_API_URL
90
+ self.initialized = False
91
+ self.last_error = None
92
+
93
+ def initialize(self, api_key: str, **kwargs) -> None:
94
+ """Initialize the adapter with API key and optional parameters."""
95
+ self.api_key = api_key
96
+ self.api_url = kwargs.get('api_url', Settings.GEMINI_API_URL)
97
+ self.initialized = True
98
+
99
+ def validate_api_key(self, api_key: str) -> bool:
100
+ """Validate the Gemini API key."""
101
+ try:
102
+ # Try to list models with the provided API key
103
+ url = f"{self.api_url}/models?key={api_key}"
104
+ response = requests.get(url)
105
+
106
+ # Check if the request was successful
107
+ if response.status_code == 200:
108
+ return True
109
+
110
+ # Store error details for debugging
111
+ self.last_error = f"API Error: Status {response.status_code}, Response: {response.text}"
112
+ print(f"Gemini API validation failed: {self.last_error}")
113
+ return False
114
+ except Exception as e:
115
+ # Store exception details for debugging
116
+ self.last_error = f"Exception: {str(e)}\n{traceback.format_exc()}"
117
+ print(f"Gemini API validation exception: {self.last_error}")
118
+ return False
119
+
120
+ def get_available_models(self) -> List[Dict[str, str]]:
121
+ """Get a list of available Gemini models."""
122
+ if not self.initialized:
123
+ raise ValueError("Adapter not initialized. Call initialize() first.")
124
+
125
+ try:
126
+ # Get available models
127
+ url = f"{self.api_url}/models?key={self.api_key}"
128
+ response = requests.get(url)
129
+
130
+ if response.status_code != 200:
131
+ print(f"Failed to get models: Status {response.status_code}, Response: {response.text}")
132
+ raise ValueError(f"Failed to get models: {response.text}")
133
+
134
+ data = response.json()
135
+
136
+ # Filter for Gemini models and format the response
137
+ models = []
138
+ for model in data.get('models', []):
139
+ if 'gemini' in model.get('name', '').lower():
140
+ model_id = model.get('name').split('/')[-1]
141
+ models.append({
142
+ 'id': model_id,
143
+ 'name': self._format_model_name(model_id)
144
+ })
145
+
146
+ # If no models found, return default models
147
+ if not models:
148
+ models = [
149
+ {'id': 'gemini-2.5-pro', 'name': 'Gemini 2.5 Pro'},
150
+ {'id': 'gemini-2.0-flash', 'name': 'Gemini 2.0 Flash'},
151
+ {'id': 'gemini-2.0-flash-lite', 'name': 'Gemini 2.0 Flash-Lite'}
152
+ ]
153
+
154
+ return models
155
+ except Exception as e:
156
+ print(f"Exception in get_available_models: {str(e)}\n{traceback.format_exc()}")
157
+ # Return default models on error
158
+ return [
159
+ {'id': 'gemini-2.5-pro', 'name': 'Gemini 2.5 Pro'},
160
+ {'id': 'gemini-2.0-flash', 'name': 'Gemini 2.0 Flash'},
161
+ {'id': 'gemini-2.0-flash-lite', 'name': 'Gemini 2.0 Flash-Lite'}
162
+ ]
163
+
164
+ def generate_rule(
165
+ self,
166
+ model: str,
167
+ rule_type: str,
168
+ description: str,
169
+ content: str,
170
+ parameters: Optional[Dict[str, Any]] = None
171
+ ) -> str:
172
+ """Generate a Cursor Rule using Gemini."""
173
+ if not self.initialized:
174
+ raise ValueError("Adapter not initialized. Call initialize() first.")
175
+
176
+ # Set default parameters if not provided
177
+ if parameters is None:
178
+ parameters = {}
179
+
180
+ # Extract parameters
181
+ temperature = parameters.get('temperature', Settings.DEFAULT_TEMPERATURE)
182
+ globs = parameters.get('globs', '')
183
+ referenced_files = parameters.get('referenced_files', '')
184
+ prompt = parameters.get('prompt', '')
185
+
186
+ # Prepare the prompt for Gemini
187
+ system_prompt = """
188
+ You are a Cursor Rules expert. Create a rule in MDC format based on the provided information.
189
+
190
+ MDC format example:
191
+ ---
192
+ description: RPC Service boilerplate
193
+ globs:
194
+ alwaysApply: false
195
+ ---
196
+
197
+ - Use our internal RPC pattern when defining services
198
+ - Always use snake_case for service names.
199
+
200
+ @service-template.ts
201
+ """
202
+
203
+ user_prompt = f"""
204
+ Create a Cursor Rule with the following details:
205
+
206
+ Rule Type: {rule_type}
207
+ Description: {description}
208
+ Content: {content}
209
+ """
210
+
211
+ if globs:
212
+ user_prompt += f"\nGlobs: {globs}"
213
+
214
+ if referenced_files:
215
+ user_prompt += f"\nReferenced Files: {referenced_files}"
216
+
217
+ if prompt:
218
+ user_prompt += f"\nAdditional Instructions: {prompt}"
219
+
220
+ # Prepare the API request
221
+ url = f"{self.api_url}/models/{model}:generateContent?key={self.api_key}"
222
+
223
+ payload = {
224
+ "contents": [
225
+ {
226
+ "role": "user",
227
+ "parts": [
228
+ {"text": system_prompt + "\n\n" + user_prompt}
229
+ ]
230
+ }
231
+ ],
232
+ "generationConfig": {
233
+ "temperature": temperature,
234
+ "topP": 0.8,
235
+ "topK": 40,
236
+ "maxOutputTokens": 2048
237
+ }
238
+ }
239
+
240
+ # Make the API request
241
+ try:
242
+ response = requests.post(url, json=payload)
243
+
244
+ if response.status_code != 200:
245
+ print(f"Failed to generate rule: Status {response.status_code}, Response: {response.text}")
246
+ raise ValueError(f"Failed to generate rule: {response.text}")
247
+
248
+ data = response.json()
249
+
250
+ # Extract the generated text
251
+ generated_text = data.get('candidates', [{}])[0].get('content', {}).get('parts', [{}])[0].get('text', '')
252
+
253
+ # If no text was generated, create a basic rule
254
+ if not generated_text:
255
+ return self._create_basic_rule(rule_type, description, content, globs, referenced_files)
256
+
257
+ return generated_text
258
+ except Exception as e:
259
+ print(f"Exception in generate_rule: {str(e)}\n{traceback.format_exc()}")
260
+ # Create a basic rule on error
261
+ return self._create_basic_rule(rule_type, description, content, globs, referenced_files)
262
+
263
+ def _format_model_name(self, model_id: str) -> str:
264
+ """Format a model ID into a human-readable name."""
265
+ # Replace hyphens with spaces and capitalize each word
266
+ name = model_id.replace('-', ' ').title()
267
+
268
+ # Special case handling
269
+ name = name.replace('Gemini ', 'Gemini ')
270
+ name = name.replace('Pro ', 'Pro ')
271
+ name = name.replace('Flash ', 'Flash ')
272
+ name = name.replace('Lite', 'Lite')
273
+
274
+ return name
275
+
276
+ def _create_basic_rule(
277
+ self,
278
+ rule_type: str,
279
+ description: str,
280
+ content: str,
281
+ globs: str = '',
282
+ referenced_files: str = ''
283
+ ) -> str:
284
+ """Create a basic rule in MDC format without using the LLM."""
285
+ # Create MDC format
286
+ mdc = '---\n'
287
+ mdc += f'description: {description}\n'
288
+
289
+ if rule_type == 'Auto Attached' and globs:
290
+ mdc += f'globs: {globs}\n'
291
+
292
+ if rule_type == 'Always':
293
+ mdc += 'alwaysApply: true\n'
294
+ else:
295
+ mdc += 'alwaysApply: false\n'
296
+
297
+ mdc += '---\n\n'
298
+ mdc += content + '\n'
299
+
300
+ # Add referenced files
301
+ if referenced_files:
302
+ mdc += '\n' + referenced_files
303
+
304
+ return mdc
305
+
306
+ # OpenAI Adapter
307
+ class OpenAIAdapter(LLMAdapter):
308
+ """Adapter for OpenAI API."""
309
+
310
+ def __init__(self):
311
+ """Initialize the OpenAI adapter."""
312
+ self.api_key = None
313
+ self.api_url = Settings.OPENAI_API_URL
314
+ self.initialized = False
315
+ self.last_error = None
316
+
317
+ def initialize(self, api_key: str, **kwargs) -> None:
318
+ """Initialize the adapter with API key and optional parameters."""
319
+ self.api_key = api_key
320
+ self.api_url = kwargs.get('api_url', Settings.OPENAI_API_URL)
321
+ self.initialized = True
322
+
323
+ def validate_api_key(self, api_key: str) -> bool:
324
+ """Validate the OpenAI API key."""
325
+ try:
326
+ # Try to list models with the provided API key
327
+ url = f"{self.api_url}/models"
328
+ headers = {
329
+ "Authorization": f"Bearer {api_key}"
330
+ }
331
+ response = requests.get(url, headers=headers)
332
+
333
+ # Check if the request was successful
334
+ if response.status_code == 200:
335
+ return True
336
+
337
+ # Store error details for debugging
338
+ self.last_error = f"API Error: Status {response.status_code}, Response: {response.text}"
339
+ print(f"OpenAI API validation failed: {self.last_error}")
340
+ return False
341
+ except Exception as e:
342
+ # Store exception details for debugging
343
+ self.last_error = f"Exception: {str(e)}\n{traceback.format_exc()}"
344
+ print(f"OpenAI API validation exception: {self.last_error}")
345
+ return False
346
+
347
+ def get_available_models(self) -> List[Dict[str, str]]:
348
+ """Get a list of available OpenAI models."""
349
+ if not self.initialized:
350
+ raise ValueError("Adapter not initialized. Call initialize() first.")
351
+
352
+ try:
353
+ # Get available models
354
+ url = f"{self.api_url}/models"
355
+ headers = {
356
+ "Authorization": f"Bearer {self.api_key}"
357
+ }
358
+ response = requests.get(url, headers=headers)
359
+
360
+ if response.status_code != 200:
361
+ print(f"Failed to get models: Status {response.status_code}, Response: {response.text}")
362
+ raise ValueError(f"Failed to get models: {response.text}")
363
+
364
+ data = response.json()
365
+
366
+ # Filter for chat models and format the response
367
+ models = []
368
+ for model in data.get('data', []):
369
+ model_id = model.get('id')
370
+ if any(prefix in model_id for prefix in ['gpt-4', 'gpt-3.5']):
371
+ models.append({
372
+ 'id': model_id,
373
+ 'name': self._format_model_name(model_id)
374
+ })
375
+
376
+ # If no models found, return default models
377
+ if not models:
378
+ models = [
379
+ {'id': 'gpt-4o', 'name': 'GPT-4o'},
380
+ {'id': 'gpt-4-turbo', 'name': 'GPT-4 Turbo'},
381
+ {'id': 'gpt-3.5-turbo', 'name': 'GPT-3.5 Turbo'}
382
+ ]
383
+
384
+ return models
385
+ except Exception as e:
386
+ print(f"Exception in get_available_models: {str(e)}\n{traceback.format_exc()}")
387
+ # Return default models on error
388
+ return [
389
+ {'id': 'gpt-4o', 'name': 'GPT-4o'},
390
+ {'id': 'gpt-4-turbo', 'name': 'GPT-4 Turbo'},
391
+ {'id': 'gpt-3.5-turbo', 'name': 'GPT-3.5 Turbo'}
392
+ ]
393
+
394
+ def generate_rule(
395
+ self,
396
+ model: str,
397
+ rule_type: str,
398
+ description: str,
399
+ content: str,
400
+ parameters: Optional[Dict[str, Any]] = None
401
+ ) -> str:
402
+ """Generate a Cursor Rule using OpenAI."""
403
+ if not self.initialized:
404
+ raise ValueError("Adapter not initialized. Call initialize() first.")
405
+
406
+ # Set default parameters if not provided
407
+ if parameters is None:
408
+ parameters = {}
409
+
410
+ # Extract parameters
411
+ temperature = parameters.get('temperature', Settings.DEFAULT_TEMPERATURE)
412
+ globs = parameters.get('globs', '')
413
+ referenced_files = parameters.get('referenced_files', '')
414
+ prompt = parameters.get('prompt', '')
415
+
416
+ # Prepare the prompt for OpenAI
417
+ system_prompt = """
418
+ You are a Cursor Rules expert. Create a rule in MDC format based on the provided information.
419
+
420
+ MDC format example:
421
+ ---
422
+ description: RPC Service boilerplate
423
+ globs:
424
+ alwaysApply: false
425
+ ---
426
+
427
+ - Use our internal RPC pattern when defining services
428
+ - Always use snake_case for service names.
429
+
430
+ @service-template.ts
431
+ """
432
+
433
+ user_prompt = f"""
434
+ Create a Cursor Rule with the following details:
435
+
436
+ Rule Type: {rule_type}
437
+ Description: {description}
438
+ Content: {content}
439
+ """
440
+
441
+ if globs:
442
+ user_prompt += f"\nGlobs: {globs}"
443
+
444
+ if referenced_files:
445
+ user_prompt += f"\nReferenced Files: {referenced_files}"
446
+
447
+ if prompt:
448
+ user_prompt += f"\nAdditional Instructions: {prompt}"
449
+
450
+ # Prepare the API request
451
+ url = f"{self.api_url}/chat/completions"
452
+ headers = {
453
+ "Authorization": f"Bearer {self.api_key}",
454
+ "Content-Type": "application/json"
455
+ }
456
+
457
+ payload = {
458
+ "model": model,
459
+ "messages": [
460
+ {
461
+ "role": "system",
462
+ "content": system_prompt
463
+ },
464
+ {
465
+ "role": "user",
466
+ "content": user_prompt
467
+ }
468
+ ],
469
+ "temperature": temperature,
470
+ "max_tokens": 2048
471
+ }
472
+
473
+ # Make the API request
474
+ try:
475
+ response = requests.post(url, headers=headers, json=payload)
476
+
477
+ if response.status_code != 200:
478
+ print(f"Failed to generate rule: Status {response.status_code}, Response: {response.text}")
479
+ raise ValueError(f"Failed to generate rule: {response.text}")
480
+
481
+ data = response.json()
482
+
483
+ # Extract the generated text
484
+ generated_text = data.get('choices', [{}])[0].get('message', {}).get('content', '')
485
+
486
+ # If no text was generated, create a basic rule
487
+ if not generated_text:
488
+ return self._create_basic_rule(rule_type, description, content, globs, referenced_files)
489
+
490
+ return generated_text
491
+ except Exception as e:
492
+ print(f"Exception in generate_rule: {str(e)}\n{traceback.format_exc()}")
493
+ # Create a basic rule on error
494
+ return self._create_basic_rule(rule_type, description, content, globs, referenced_files)
495
+
496
+ def _format_model_name(self, model_id: str) -> str:
497
+ """Format a model ID into a human-readable name."""
498
+ # Replace hyphens with spaces and capitalize each word
499
+ name = model_id.replace('-', ' ').title()
500
+
501
+ # Special case handling
502
+ name = name.replace('Gpt ', 'GPT ')
503
+ name = name.replace('Gpt4', 'GPT-4')
504
+ name = name.replace('Gpt3', 'GPT-3')
505
+ name = name.replace('Gpt 4', 'GPT-4')
506
+ name = name.replace('Gpt 3', 'GPT-3')
507
+ name = name.replace('Turbo', 'Turbo')
508
+ name = name.replace('O', 'o')
509
+
510
+ return name
511
+
512
+ def _create_basic_rule(
513
+ self,
514
+ rule_type: str,
515
+ description: str,
516
+ content: str,
517
+ globs: str = '',
518
+ referenced_files: str = ''
519
+ ) -> str:
520
+ """Create a basic rule in MDC format without using the LLM."""
521
+ # Create MDC format
522
+ mdc = '---\n'
523
+ mdc += f'description: {description}\n'
524
+
525
+ if rule_type == 'Auto Attached' and globs:
526
+ mdc += f'globs: {globs}\n'
527
+
528
+ if rule_type == 'Always':
529
+ mdc += 'alwaysApply: true\n'
530
+ else:
531
+ mdc += 'alwaysApply: false\n'
532
+
533
+ mdc += '---\n\n'
534
+ mdc += content + '\n'
535
+
536
+ # Add referenced files
537
+ if referenced_files:
538
+ mdc += '\n' + referenced_files
539
+
540
+ return mdc
541
+
542
+ # OpenRouter Adapter
543
+ class OpenRouterAdapter(LLMAdapter):
544
+ """Adapter for OpenRouter API."""
545
+
546
+ def __init__(self):
547
+ """Initialize the OpenRouter adapter."""
548
+ self.api_key = None
549
+ self.api_url = Settings.OPENROUTER_API_URL
550
+ self.initialized = False
551
+ self.last_error = None
552
+
553
+ def initialize(self, api_key: str, **kwargs) -> None:
554
+ """Initialize the adapter with API key and optional parameters."""
555
+ self.api_key = api_key
556
+ self.api_url = kwargs.get('api_url', Settings.OPENROUTER_API_URL)
557
+ self.site_url = kwargs.get('site_url', 'https://cursor-rules-generator.example.com')
558
+ self.site_name = kwargs.get('site_name', 'Cursor Rules Generator')
559
+ self.initialized = True
560
+
561
+ def validate_api_key(self, api_key: str) -> bool:
562
+ """Validate the OpenRouter API key."""
563
+ try:
564
+ # Try to list models with the provided API key
565
+ url = f"{self.api_url}/models"
566
+ headers = {
567
+ "Authorization": f"Bearer {api_key}"
568
+ }
569
+ response = requests.get(url, headers=headers)
570
+
571
+ # Check if the request was successful
572
+ if response.status_code == 200:
573
+ return True
574
+
575
+ # Store error details for debugging
576
+ self.last_error = f"API Error: Status {response.status_code}, Response: {response.text}"
577
+ print(f"OpenRouter API validation failed: {self.last_error}")
578
+ return False
579
+ except Exception as e:
580
+ # Store exception details for debugging
581
+ self.last_error = f"Exception: {str(e)}\n{traceback.format_exc()}"
582
+ print(f"OpenRouter API validation exception: {self.last_error}")
583
+ return False
584
+
585
+ def get_available_models(self) -> List[Dict[str, str]]:
586
+ """Get a list of available OpenRouter models."""
587
+ if not self.initialized:
588
+ raise ValueError("Adapter not initialized. Call initialize() first.")
589
+
590
+ try:
591
+ # Get available models
592
+ url = f"{self.api_url}/models"
593
+ headers = {
594
+ "Authorization": f"Bearer {self.api_key}"
595
+ }
596
+ response = requests.get(url, headers=headers)
597
+
598
+ if response.status_code != 200:
599
+ print(f"Failed to get models: Status {response.status_code}, Response: {response.text}")
600
+ raise ValueError(f"Failed to get models: {response.text}")
601
+
602
+ data = response.json()
603
+
604
+ # Format the response
605
+ models = []
606
+ for model in data.get('data', []):
607
+ model_id = model.get('id')
608
+ model_name = model.get('name', model_id)
609
+
610
+ # Skip non-chat models
611
+ if not model.get('capabilities', {}).get('chat'):
612
+ continue
613
+
614
+ models.append({
615
+ 'id': model_id,
616
+ 'name': model_name
617
+ })
618
+
619
+ # If no models found, return default models
620
+ if not models:
621
+ models = [
622
+ {'id': 'openai/gpt-4o', 'name': 'OpenAI GPT-4o'},
623
+ {'id': 'anthropic/claude-3-opus', 'name': 'Anthropic Claude 3 Opus'},
624
+ {'id': 'google/gemini-2.5-pro', 'name': 'Google Gemini 2.5 Pro'},
625
+ {'id': 'meta-llama/llama-3-70b-instruct', 'name': 'Meta Llama 3 70B'}
626
+ ]
627
+
628
+ return models
629
+ except Exception as e:
630
+ print(f"Exception in get_available_models: {str(e)}\n{traceback.format_exc()}")
631
+ # Return default models on error
632
+ return [
633
+ {'id': 'openai/gpt-4o', 'name': 'OpenAI GPT-4o'},
634
+ {'id': 'anthropic/claude-3-opus', 'name': 'Anthropic Claude 3 Opus'},
635
+ {'id': 'google/gemini-2.5-pro', 'name': 'Google Gemini 2.5 Pro'},
636
+ {'id': 'meta-llama/llama-3-70b-instruct', 'name': 'Meta Llama 3 70B'}
637
+ ]
638
+
639
+ def generate_rule(
640
+ self,
641
+ model: str,
642
+ rule_type: str,
643
+ description: str,
644
+ content: str,
645
+ parameters: Optional[Dict[str, Any]] = None
646
+ ) -> str:
647
+ """Generate a Cursor Rule using OpenRouter."""
648
+ if not self.initialized:
649
+ raise ValueError("Adapter not initialized. Call initialize() first.")
650
+
651
+ # Set default parameters if not provided
652
+ if parameters is None:
653
+ parameters = {}
654
+
655
+ # Extract parameters
656
+ temperature = parameters.get('temperature', Settings.DEFAULT_TEMPERATURE)
657
+ globs = parameters.get('globs', '')
658
+ referenced_files = parameters.get('referenced_files', '')
659
+ prompt = parameters.get('prompt', '')
660
+
661
+ # Prepare the prompt for OpenRouter
662
+ system_prompt = """
663
+ You are a Cursor Rules expert. Create a rule in MDC format based on the provided information.
664
+
665
+ MDC format example:
666
+ ---
667
+ description: RPC Service boilerplate
668
+ globs:
669
+ alwaysApply: false
670
+ ---
671
+
672
+ - Use our internal RPC pattern when defining services
673
+ - Always use snake_case for service names.
674
+
675
+ @service-template.ts
676
+ """
677
+
678
+ user_prompt = f"""
679
+ Create a Cursor Rule with the following details:
680
+
681
+ Rule Type: {rule_type}
682
+ Description: {description}
683
+ Content: {content}
684
+ """
685
+
686
+ if globs:
687
+ user_prompt += f"\nGlobs: {globs}"
688
+
689
+ if referenced_files:
690
+ user_prompt += f"\nReferenced Files: {referenced_files}"
691
+
692
+ if prompt:
693
+ user_prompt += f"\nAdditional Instructions: {prompt}"
694
+
695
+ # Prepare the API request
696
+ url = f"{self.api_url}/chat/completions"
697
+ headers = {
698
+ "Authorization": f"Bearer {self.api_key}",
699
+ "Content-Type": "application/json",
700
+ "HTTP-Referer": self.site_url,
701
+ "X-Title": self.site_name
702
+ }
703
+
704
+ payload = {
705
+ "model": model,
706
+ "messages": [
707
+ {
708
+ "role": "system",
709
+ "content": system_prompt
710
+ },
711
+ {
712
+ "role": "user",
713
+ "content": user_prompt
714
+ }
715
+ ],
716
+ "temperature": temperature,
717
+ "max_tokens": 2048
718
+ }
719
+
720
+ # Make the API request
721
+ try:
722
+ response = requests.post(url, headers=headers, json=payload)
723
+
724
+ if response.status_code != 200:
725
+ print(f"Failed to generate rule: Status {response.status_code}, Response: {response.text}")
726
+ raise ValueError(f"Failed to generate rule: {response.text}")
727
+
728
+ data = response.json()
729
+
730
+ # Extract the generated text
731
+ generated_text = data.get('choices', [{}])[0].get('message', {}).get('content', '')
732
+
733
+ # If no text was generated, create a basic rule
734
+ if not generated_text:
735
+ return self._create_basic_rule(rule_type, description, content, globs, referenced_files)
736
+
737
+ return generated_text
738
+ except Exception as e:
739
+ print(f"Exception in generate_rule: {str(e)}\n{traceback.format_exc()}")
740
+ # Create a basic rule on error
741
+ return self._create_basic_rule(rule_type, description, content, globs, referenced_files)
742
+
743
+ def _create_basic_rule(
744
+ self,
745
+ rule_type: str,
746
+ description: str,
747
+ content: str,
748
+ globs: str = '',
749
+ referenced_files: str = ''
750
+ ) -> str:
751
+ """Create a basic rule in MDC format without using the LLM."""
752
+ # Create MDC format
753
+ mdc = '---\n'
754
+ mdc += f'description: {description}\n'
755
+
756
+ if rule_type == 'Auto Attached' and globs:
757
+ mdc += f'globs: {globs}\n'
758
+
759
+ if rule_type == 'Always':
760
+ mdc += 'alwaysApply: true\n'
761
+ else:
762
+ mdc += 'alwaysApply: false\n'
763
+
764
+ mdc += '---\n\n'
765
+ mdc += content + '\n'
766
+
767
+ # Add referenced files
768
+ if referenced_files:
769
+ mdc += '\n' + referenced_files
770
+
771
+ return mdc
772
+
773
+ # LLM Adapter Factory
774
+ class LLMAdapterFactory:
775
+ """Factory for creating LLM adapters."""
776
+
777
+ @staticmethod
778
+ def create_adapter(provider_name: str) -> LLMAdapter:
779
+ """Create an adapter for the specified provider."""
780
+ provider_name = provider_name.lower()
781
+
782
+ if provider_name == "gemini":
783
+ return GeminiAdapter()
784
+ elif provider_name == "openai":
785
+ return OpenAIAdapter()
786
+ elif provider_name == "openrouter":
787
+ return OpenRouterAdapter()
788
+ else:
789
+ raise ValueError(f"Unsupported provider: {provider_name}")
790
+
791
+ @staticmethod
792
+ def get_supported_providers() -> Dict[str, str]:
793
+ """Get a dictionary of supported providers."""
794
+ return {
795
+ "gemini": "Google Gemini",
796
+ "openai": "OpenAI",
797
+ "openrouter": "OpenRouter"
798
+ }
799
+
800
+ # Rule Generator
801
+ class RuleGenerator:
802
+ """Engine for generating Cursor Rules."""
803
+
804
+ def __init__(self):
805
+ """Initialize the rule generator."""
806
+ self.factory = LLMAdapterFactory()
807
+
808
+ def create_rule(
809
+ self,
810
+ provider: str,
811
+ model: str,
812
+ rule_type: str,
813
+ description: str,
814
+ content: str,
815
+ api_key: str,
816
+ parameters: Optional[Dict[str, Any]] = None
817
+ ) -> str:
818
+ """Create a Cursor Rule using the specified LLM provider."""
819
+ # Set default parameters if not provided
820
+ if parameters is None:
821
+ parameters = {}
822
+
823
+ try:
824
+ # Create and initialize the adapter
825
+ adapter = self.factory.create_adapter(provider)
826
+ adapter.initialize(api_key)
827
+
828
+ # Generate the rule using the adapter
829
+ rule = adapter.generate_rule(model, rule_type, description, content, parameters)
830
+
831
+ return rule
832
+ except Exception as e:
833
+ print(f"Exception in create_rule: {str(e)}\n{traceback.format_exc()}")
834
+ # If LLM generation fails, create a basic rule
835
+ return self._create_basic_rule(rule_type, description, content, parameters)
836
+
837
+ def _create_basic_rule(
838
+ self,
839
+ rule_type: str,
840
+ description: str,
841
+ content: str,
842
+ parameters: Optional[Dict[str, Any]] = None
843
+ ) -> str:
844
+ """Create a basic rule in MDC format without using an LLM."""
845
+ # Set default parameters if not provided
846
+ if parameters is None:
847
+ parameters = {}
848
+
849
+ # Extract parameters
850
+ globs = parameters.get('globs', '')
851
+ referenced_files = parameters.get('referenced_files', '')
852
+
853
+ # Create MDC format
854
+ mdc = '---\n'
855
+ mdc += f'description: {description}\n'
856
+
857
+ if rule_type == 'Auto Attached' and globs:
858
+ mdc += f'globs: {globs}\n'
859
+
860
+ if rule_type == 'Always':
861
+ mdc += 'alwaysApply: true\n'
862
+ else:
863
+ mdc += 'alwaysApply: false\n'
864
+
865
+ mdc += '---\n\n'
866
+ mdc += content + '\n'
867
+
868
+ # Add referenced files
869
+ if referenced_files:
870
+ mdc += '\n' + referenced_files
871
+
872
+ return mdc
873
+
874
+ def validate_rule_type(self, rule_type: str) -> bool:
875
+ """Validate if the rule type is supported."""
876
+ valid_types = ['Always', 'Auto Attached', 'Agent Requested', 'Manual']
877
+ return rule_type in valid_types
878
+
879
+ def get_rule_types(self) -> List[Dict[str, str]]:
880
+ """Get a list of supported rule types."""
881
+ return [
882
+ {
883
+ 'id': 'Always',
884
+ 'name': 'Always',
885
+ 'description': 'Always included in the model context'
886
+ },
887
+ {
888
+ 'id': 'Auto Attached',
889
+ 'name': 'Auto Attached',
890
+ 'description': 'Included when files matching glob patterns are referenced'
891
+ },
892
+ {
893
+ 'id': 'Agent Requested',
894
+ 'name': 'Agent Requested',
895
+ 'description': 'Rule is presented to the AI, which decides whether to include it'
896
+ },
897
+ {
898
+ 'id': 'Manual',
899
+ 'name': 'Manual',
900
+ 'description': 'Only included when explicitly referenced using @ruleName'
901
+ }
902
+ ]
903
+
904
+ # Initialize components
905
+ rule_generator = RuleGenerator()
906
+ factory = LLMAdapterFactory()
907
+
908
+ # Get supported providers
909
+ providers = factory.get_supported_providers()
910
+ provider_choices = list(providers.keys())
911
+
912
+ # Get rule types
913
+ rule_types = rule_generator.get_rule_types()
914
+ rule_type_choices = [rt['id'] for rt in rule_types]
915
+
916
+ def validate_api_key(provider, api_key):
917
+ """Validate an API key for a specific provider.
918
+
919
+ Args:
920
+ provider: The LLM provider
921
+ api_key: The API key to validate
922
+
923
+ Returns:
924
+ tuple: (success, message, models)
925
+ """
926
+ if not provider or not api_key:
927
+ return False, "Lütfen bir sağlayıcı seçin ve API anahtarı girin.", [], []
928
+
929
+ try:
930
+ # Create the adapter
931
+ adapter = factory.create_adapter(provider)
932
+
933
+ # Print debug info
934
+ print(f"Validating {provider} API key: {api_key[:5]}...{api_key[-5:] if len(api_key) > 10 else ''}")
935
+
936
+ # Validate the API key
937
+ valid = adapter.validate_api_key(api_key)
938
+
939
+ if valid:
940
+ # Initialize the adapter
941
+ adapter.initialize(api_key)
942
+
943
+ # Get available models
944
+ models = adapter.get_available_models()
945
+ model_names = [model['name'] for model in models]
946
+ model_ids = [model['id'] for model in models]
947
+
948
+ return True, "API anahtarı doğrulandı.", model_names, model_ids
949
+ else:
950
+ error_msg = getattr(adapter, 'last_error', 'Bilinmeyen hata')
951
+ return False, f"Geçersiz API anahtarı. Hata: {error_msg}", [], []
952
+ except Exception as e:
953
+ error_details = traceback.format_exc()
954
+ print(f"Exception in validate_api_key: {str(e)}\n{error_details}")
955
+ return False, f"Hata: {str(e)}", [], []
956
+
957
+ def generate_rule(provider, api_key, model_index, model_ids, rule_type, description, content, globs, referenced_files, prompt, temperature):
958
+ """Generate a Cursor Rule.
959
+
960
+ Args:
961
+ provider: The LLM provider
962
+ api_key: The API key for the provider
963
+ model_index: The index of the selected model
964
+ model_ids: The list of model IDs
965
+ rule_type: The type of rule to generate
966
+ description: A short description of the rule's purpose
967
+ content: The main content of the rule
968
+ globs: Glob patterns for Auto Attached rules
969
+ referenced_files: Referenced files
970
+ prompt: Additional instructions for the LLM
971
+ temperature: Temperature parameter for generation
972
+
973
+ Returns:
974
+ tuple: (success, message, rule)
975
+ """
976
+ if not provider or not api_key or model_index is None or not rule_type or not description or not content:
977
+ return False, "Lütfen tüm gerekli alanları doldurun.", ""
978
+
979
+ # Get the model ID
980
+ if not model_ids or model_index >= len(model_ids):
981
+ return False, "Geçersiz model seçimi.", ""
982
+
983
+ model = model_ids[model_index]
984
+
985
+ # Validate rule type
986
+ if not rule_generator.validate_rule_type(rule_type):
987
+ return False, f"Geçersiz kural tipi: {rule_type}", ""
988
+
989
+ # Validate globs for Auto Attached rule type
990
+ if rule_type == 'Auto Attached' and not globs:
991
+ return False, "Auto Attached kural tipi için glob desenleri gereklidir.", ""
992
+
993
+ try:
994
+ # Prepare parameters
995
+ parameters = {
996
+ 'globs': globs,
997
+ 'referenced_files': referenced_files,
998
+ 'prompt': prompt,
999
+ 'temperature': float(temperature)
1000
+ }
1001
+
1002
+ # Generate the rule
1003
+ rule = rule_generator.create_rule(
1004
+ provider=provider,
1005
+ model=model,
1006
+ rule_type=rule_type,
1007
+ description=description,
1008
+ content=content,
1009
+ api_key=api_key,
1010
+ parameters=parameters
1011
+ )
1012
+
1013
+ return True, "Kural başarıyla oluşturuldu.", rule
1014
+ except Exception as e:
1015
+ error_details = traceback.format_exc()
1016
+ print(f"Exception in generate_rule: {str(e)}\n{error_details}")
1017
+ return False, f"Kural oluşturulurken bir hata oluştu: {str(e)}", ""
1018
+
1019
+ def update_rule_type_info(rule_type):
1020
+ """Update the rule type information.
1021
+
1022
+ Args:
1023
+ rule_type: The selected rule type
1024
+
1025
+ Returns:
1026
+ str: Information about the selected rule type
1027
+ """
1028
+ if rule_type == 'Always':
1029
+ return "Her zaman model bağlamına dahil edilir."
1030
+ elif rule_type == 'Auto Attached':
1031
+ return "Glob desenine uyan dosyalar referans alındığında dahil edilir."
1032
+ elif rule_type == 'Agent Requested':
1033
+ return "Kural AI'ya sunulur, dahil edilip edilmeyeceğine AI karar verir."
1034
+ elif rule_type == 'Manual':
1035
+ return "Yalnızca @ruleName kullanılarak açıkça belirtildiğinde dahil edilir."
1036
+ else:
1037
+ return ""
1038
+
1039
+ def update_globs_visibility(rule_type):
1040
+ """Update the visibility of the globs input.
1041
+
1042
+ Args:
1043
+ rule_type: The selected rule type
1044
+
1045
+ Returns:
1046
+ bool: Whether the globs input should be visible
1047
+ """
1048
+ return rule_type == 'Auto Attached'
1049
+
1050
+ # Create Gradio interface
1051
+ with gr.Blocks(title="Cursor Rules Oluşturucu") as demo:
1052
+ gr.Markdown("# Cursor Rules Oluşturucu")
1053
+ gr.Markdown("Gemini, OpenRouter, OpenAI API ve tüm modellerini destekleyen dinamik bir Cursor Rules oluşturucu.")
1054
+
1055
+ with gr.Row():
1056
+ with gr.Column():
1057
+ provider = gr.Dropdown(
1058
+ choices=provider_choices,
1059
+ label="LLM Sağlayıcı",
1060
+ value=provider_choices[0] if provider_choices else None
1061
+ )
1062
+
1063
+ api_key = gr.Textbox(
1064
+ label="API Anahtarı",
1065
+ placeholder="API anahtarınızı girin",
1066
+ type="password"
1067
+ )
1068
+
1069
+ validate_btn = gr.Button("API Anahtarını Doğrula")
1070
+
1071
+ api_status = gr.Textbox(
1072
+ label="API Durumu",
1073
+ interactive=False
1074
+ )
1075
+
1076
+ model_dropdown = gr.Dropdown(
1077
+ label="Model",
1078
+ choices=[],
1079
+ interactive=False
1080
+ )
1081
+
1082
+ # Hidden field to store model IDs
1083
+ model_ids = gr.State([])
1084
+
1085
+ rule_type = gr.Dropdown(
1086
+ choices=rule_type_choices,
1087
+ label="Kural Tipi",
1088
+ value=rule_type_choices[0] if rule_type_choices else None
1089
+ )
1090
+
1091
+ rule_type_info = gr.Textbox(
1092
+ label="Kural Tipi Bilgisi",
1093
+ interactive=False,
1094
+ value=update_rule_type_info(rule_type_choices[0] if rule_type_choices else "")
1095
+ )
1096
+
1097
+ description = gr.Textbox(
1098
+ label="Açıklama",
1099
+ placeholder="Kuralın amacını açıklayan kısa bir açıklama"
1100
+ )
1101
+
1102
+ globs = gr.Textbox(
1103
+ label="Glob Desenleri (Auto Attached için)",
1104
+ placeholder="Örn: *.ts, src/*.js",
1105
+ visible=False
1106
+ )
1107
+
1108
+ content = gr.Textbox(
1109
+ label="Kural İçeriği",
1110
+ placeholder="Kuralın ana içeriği",
1111
+ lines=10
1112
+ )
1113
+
1114
+ referenced_files = gr.Textbox(
1115
+ label="Referans Dosyaları (İsteğe bağlı)",
1116
+ placeholder="Her satıra bir dosya adı girin, örn: @service-template.ts",
1117
+ lines=3
1118
+ )
1119
+
1120
+ prompt = gr.Textbox(
1121
+ label="AI Prompt (İsteğe bağlı)",
1122
+ placeholder="AI'ya özel talimatlar verin",
1123
+ lines=3
1124
+ )
1125
+
1126
+ temperature = gr.Slider(
1127
+ label="Sıcaklık",
1128
+ minimum=0.0,
1129
+ maximum=1.0,
1130
+ value=0.7,
1131
+ step=0.1
1132
+ )
1133
+
1134
+ generate_btn = gr.Button("Kural Oluştur")
1135
+
1136
+ with gr.Column():
1137
+ generation_status = gr.Textbox(
1138
+ label="Durum",
1139
+ interactive=False
1140
+ )
1141
+
1142
+ rule_output = gr.Textbox(
1143
+ label="Oluşturulan Kural",
1144
+ lines=20,
1145
+ interactive=False
1146
+ )
1147
+
1148
+ download_btn = gr.Button("İndir")
1149
+
1150
+ # API key validation
1151
+ validate_btn.click(
1152
+ fn=validate_api_key,
1153
+ inputs=[provider, api_key],
1154
+ outputs=[api_status, model_dropdown, model_ids]
1155
+ )
1156
+
1157
+ # Rule type change
1158
+ rule_type.change(
1159
+ fn=update_rule_type_info,
1160
+ inputs=[rule_type],
1161
+ outputs=[rule_type_info]
1162
+ )
1163
+
1164
+ rule_type.change(
1165
+ fn=update_globs_visibility,
1166
+ inputs=[rule_type],
1167
+ outputs=[globs]
1168
+ )
1169
+
1170
+ # Generate rule
1171
+ generate_btn.click(
1172
+ fn=generate_rule,
1173
+ inputs=[
1174
+ provider,
1175
+ api_key,
1176
+ model_dropdown,
1177
+ model_ids,
1178
+ rule_type,
1179
+ description,
1180
+ content,
1181
+ globs,
1182
+ referenced_files,
1183
+ prompt,
1184
+ temperature
1185
+ ],
1186
+ outputs=[generation_status, rule_output]
1187
+ )
1188
+
1189
+ # Download rule
1190
+ def download_rule(rule, description):
1191
+ if not rule:
1192
+ return None
1193
+
1194
+ # Create file name from description
1195
+ file_name = description.lower().replace(" ", "-").replace("/", "-")
1196
+ if not file_name:
1197
+ file_name = "cursor-rule"
1198
+
1199
+ return {
1200
+ "name": f"{file_name}.mdc",
1201
+ "data": rule
1202
+ }
1203
+
1204
+ download_btn.click(
1205
+ fn=download_rule,
1206
+ inputs=[rule_output, description],
1207
+ outputs=[gr.File()]
1208
+ )
1209
+
1210
+ # Launch the app
1211
+ if __name__ == "__main__":
1212
+ demo.launch(
1213
+ server_name="0.0.0.0",
1214
+ server_port=int(os.environ.get("PORT", 7860)),
1215
+ share=True
1216
+ )