diff --git a/backend/core/__pycache__/group_settings_manager.cpython-310.pyc b/backend/core/__pycache__/group_settings_manager.cpython-310.pyc new file mode 100644 index 0000000..f3b3b6e Binary files /dev/null and b/backend/core/__pycache__/group_settings_manager.cpython-310.pyc differ diff --git a/backend/core/__pycache__/profile_manager.cpython-310.pyc b/backend/core/__pycache__/profile_manager.cpython-310.pyc index bfba5bc..0851fe0 100644 Binary files a/backend/core/__pycache__/profile_manager.cpython-310.pyc and b/backend/core/__pycache__/profile_manager.cpython-310.pyc differ diff --git a/backend/core/__pycache__/script_manager.cpython-310.pyc b/backend/core/__pycache__/script_manager.cpython-310.pyc index bbb3859..f9f5dbb 100644 Binary files a/backend/core/__pycache__/script_manager.cpython-310.pyc and b/backend/core/__pycache__/script_manager.cpython-310.pyc differ diff --git a/backend/core/group_settings_manager.py b/backend/core/group_settings_manager.py new file mode 100644 index 0000000..9063620 --- /dev/null +++ b/backend/core/group_settings_manager.py @@ -0,0 +1,121 @@ +from pathlib import Path +import json +from typing import Dict, Any +from datetime import datetime +import os + + +class GroupSettingsManager: + """Manages settings for script groups""" + + def __init__(self, script_groups_dir: Path): + self.script_groups_dir = script_groups_dir + self.config_schema = self._load_config_schema() + + def _load_config_schema(self) -> Dict[str, Any]: + """Load the main configuration schema for script groups""" + schema_file = self.script_groups_dir / "config.json" + try: + with open(schema_file, "r", encoding="utf-8") as f: + return json.load(f) + except Exception as e: + print(f"Error loading group config schema: {e}") + return { + "config_schema": { + "work_dir": { + "type": "string", + "description": "Working directory for this script group", + "required": True, + }, + "description": { + "type": "string", + "description": "Description of this script group", + "default": "", + }, + } + } + + def _validate_setting( + self, key: str, value: Any, field_schema: Dict[str, Any] + ) -> Any: + """Validate and convert a single setting value""" + field_type = field_schema.get("type") + + if value is None or value == "": + if field_schema.get("required", False): + raise ValueError(f"Field '{key}' is required") + return field_schema.get("default") + + try: + if field_type == "string": + return str(value) + elif field_type == "number": + return float(value) if "." in str(value) else int(value) + elif field_type == "boolean": + if isinstance(value, str): + return value.lower() == "true" + return bool(value) + elif field_type == "directory": + path = Path(value) + if not path.is_absolute(): + path = Path(self.script_groups_dir) / path + if not path.exists(): + path.mkdir(parents=True, exist_ok=True) + return str(path) + else: + return value + except Exception as e: + raise ValueError(f"Invalid value for field '{key}': {str(e)}") + + def get_group_settings(self, group_id: str) -> Dict[str, Any]: + """Get settings for a specific script group""" + settings_file = self.script_groups_dir / group_id / "group.json" + + if settings_file.exists(): + try: + with open(settings_file, "r", encoding="utf-8") as f: + return json.load(f) + except Exception as e: + print(f"Error loading group settings: {e}") + + return { + "work_dir": "", + "description": "", + "created_at": datetime.now().isoformat(), + "updated_at": datetime.now().isoformat(), + } + + def update_group_settings(self, group_id: str, settings: Dict[str, Any]): + """Update settings for a specific script group""" + schema = self.config_schema.get("config_schema", {}) + validated_settings = {} + + # Validate each setting against schema + for key, field_schema in schema.items(): + if key in settings: + validated_settings[key] = self._validate_setting( + key, settings[key], field_schema + ) + elif field_schema.get("required", False): + raise ValueError(f"Required field '{key}' is missing") + else: + validated_settings[key] = field_schema.get("default") + + # Add non-schema fields + for key, value in settings.items(): + if key not in schema: + validated_settings[key] = value + + # Update timestamps + validated_settings["updated_at"] = datetime.now().isoformat() + + group_dir = self.script_groups_dir / group_id + settings_file = group_dir / "group.json" + + if not settings_file.exists(): + validated_settings["created_at"] = validated_settings["updated_at"] + group_dir.mkdir(parents=True, exist_ok=True) + + # Save settings + with open(settings_file, "w", encoding="utf-8") as f: + json.dump(validated_settings, f, indent=4) diff --git a/backend/core/profile_manager.py b/backend/core/profile_manager.py index 8b0037b..3baa562 100644 --- a/backend/core/profile_manager.py +++ b/backend/core/profile_manager.py @@ -4,32 +4,28 @@ import json from typing import Dict, Any, List, Optional from datetime import datetime + class ProfileManager: """Manages configuration profiles""" - + DEFAULT_PROFILE = { "id": "default", "name": "Default Profile", - "work_dir": "", - "llm_settings": { - "model": "gpt-4", - "temperature": 0.7, - "api_key": "" - }, + "llm_settings": {"model": "gpt-4", "temperature": 0.7, "api_key": ""}, "created_at": "", - "updated_at": "" + "updated_at": "", } - + def __init__(self, data_dir: Path): self.data_dir = data_dir self.profiles_file = data_dir / "profiles.json" self.profiles: Dict[str, Dict] = self._load_profiles() - + def _load_profiles(self) -> Dict[str, Dict]: """Load profiles from file""" if self.profiles_file.exists(): try: - with open(self.profiles_file, 'r', encoding='utf-8') as f: + with open(self.profiles_file, "r", encoding="utf-8") as f: profiles = json.load(f) # Ensure default profile exists if "default" not in profiles: @@ -45,7 +41,7 @@ class ProfileManager: profiles = {"default": self._create_default_profile()} self._save_profiles(profiles) return profiles - + def _create_default_profile(self) -> Dict[str, Any]: """Create default profile with timestamp""" profile = self.DEFAULT_PROFILE.copy() @@ -53,83 +49,85 @@ class ProfileManager: profile["created_at"] = now profile["updated_at"] = now return profile - + def _save_profiles(self, profiles: Optional[Dict] = None): """Save profiles to file""" if profiles is None: profiles = self.profiles try: print(f"Saving profiles to: {self.profiles_file}") # Agregar debug - with open(self.profiles_file, 'w', encoding='utf-8') as f: + with open(self.profiles_file, "w", encoding="utf-8") as f: json.dump(profiles, f, indent=4) print("Profiles saved successfully") # Agregar debug except Exception as e: print(f"Error saving profiles: {e}") # Agregar debug raise # Re-lanzar la excepción para que se maneje arriba - + def get_all_profiles(self) -> List[Dict[str, Any]]: """Get all profiles""" return list(self.profiles.values()) - + def get_profile(self, profile_id: str) -> Optional[Dict[str, Any]]: """Get specific profile""" return self.profiles.get(profile_id) - + def create_profile(self, profile_data: Dict[str, Any]) -> Dict[str, Any]: """Create new profile""" if "id" not in profile_data: raise ValueError("Profile must have an id") - + profile_id = profile_data["id"] if profile_id in self.profiles: raise ValueError(f"Profile {profile_id} already exists") - + # Add timestamps now = datetime.now().isoformat() profile_data["created_at"] = now profile_data["updated_at"] = now - + # Ensure required fields - for key in ["name", "work_dir", "llm_settings"]: + for key in ["name", "llm_settings"]: if key not in profile_data: profile_data[key] = self.DEFAULT_PROFILE[key] - + self.profiles[profile_id] = profile_data self._save_profiles() return profile_data - - def update_profile(self, profile_id: str, profile_data: Dict[str, Any]) -> Dict[str, Any]: + + def update_profile( + self, profile_id: str, profile_data: Dict[str, Any] + ) -> Dict[str, Any]: """Update existing profile""" try: print(f"Updating profile {profile_id} with data: {profile_data}") if profile_id not in self.profiles: raise ValueError(f"Profile {profile_id} not found") - + if profile_id == "default" and "id" in profile_data: raise ValueError("Cannot change id of default profile") - + # Update timestamp profile_data["updated_at"] = datetime.now().isoformat() - + # Update profile current_profile = self.profiles[profile_id].copy() # Hacer una copia current_profile.update(profile_data) # Actualizar la copia self.profiles[profile_id] = current_profile # Asignar la copia actualizada - + print(f"Updated profile: {self.profiles[profile_id]}") self._save_profiles() return self.profiles[profile_id] except Exception as e: print(f"Error in update_profile: {e}") # Agregar debug raise - + def delete_profile(self, profile_id: str): """Delete profile""" if profile_id == "default": raise ValueError("Cannot delete default profile") - + if profile_id not in self.profiles: raise ValueError(f"Profile {profile_id} not found") - + del self.profiles[profile_id] - self._save_profiles() \ No newline at end of file + self._save_profiles() diff --git a/backend/core/script_manager.py b/backend/core/script_manager.py index 404f670..03d83a6 100644 --- a/backend/core/script_manager.py +++ b/backend/core/script_manager.py @@ -4,17 +4,30 @@ import importlib.util import inspect from typing import Dict, List, Any, Optional import json +from .group_settings_manager import GroupSettingsManager # Agregar esta importación + class ScriptManager: + def __init__(self, script_groups_dir: Path): + self.script_groups_dir = script_groups_dir + self.group_settings = GroupSettingsManager(script_groups_dir) + + def get_group_settings(self, group_id: str) -> Dict[str, Any]: + """Get settings for a script group""" + return self.group_settings.get_group_settings(group_id) + + def update_group_settings(self, group_id: str, settings: Dict[str, Any]): + """Update settings for a script group""" + return self.group_settings.update_group_settings(group_id, settings) def get_group_config_schema(self, group_id: str) -> Dict[str, Any]: """Get configuration schema for a script group""" config_file = self.script_groups_dir / group_id / "config.json" print(f"Looking for config file: {config_file}") # Debug - + if config_file.exists(): try: - with open(config_file, 'r', encoding='utf-8') as f: + with open(config_file, "r", encoding="utf-8") as f: schema = json.load(f) print(f"Loaded schema: {schema}") # Debug return schema @@ -22,154 +35,155 @@ class ScriptManager: print(f"Error loading group config schema: {e}") # Debug else: print(f"Config file not found: {config_file}") # Debug - + # Retornar un schema vacío si no existe el archivo - return { - "group_name": group_id, - "description": "", - "config_schema": {} - } - + return {"group_name": group_id, "description": "", "config_schema": {}} + def get_available_groups(self) -> List[Dict[str, Any]]: """Get list of available script groups""" groups = [] - + for group_dir in self.script_groups_dir.iterdir(): - if group_dir.is_dir() and not group_dir.name.startswith('_'): - groups.append({ - "id": group_dir.name, - "name": group_dir.name.replace('_', ' ').title(), - "path": str(group_dir) - }) - + if group_dir.is_dir() and not group_dir.name.startswith("_"): + groups.append( + { + "id": group_dir.name, + "name": group_dir.name.replace("_", " ").title(), + "path": str(group_dir), + } + ) + return groups - + def get_group_scripts(self, group_id: str) -> List[Dict[str, Any]]: """Get scripts for a specific group""" group_dir = self.script_groups_dir / group_id print(f"Looking for scripts in: {group_dir}") # Debug - + if not group_dir.exists() or not group_dir.is_dir(): print(f"Directory not found: {group_dir}") # Debug raise ValueError(f"Script group '{group_id}' not found") - + scripts = [] - for script_file in group_dir.glob('x[0-9].py'): + for script_file in group_dir.glob("x[0-9].py"): print(f"Found script file: {script_file}") # Debug script_info = self._analyze_script(script_file) if script_info: scripts.append(script_info) - - return sorted(scripts, key=lambda x: x['id']) - - def __init__(self, script_groups_dir: Path): - self.script_groups_dir = script_groups_dir - + + return sorted(scripts, key=lambda x: x["id"]) + def discover_groups(self) -> List[Dict[str, Any]]: """Discover all script groups""" groups = [] - + for group_dir in self.script_groups_dir.iterdir(): - if group_dir.is_dir() and not group_dir.name.startswith('_'): + if group_dir.is_dir() and not group_dir.name.startswith("_"): group_info = self._analyze_group(group_dir) if group_info: groups.append(group_info) - + return groups - + def _analyze_group(self, group_dir: Path) -> Optional[Dict[str, Any]]: """Analyze a script group directory""" scripts = [] - - for script_file in group_dir.glob('x[0-9].py'): + + for script_file in group_dir.glob("x[0-9].py"): try: script_info = self._analyze_script(script_file) if script_info: scripts.append(script_info) except Exception as e: print(f"Error analyzing script {script_file}: {e}") - + if scripts: return { "id": group_dir.name, - "name": group_dir.name.replace('_', ' ').title(), - "scripts": sorted(scripts, key=lambda x: x['id']) + "name": group_dir.name.replace("_", " ").title(), + "scripts": sorted(scripts, key=lambda x: x["id"]), } return None - + def _analyze_script(self, script_file: Path) -> Optional[Dict[str, Any]]: """Analyze a single script file""" try: # Import script module - spec = importlib.util.spec_from_file_location( - script_file.stem, - script_file - ) + spec = importlib.util.spec_from_file_location(script_file.stem, script_file) module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) - + # Find script class script_class = None for name, obj in inspect.getmembers(module): - if (inspect.isclass(obj) and - obj.__module__ == module.__name__ and - hasattr(obj, 'run')): + if ( + inspect.isclass(obj) + and obj.__module__ == module.__name__ + and hasattr(obj, "run") + ): script_class = obj break - + if script_class: # Extraer la primera línea del docstring como nombre docstring = inspect.getdoc(script_class) if docstring: - name, *description = docstring.split('\n', 1) - description = description[0] if description else '' + name, *description = docstring.split("\n", 1) + description = description[0] if description else "" else: name = script_file.stem - description = '' - + description = "" + return { "id": script_file.stem, "name": name.strip(), "description": description.strip(), - "file": str(script_file.relative_to(self.script_groups_dir)) + "file": str(script_file.relative_to(self.script_groups_dir)), } - + except Exception as e: print(f"Error loading script {script_file}: {e}") - + return None - - def execute_script(self, group_id: str, script_id: str, work_dir: str, - profile: Dict[str, Any]) -> Dict[str, Any]: + + def execute_script( + self, group_id: str, script_id: str, profile: Dict[str, Any] + ) -> Dict[str, Any]: """Execute a specific script""" + # Get group settings first + group_settings = self.group_settings.get_group_settings(group_id) + work_dir = group_settings.get("work_dir") + + if not work_dir: + raise ValueError(f"No work directory configured for group {group_id}") + script_file = self.script_groups_dir / group_id / f"{script_id}.py" - + if not script_file.exists(): raise ValueError(f"Script {script_id} not found in group {group_id}") - + try: # Import script module spec = importlib.util.spec_from_file_location(script_id, script_file) module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) - + # Find and instantiate script class script_class = None for name, obj in inspect.getmembers(module): - if (inspect.isclass(obj) and - obj.__module__ == module.__name__ and - hasattr(obj, 'run')): + if ( + inspect.isclass(obj) + and obj.__module__ == module.__name__ + and hasattr(obj, "run") + ): script_class = obj break - + if not script_class: raise ValueError(f"No valid script class found in {script_id}") - + script = script_class() return script.run(work_dir, profile) - + except Exception as e: - return { - "status": "error", - "error": str(e) - } + return {"status": "error", "error": str(e)} diff --git a/backend/script_groups/config.json b/backend/script_groups/config.json new file mode 100644 index 0000000..4886062 --- /dev/null +++ b/backend/script_groups/config.json @@ -0,0 +1,30 @@ +{ + "description": "Configuration schema for script groups", + "config_schema": { + "work_dir": { + "type": "string", + "description": "Working directory for this script group", + "required": true + }, + "description": { + "type": "string", + "description": "Description of this script group", + "default": "" + }, + "backup_dir": { + "type": "directory", + "description": "Backup directory path", + "default": "" + }, + "max_files": { + "type": "number", + "description": "Maximum number of files to process", + "default": 1000 + }, + "enable_backup": { + "type": "boolean", + "description": "Enable automatic backups", + "default": false + } + } +} diff --git a/claude/__init__.py b/claude/__init__.py new file mode 100644 index 0000000..a1de208 --- /dev/null +++ b/claude/__init__.py @@ -0,0 +1 @@ +# backend/__init__.py diff --git a/claude/__init___1.py b/claude/__init___1.py new file mode 100644 index 0000000..d258405 --- /dev/null +++ b/claude/__init___1.py @@ -0,0 +1 @@ +# backend/core/__init__.py diff --git a/claude/__init___2.py b/claude/__init___2.py new file mode 100644 index 0000000..772a730 --- /dev/null +++ b/claude/__init___2.py @@ -0,0 +1 @@ +# backend/script_groups/__init__.py diff --git a/claude/__init___3.py b/claude/__init___3.py new file mode 100644 index 0000000..4e7ed26 --- /dev/null +++ b/claude/__init___3.py @@ -0,0 +1 @@ +# backend/script_groups/example_group/__init__.py diff --git a/claude/app.py b/claude/app.py new file mode 100644 index 0000000..1bf0035 --- /dev/null +++ b/claude/app.py @@ -0,0 +1,193 @@ +# backend/app.py +import os +import sys +from pathlib import Path + +# Add the parent directory to Python path +backend_dir = Path(__file__).parent.parent # Sube un nivel más para incluir la carpeta raíz +if str(backend_dir) not in sys.path: + sys.path.append(str(backend_dir)) + +from flask import Flask, render_template, jsonify, request, send_from_directory +from core.directory_handler import select_directory +from core.script_manager import ScriptManager +from core.profile_manager import ProfileManager + +app = Flask(__name__, + template_folder='../frontend/templates', + static_folder='../frontend/static') + +# Initialize managers +data_dir = Path(__file__).parent.parent / 'data' +script_groups_dir = Path(__file__).parent / 'script_groups' + +profile_manager = ProfileManager(data_dir) +script_manager = ScriptManager(script_groups_dir) + +@app.route('/') +def index(): + """Render main page""" + return render_template('index.html') + +# Profile endpoints +@app.route('/api/profiles', methods=['GET']) +def get_profiles(): + """Get all profiles""" + return jsonify(profile_manager.get_all_profiles()) + +@app.route('/api/profiles/', methods=['GET']) +def get_profile(profile_id): + """Get specific profile""" + profile = profile_manager.get_profile(profile_id) + if profile: + return jsonify(profile) + return jsonify({"error": "Profile not found"}), 404 + +@app.route('/api/profiles', methods=['POST']) +def create_profile(): + """Create new profile""" + profile_data = request.json + try: + profile = profile_manager.create_profile(profile_data) + return jsonify(profile) + except Exception as e: + return jsonify({"error": str(e)}), 400 + +@app.route('/api/profiles/', methods=['PUT']) +def update_profile(profile_id): + """Update existing profile""" + try: + profile_data = request.json + print(f"Received update request for profile {profile_id}: {profile_data}") # Debug + profile = profile_manager.update_profile(profile_id, profile_data) + print(f"Profile updated: {profile}") # Debug + return jsonify(profile) + except Exception as e: + print(f"Error updating profile: {e}") # Debug + return jsonify({"error": str(e)}), 400 + +@app.route('/api/profiles/', methods=['DELETE']) +def delete_profile(profile_id): + """Delete profile""" + try: + profile_manager.delete_profile(profile_id) + return jsonify({"status": "success"}) + except Exception as e: + return jsonify({"error": str(e)}), 400 + +@app.route('/api/script-groups', methods=['GET']) +def get_script_groups(): + """Get all available script groups""" + try: + groups = script_manager.get_available_groups() + return jsonify(groups) + except Exception as e: + return jsonify({"error": str(e)}), 500 + +# Directory handling endpoints +@app.route('/api/select-directory', methods=['GET']) +def handle_select_directory(): + """Handle directory selection""" + print("Handling directory selection request") # Debug + result = select_directory() + print(f"Directory selection result: {result}") # Debug + if "error" in result: + return jsonify(result), 400 + return jsonify(result) + +# Script management endpoints +@app.route('/api/scripts', methods=['GET']) +def get_scripts(): + """Get all available script groups""" + try: + groups = script_manager.discover_groups() + return jsonify(groups) + except Exception as e: + return jsonify({"error": str(e)}), 500 + +@app.route('/api/scripts///run', methods=['POST']) +def run_script(group_id, script_id): + """Execute a specific script""" + data = request.json + work_dir = data.get('work_dir') + profile = data.get('profile') + + if not work_dir: + return jsonify({"error": "Work directory not specified"}), 400 + if not profile: + return jsonify({"error": "Profile not specified"}), 400 + + try: + result = script_manager.execute_script(group_id, script_id, work_dir, profile) + return jsonify(result) + except Exception as e: + return jsonify({"error": str(e)}), 500 + +# Work directory configuration endpoints +@app.route('/api/workdir-config/', methods=['GET']) +def get_workdir_config(work_dir): + """Get work directory configuration""" + from core.workdir_config import WorkDirConfigManager + config_manager = WorkDirConfigManager(work_dir) + return jsonify(config_manager.get_config()) + +@app.route('/api/workdir-config//group/', methods=['GET']) +def get_group_config(work_dir, group_id): + """Get group configuration from work directory""" + from core.workdir_config import WorkDirConfigManager + config_manager = WorkDirConfigManager(work_dir) + return jsonify(config_manager.get_group_config(group_id)) + +@app.route('/api/workdir-config//group/', methods=['PUT']) +def update_group_config(work_dir, group_id): + """Update group configuration in work directory""" + from core.workdir_config import WorkDirConfigManager + config_manager = WorkDirConfigManager(work_dir) + + try: + settings = request.json + config_manager.update_group_config(group_id, settings) + return jsonify({"status": "success"}) + except Exception as e: + return jsonify({"error": str(e)}), 400 + +@app.route('/api/script-groups//config-schema', methods=['PUT']) +def update_group_config_schema(group_id): + """Update configuration schema for a script group""" + try: + schema = request.json + config_file = Path(script_manager.script_groups_dir) / group_id / "config.json" + + with open(config_file, 'w', encoding='utf-8') as f: + json.dump(schema, f, indent=4) + + return jsonify({"status": "success"}) + except Exception as e: + return jsonify({"error": str(e)}), 500 + +@app.route('/api/script-groups//scripts', methods=['GET']) +def get_group_scripts(group_id): + """Get scripts for a specific group""" + try: + print(f"Loading scripts for group: {group_id}") # Debug + scripts = script_manager.get_group_scripts(group_id) + print(f"Scripts found: {scripts}") # Debug + return jsonify(scripts) + except Exception as e: + print(f"Error loading scripts: {str(e)}") # Debug + return jsonify({"error": str(e)}), 500 + +@app.route('/api/script-groups//config-schema', methods=['GET']) +def get_group_config_schema(group_id): + """Get configuration schema for a script group""" + try: + print(f"Loading config schema for group: {group_id}") # Debug + schema = script_manager.get_group_config_schema(group_id) + print(f"Schema loaded: {schema}") # Debug + return jsonify(schema) + except Exception as e: + print(f"Error loading schema: {str(e)}") # Debug + return jsonify({"error": str(e)}), 500 + +if __name__ == '__main__': + app.run(debug=True, port=5000) diff --git a/claude/base.html b/claude/base.html new file mode 100644 index 0000000..4282250 --- /dev/null +++ b/claude/base.html @@ -0,0 +1,15 @@ + + + + + + + + Local Scripts Web + + + + + {% block content %}{% endblock %} + + \ No newline at end of file diff --git a/claude/base_script.py b/claude/base_script.py new file mode 100644 index 0000000..2a34b9a --- /dev/null +++ b/claude/base_script.py @@ -0,0 +1,61 @@ +# backend/script_groups/base_script.py +from typing import Dict, Any +from pathlib import Path +import json + +class BaseScript: + """Base class for all scripts""" + + def run(self, work_dir: str, profile: Dict[str, Any]) -> Dict[str, Any]: + """ + Execute the script + + Args: + work_dir (str): Working directory path + profile (Dict[str, Any]): Current profile configuration + + Returns: + Dict[str, Any]: Execution results + """ + raise NotImplementedError("Script must implement run method") + + def get_config(self, work_dir: str, group_id: str) -> Dict[str, Any]: + """Get group configuration from work directory""" + config_file = Path(work_dir) / "script_config.json" + + if config_file.exists(): + try: + with open(config_file, 'r', encoding='utf-8') as f: + config = json.load(f) + return config.get("group_settings", {}).get(group_id, {}) + except Exception as e: + print(f"Error loading config: {e}") + + return {} + + def save_config(self, work_dir: str, group_id: str, settings: Dict[str, Any]): + """Save group configuration to work directory""" + config_file = Path(work_dir) / "script_config.json" + + try: + # Load existing config or create new + if config_file.exists(): + with open(config_file, 'r', encoding='utf-8') as f: + config = json.load(f) + else: + config = { + "version": "1.0", + "group_settings": {} + } + + # Update settings + if "group_settings" not in config: + config["group_settings"] = {} + config["group_settings"][group_id] = settings + + # Save config + with open(config_file, 'w', encoding='utf-8') as f: + json.dump(config, f, indent=4) + + except Exception as e: + print(f"Error saving config: {e}") diff --git a/claude/claude_file_organizer.py b/claude/claude_file_organizer.py new file mode 100644 index 0000000..93391ad --- /dev/null +++ b/claude/claude_file_organizer.py @@ -0,0 +1,172 @@ +# claude_file_organizer.py +import os +import shutil +from pathlib import Path +import re + +class ClaudeProjectOrganizer: + def __init__(self): + self.source_dir = Path.cwd() + self.claude_dir = self.source_dir / 'claude' + self.file_mapping = {} + + def should_skip_directory(self, dir_name): + skip_dirs = {'.git', '__pycache__', 'venv', 'env', '.pytest_cache', '.vscode', 'claude'} + return dir_name in skip_dirs + + def get_comment_prefix(self, file_extension): + """Determina el prefijo de comentario según la extensión del archivo""" + comment_styles = { + '.py': '#', + '.js': '//', + '.css': '/*', + '.html': '', + } + return comment_suffixes.get(file_extension.lower(), '') + + def normalize_path(self, path_str: str) -> str: + """Normaliza la ruta usando forward slashes""" + return str(path_str).replace('\\', '/') + + def check_existing_path_comment(self, content: str, normalized_path: str, comment_prefix: str) -> bool: + """Verifica si ya existe un comentario con la ruta en el archivo""" + # Escapar caracteres especiales en el prefijo de comentario para regex + escaped_prefix = re.escape(comment_prefix) + + # Crear patrones para buscar tanto forward como backward slashes + forward_pattern = f"{escaped_prefix}\\s*{re.escape(normalized_path)}\\b" + backward_path = normalized_path.replace('/', '\\\\') # Doble backslash para el patrón + backward_pattern = f"{escaped_prefix}\\s*{re.escape(backward_path)}" + + # Buscar en las primeras líneas del archivo + first_lines = content.split('\n')[:5] + for line in first_lines: + if (re.search(forward_pattern, line) or + re.search(backward_pattern, line)): + return True + return False + + def add_path_comment(self, file_path: Path, content: str) -> str: + """Agrega un comentario con la ruta al inicio del archivo si no existe""" + relative_path = file_path.relative_to(self.source_dir) + normalized_path = self.normalize_path(relative_path) + comment_prefix = self.get_comment_prefix(file_path.suffix) + + if comment_prefix is None: + return content + + comment_suffix = self.get_comment_suffix(file_path.suffix) + + # Verificar si ya existe el comentario + if self.check_existing_path_comment(content, normalized_path, comment_prefix): + print(f" - Comentario de ruta ya existe en {file_path}") + return content + + path_comment = f"{comment_prefix} {normalized_path}{comment_suffix}\n" + + # Para archivos HTML, insertar después del doctype si existe + if file_path.suffix.lower() == '.html': + if content.lower().startswith('') + 1 + return content[:doctype_end] + '\n' + path_comment + content[doctype_end:] + + return path_comment + content + + def clean_claude_directory(self): + if self.claude_dir.exists(): + shutil.rmtree(self.claude_dir) + self.claude_dir.mkdir() + print(f"Directorio claude limpiado: {self.claude_dir}") + + def copy_files(self): + self.clean_claude_directory() + + for root, dirs, files in os.walk(self.source_dir): + dirs[:] = [d for d in dirs if not self.should_skip_directory(d)] + current_path = Path(root) + + for file in files: + file_path = current_path / file + + if file.endswith(('.py', '.js', '.css', '.html', '.json', '.yml', '.yaml', + '.tsx', '.ts', '.jsx', '.scss', '.less')): + target_path = self.claude_dir / file + + # Si el archivo ya existe en el directorio claude, agregar un sufijo numérico + if target_path.exists(): + base = target_path.stem + ext = target_path.suffix + counter = 1 + while target_path.exists(): + target_path = self.claude_dir / f"{base}_{counter}{ext}" + counter += 1 + + try: + # Leer el contenido del archivo + with open(file_path, 'r', encoding='utf-8') as f: + content = f.read() + + # Agregar el comentario con la ruta si no existe + modified_content = self.add_path_comment(file_path, content) + + # Escribir el nuevo contenido + with open(target_path, 'w', encoding='utf-8', newline='\n') as f: + f.write(modified_content) + + self.file_mapping[str(file_path)] = target_path.name + print(f"Copiado: {file_path} -> {target_path}") + + except UnicodeDecodeError: + print(f"Advertencia: No se pudo procesar {file_path} como texto. Copiando sin modificar...") + shutil.copy2(file_path, target_path) + except Exception as e: + print(f"Error procesando {file_path}: {str(e)}") + + def generate_tree_report(self): + """Genera el reporte en formato árbol visual""" + report = ["Estructura del proyecto original:\n"] + + def add_to_report(path, prefix="", is_last=True): + report.append(prefix + ("└── " if is_last else "├── ") + path.name) + + if path.is_dir() and not self.should_skip_directory(path.name): + children = sorted(path.iterdir(), key=lambda x: (x.is_file(), x.name)) + children = [c for c in children if not (c.is_dir() and self.should_skip_directory(c.name))] + + for i, child in enumerate(children): + is_last_child = i == len(children) - 1 + new_prefix = prefix + (" " if is_last else "│ ") + add_to_report(child, new_prefix, is_last_child) + + add_to_report(self.source_dir) + + report_path = self.claude_dir / "project_structure.txt" + with open(report_path, "w", encoding="utf-8") as f: + f.write("\n".join(report)) + print(f"\nReporte generado en: {report_path}") + +def main(): + try: + print("Iniciando organización de archivos para Claude...") + organizer = ClaudeProjectOrganizer() + organizer.copy_files() + organizer.generate_tree_report() + print("\n¡Proceso completado exitosamente!") + except Exception as e: + print(f"\nError durante la ejecución: {str(e)}") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/claude/config.json b/claude/config.json new file mode 100644 index 0000000..4886062 --- /dev/null +++ b/claude/config.json @@ -0,0 +1,30 @@ +{ + "description": "Configuration schema for script groups", + "config_schema": { + "work_dir": { + "type": "string", + "description": "Working directory for this script group", + "required": true + }, + "description": { + "type": "string", + "description": "Description of this script group", + "default": "" + }, + "backup_dir": { + "type": "directory", + "description": "Backup directory path", + "default": "" + }, + "max_files": { + "type": "number", + "description": "Maximum number of files to process", + "default": 1000 + }, + "enable_backup": { + "type": "boolean", + "description": "Enable automatic backups", + "default": false + } + } +} diff --git a/claude/config_1.json b/claude/config_1.json new file mode 100644 index 0000000..36be0b3 --- /dev/null +++ b/claude/config_1.json @@ -0,0 +1,32 @@ +{ + "group_name": "System Analysis", + "description": "Scripts for system analysis and file management", + "config_schema": { + "exclude_dirs": { + "type": "string", + "description": "Directories to exclude (comma separated)", + "default": "venv,__pycache__,.git" + }, + "count_hidden": { + "type": "boolean", + "description": "Include hidden files in count", + "default": false + }, + "min_size": { + "type": "number", + "description": "Minimum file size to count (in bytes)", + "default": 0 + }, + "save_report": { + "type": "boolean", + "description": "Save results to file", + "default": true + }, + "report_format": { + "type": "select", + "options": ["txt", "json", "csv"], + "description": "Format for saved reports", + "default": "json" + } + } +} \ No newline at end of file diff --git a/claude/directory_handler.py b/claude/directory_handler.py new file mode 100644 index 0000000..53f5ad9 --- /dev/null +++ b/claude/directory_handler.py @@ -0,0 +1,23 @@ +# backend/core/directory_handler.py +import os +from pathlib import Path +import tkinter as tk +from tkinter import filedialog +from flask import jsonify + +def select_directory(): + """Show directory selection dialog and return selected path""" + root = tk.Tk() + root.withdraw() + root.attributes('-topmost', True) # Hace que el diálogo siempre esté encima + + try: + directory = filedialog.askdirectory( + title="Select Work Directory", + initialdir=os.path.expanduser("~") + ) + return {"path": directory} if directory else {"error": "No directory selected"} + except Exception as e: + return {"error": str(e)} + finally: + root.destroy() \ No newline at end of file diff --git a/claude/group_settings_manager.py b/claude/group_settings_manager.py new file mode 100644 index 0000000..4e04cc8 --- /dev/null +++ b/claude/group_settings_manager.py @@ -0,0 +1,122 @@ +# backend/core/group_settings_manager.py +from pathlib import Path +import json +from typing import Dict, Any +from datetime import datetime +import os + + +class GroupSettingsManager: + """Manages settings for script groups""" + + def __init__(self, script_groups_dir: Path): + self.script_groups_dir = script_groups_dir + self.config_schema = self._load_config_schema() + + def _load_config_schema(self) -> Dict[str, Any]: + """Load the main configuration schema for script groups""" + schema_file = self.script_groups_dir / "config.json" + try: + with open(schema_file, "r", encoding="utf-8") as f: + return json.load(f) + except Exception as e: + print(f"Error loading group config schema: {e}") + return { + "config_schema": { + "work_dir": { + "type": "string", + "description": "Working directory for this script group", + "required": True, + }, + "description": { + "type": "string", + "description": "Description of this script group", + "default": "", + }, + } + } + + def _validate_setting( + self, key: str, value: Any, field_schema: Dict[str, Any] + ) -> Any: + """Validate and convert a single setting value""" + field_type = field_schema.get("type") + + if value is None or value == "": + if field_schema.get("required", False): + raise ValueError(f"Field '{key}' is required") + return field_schema.get("default") + + try: + if field_type == "string": + return str(value) + elif field_type == "number": + return float(value) if "." in str(value) else int(value) + elif field_type == "boolean": + if isinstance(value, str): + return value.lower() == "true" + return bool(value) + elif field_type == "directory": + path = Path(value) + if not path.is_absolute(): + path = Path(self.script_groups_dir) / path + if not path.exists(): + path.mkdir(parents=True, exist_ok=True) + return str(path) + else: + return value + except Exception as e: + raise ValueError(f"Invalid value for field '{key}': {str(e)}") + + def get_group_settings(self, group_id: str) -> Dict[str, Any]: + """Get settings for a specific script group""" + settings_file = self.script_groups_dir / group_id / "group.json" + + if settings_file.exists(): + try: + with open(settings_file, "r", encoding="utf-8") as f: + return json.load(f) + except Exception as e: + print(f"Error loading group settings: {e}") + + return { + "work_dir": "", + "description": "", + "created_at": datetime.now().isoformat(), + "updated_at": datetime.now().isoformat(), + } + + def update_group_settings(self, group_id: str, settings: Dict[str, Any]): + """Update settings for a specific script group""" + schema = self.config_schema.get("config_schema", {}) + validated_settings = {} + + # Validate each setting against schema + for key, field_schema in schema.items(): + if key in settings: + validated_settings[key] = self._validate_setting( + key, settings[key], field_schema + ) + elif field_schema.get("required", False): + raise ValueError(f"Required field '{key}' is missing") + else: + validated_settings[key] = field_schema.get("default") + + # Add non-schema fields + for key, value in settings.items(): + if key not in schema: + validated_settings[key] = value + + # Update timestamps + validated_settings["updated_at"] = datetime.now().isoformat() + + group_dir = self.script_groups_dir / group_id + settings_file = group_dir / "group.json" + + if not settings_file.exists(): + validated_settings["created_at"] = validated_settings["updated_at"] + group_dir.mkdir(parents=True, exist_ok=True) + + # Save settings + with open(settings_file, "w", encoding="utf-8") as f: + json.dump(validated_settings, f, indent=4) diff --git a/claude/index.html b/claude/index.html new file mode 100644 index 0000000..0912f1a --- /dev/null +++ b/claude/index.html @@ -0,0 +1,115 @@ + + + + + + + + Local Scripts Web + + + + + + + +
+ + + + +
+
+
+ +
+
+

Work Directory

+
+ + +
+
+
+ + +
+
+

Scripts

+
+ + +
+
+
+ + +
+
+
+

Output

+ +
+
+
+
+
+
+
+
+
+ + + + + + + + + + + \ No newline at end of file diff --git a/claude/main.js b/claude/main.js new file mode 100644 index 0000000..951ab99 --- /dev/null +++ b/claude/main.js @@ -0,0 +1,264 @@ +// frontend/static/js/main.js + +// Global state +let currentProfile = null; + +// Definir clases comunes para inputs +const STYLES = { + editableInput: "mt-1 block w-full rounded-md border-2 border-gray-300 bg-green-50 px-3 py-2 shadow-sm focus:border-indigo-500 focus:ring-indigo-500", + readonlyInput: "mt-1 block w-full rounded-md border-2 border-gray-200 bg-gray-100 px-3 py-2 shadow-sm", + button: "px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600", + buttonSecondary: "px-4 py-2 bg-gray-200 text-gray-800 rounded-md hover:bg-gray-300" +}; + +async function initializeApp() { + try { + console.log('Inicializando aplicación...'); + + // Cargar perfiles + const profiles = await apiRequest('/profiles'); + console.log('Profiles loaded:', profiles); + + // Obtener último perfil usado + const lastProfileId = localStorage.getItem('lastProfileId') || 'default'; + console.log('Last profile ID:', lastProfileId); + + // Actualizar selector de perfiles + updateProfileSelector(profiles); + + // Seleccionar el último perfil usado + const selectedProfile = profiles.find(p => p.id === lastProfileId) || profiles[0]; + if (selectedProfile) { + console.log('Selecting profile:', selectedProfile.id); + await selectProfile(selectedProfile.id); + } + + // Cargar grupos de scripts y restaurar la última selección + await restoreScriptGroup(); + + // Actualizar la interfaz + updateWorkDirDisplay(); + + } catch (error) { + console.error('Error al inicializar la aplicación:', error); + showError('Error al inicializar la aplicación'); + } +} + +async function restoreScriptGroup() { + try { + // Primero cargar los grupos disponibles + await loadScriptGroups(); + + // Luego intentar restaurar el último grupo seleccionado + const lastGroupId = localStorage.getItem('lastGroupId'); + if (lastGroupId) { + console.log('Restoring last group:', lastGroupId); + const groupSelect = document.getElementById('groupSelect'); + if (groupSelect) { + groupSelect.value = lastGroupId; + if (groupSelect.value) { // Verifica que el valor se haya establecido correctamente + await loadGroupScripts(lastGroupId); + } else { + console.log('Selected group no longer exists:', lastGroupId); + localStorage.removeItem('lastGroupId'); + } + } + } + } catch (error) { + console.error('Error restoring script group:', error); + } +} + +// Función para restaurar el último estado +async function restoreLastState() { + const lastProfileId = localStorage.getItem('lastProfileId'); + const lastGroupId = localStorage.getItem('lastGroupId'); + + console.log('Restoring last state:', { lastProfileId, lastGroupId }); + + if (lastProfileId) { + const profileSelect = document.getElementById('profileSelect'); + profileSelect.value = lastProfileId; + await selectProfile(lastProfileId); + } + + if (lastGroupId) { + const groupSelect = document.getElementById('groupSelect'); + if (groupSelect) { + groupSelect.value = lastGroupId; + await loadGroupScripts(lastGroupId); + } + } +} + +// API functions +async function apiRequest(endpoint, options = {}) { + try { + const response = await fetch(`/api${endpoint}`, { + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.error || 'Error en la solicitud API'); + } + + return await response.json(); + } catch (error) { + console.error('Error API:', error); + showError(error.message); + throw error; + } +} + +async function loadProfiles() { + try { + const profiles = await apiRequest('/profiles'); + updateProfileSelector(profiles); + + // Obtener último perfil usado + const lastProfileId = localStorage.getItem('lastProfileId'); + + // Seleccionar perfil guardado o el default + const defaultProfile = profiles.find(p => p.id === (lastProfileId || 'default')) || profiles[0]; + if (defaultProfile) { + await selectProfile(defaultProfile.id); + } + } catch (error) { + showError('Error al cargar los perfiles'); + } +} + +async function selectProfile(profileId) { + try { + console.log('Seleccionando perfil:', profileId); + currentProfile = await apiRequest(`/profiles/${profileId}`); + + // Guardar en localStorage + localStorage.setItem('lastProfileId', profileId); + console.log('Profile ID saved to storage:', profileId); + + // Actualizar explícitamente el valor del combo + const select = document.getElementById('profileSelect'); + if (select) { + select.value = profileId; + console.log('Updated profileSelect value to:', profileId); + } + + updateWorkDirDisplay(); + + // Recargar scripts con el último grupo seleccionado + await restoreScriptGroup(); + } catch (error) { + console.error('Error al seleccionar perfil:', error); + showError('Error al cargar el perfil'); + } +} + +// Initialize when page loads +document.addEventListener('DOMContentLoaded', initializeApp); + +function updateProfileSelector(profiles) { + const select = document.getElementById('profileSelect'); + const lastProfileId = localStorage.getItem('lastProfileId') || 'default'; + + console.log('Updating profile selector. Last profile ID:', lastProfileId); + + // Construir las opciones + select.innerHTML = profiles.map(profile => ` + + `).join(''); + + // Asegurar que el valor seleccionado sea correcto + select.value = lastProfileId; + console.log('Set profileSelect value to:', lastProfileId); +} + +async function changeProfile() { + const select = document.getElementById('profileSelect'); + if (select.value) { + await selectProfile(select.value); + await loadScriptGroups(); // Reload scripts when profile changes + } +} + +// Work directory functions +function updateWorkDirDisplay() { + const input = document.getElementById('workDirPath'); + if (input && currentProfile) { + input.value = currentProfile.work_dir || ''; + } +} + +async function selectWorkDir() { + try { + console.log('Requesting directory selection...'); // Debug + const response = await apiRequest('/select-directory'); + console.log('Directory selection response:', response); // Debug + + if (response.path) { + console.log('Updating profile with new work_dir:', response.path); // Debug + const updateResponse = await apiRequest(`/profiles/${currentProfile.id}`, { + method: 'PUT', + body: JSON.stringify({ + ...currentProfile, + work_dir: response.path + }) + }); + console.log('Profile update response:', updateResponse); // Debug + + await selectProfile(currentProfile.id); + showSuccess('Directorio de trabajo actualizado correctamente'); + } + } catch (error) { + console.error('Error al seleccionar directorio:', error); // Debug + showError('Error al actualizar el directorio de trabajo'); + } +} + +// Output functions +function showError(message) { + const output = document.getElementById('outputArea'); + const timestamp = new Date().toLocaleTimeString(); + output.innerHTML += `\n[${timestamp}] ERROR: ${message}`; + output.scrollTop = output.scrollHeight; +} + +function showSuccess(message) { + const output = document.getElementById('outputArea'); + const timestamp = new Date().toLocaleTimeString(); + output.innerHTML += `\n[${timestamp}] SUCCESS: ${message}`; + output.scrollTop = output.scrollHeight; +} + +function clearOutput() { + const output = document.getElementById('outputArea'); + output.innerHTML = ''; +} + +// Modal helper functions +function closeModal(button) { + const modal = button.closest('.modal'); + if (modal) { + modal.remove(); + } +} + +// Global error handler +window.addEventListener('unhandledrejection', function(event) { + console.error('Unhandled promise rejection:', event.reason); + showError('An unexpected error occurred'); +}); + +// Export functions for use in other modules +window.showError = showError; +window.showSuccess = showSuccess; +window.closeModal = closeModal; +window.currentProfile = currentProfile; \ No newline at end of file diff --git a/claude/modal.js b/claude/modal.js new file mode 100644 index 0000000..4e17e74 --- /dev/null +++ b/claude/modal.js @@ -0,0 +1,39 @@ +// frontend/static/js/modal.js +// static/js/modal.js +function createModal(title, content, onSave = null) { + const modal = document.createElement('div'); + modal.className = 'fixed inset-0 bg-gray-500 bg-opacity-75 flex items-center justify-center z-50'; + + modal.innerHTML = ` +
+
+

${title}

+
+
+ ${content} +
+
+ + ${onSave ? ` + + ` : ''} +
+
+ `; + + document.body.appendChild(modal); + return modal; +} + +function closeModal(button) { + const modal = button.closest('.fixed'); + if (modal) { + modal.remove(); + } +} \ No newline at end of file diff --git a/claude/profile.js b/claude/profile.js new file mode 100644 index 0000000..4904f92 --- /dev/null +++ b/claude/profile.js @@ -0,0 +1,324 @@ +// frontend/static/js/profile.js +let selectedProfileId = localStorage.getItem('selectedProfileId') || 'default'; +let editingProfile = null; + +// Profile functions +async function loadProfiles() { + try { + const response = await apiRequest('/profiles'); + const profiles = Object.values(response); + + // Actualizar el selector manteniendo el valor seleccionado + const select = document.getElementById('profileSelect'); + select.innerHTML = profiles.map(profile => ` + + `).join(''); + + // Establecer el valor seleccionado después de actualizar las opciones + if (response[selectedProfileId]) { + select.value = selectedProfileId; + await selectProfile(selectedProfileId); + } else { + selectedProfileId = 'default'; + select.value = 'default'; + await selectProfile('default'); + } + + // Asegurarse de que el evento change no sobrescriba la selección + select.addEventListener('change', onProfileChange, { once: true }); + + } catch (error) { + showError('Error al cargar los perfiles'); + } +} + +async function selectProfile(profileId) { + try { + currentProfile = await apiRequest(`/profiles/${profileId}`); + updateWorkDirDisplay(); + } catch (error) { + showError('Failed to load profile'); + } +} + +async function changeProfile() { + const select = document.getElementById('profileSelect'); + await selectProfile(select.value); +} + +async function selectWorkDir() { + try { + const response = await apiRequest('/select-directory'); + if (response.path) { + await apiRequest(`/profiles/${currentProfile.id}`, { + method: 'PUT', + body: JSON.stringify({ + ...currentProfile, + work_dir: response.path + }) + }); + await selectProfile(currentProfile.id); + showSuccess('Work directory updated successfully'); + } + } catch (error) { + showError('Failed to update work directory'); + } +} + +// Profile editor modal + +function showProfileEditor(profile = null) { + editingProfile = profile; + const modal = document.createElement('div'); + modal.className = 'modal active'; + + const editableInputClass = "mt-1 block w-full rounded-md border-2 border-gray-300 bg-green-50 px-3 py-2 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"; + const readonlyInputClass = "mt-1 block w-full rounded-md border-2 border-gray-200 bg-gray-100 px-3 py-2 shadow-sm"; + + modal.innerHTML = ` + + `; + + document.body.appendChild(modal); +} + +async function saveProfile(event) { + event.preventDefault(); + const form = event.target; + const formData = new FormData(form); + + const profileData = { + id: formData.get('id'), + name: formData.get('name'), + work_dir: formData.get('work_dir'), + llm_settings: { + model: formData.get('llm_model'), + api_key: formData.get('api_key'), + temperature: parseFloat(formData.get('temperature')) + } + }; + + try { + if (editingProfile) { + await apiRequest(`/profiles/${editingProfile.id}`, { + method: 'PUT', + body: JSON.stringify(profileData) + }); + } else { + await apiRequest('/profiles', { + method: 'POST', + body: JSON.stringify(profileData) + }); + } + + await loadProfiles(); + closeModal(event.target); + showSuccess(`Perfil ${editingProfile ? 'actualizado' : 'creado'} correctamente`); + } catch (error) { + showError(`Error al ${editingProfile ? 'actualizar' : 'crear'} el perfil`); + } +} + +// static/js/profile.js +async function editProfile() { + if (!currentProfile) { + showError('No profile selected'); + return; + } + + const content = ` +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ `; + + const modal = createModal('Edit Profile', content, true); + modal.querySelector('[onclick="saveModal(this)"]').onclick = async () => { + await saveProfile(modal); + }; +} + +async function saveProfile(modal) { + const form = modal.querySelector('#profileForm'); + const formData = new FormData(form); + + const profileData = { + id: formData.get('id'), + name: formData.get('name'), + work_dir: formData.get('work_dir'), + llm_settings: { + model: formData.get('llm_model'), + api_key: formData.get('api_key'), + temperature: parseFloat(formData.get('temperature')) + } + }; + + try { + if (editingProfile) { + await apiRequest(`/profiles/${editingProfile.id}`, { + method: 'PUT', + body: JSON.stringify(profileData) + }); + } else { + await apiRequest('/profiles', { + method: 'POST', + body: JSON.stringify(profileData) + }); + } + + await loadProfiles(); + closeModal(modal); + showSuccess(`Perfil ${editingProfile ? 'actualizado' : 'creado'} correctamente`); + } catch (error) { + showError(`Error al ${editingProfile ? 'actualizar' : 'crear'} el perfil`); + } +} + +function newProfile() { + const editableInputClass = "mt-1 block w-full rounded-md border-2 border-gray-300 bg-green-50 px-3 py-2 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"; + const readonlyInputClass = "mt-1 block w-full rounded-md border-2 border-gray-200 bg-gray-100 px-3 py-2 shadow-sm"; + + const content = ` +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ `; + + const modal = createModal('New Profile', content, true); + editingProfile = null; + + modal.querySelector('[onclick="saveModal(this)"]').onclick = async () => { + await saveProfile(modal); + }; +} + +async function onProfileChange(event) { + const newProfileId = event.target.value; + if (newProfileId !== selectedProfileId) { + selectedProfileId = newProfileId; + localStorage.setItem('selectedProfileId', selectedProfileId); + await selectProfile(selectedProfileId); + } +} \ No newline at end of file diff --git a/claude/profile_manager.py b/claude/profile_manager.py new file mode 100644 index 0000000..3baa562 --- /dev/null +++ b/claude/profile_manager.py @@ -0,0 +1,133 @@ +# backend/core/profile_manager.py +from pathlib import Path +import json +from typing import Dict, Any, List, Optional +from datetime import datetime + + +class ProfileManager: + """Manages configuration profiles""" + + DEFAULT_PROFILE = { + "id": "default", + "name": "Default Profile", + "llm_settings": {"model": "gpt-4", "temperature": 0.7, "api_key": ""}, + "created_at": "", + "updated_at": "", + } + + def __init__(self, data_dir: Path): + self.data_dir = data_dir + self.profiles_file = data_dir / "profiles.json" + self.profiles: Dict[str, Dict] = self._load_profiles() + + def _load_profiles(self) -> Dict[str, Dict]: + """Load profiles from file""" + if self.profiles_file.exists(): + try: + with open(self.profiles_file, "r", encoding="utf-8") as f: + profiles = json.load(f) + # Ensure default profile exists + if "default" not in profiles: + profiles["default"] = self._create_default_profile() + return profiles + except Exception as e: + print(f"Error loading profiles: {e}") + return {"default": self._create_default_profile()} + else: + # Create directory if it doesn't exist + self.profiles_file.parent.mkdir(parents=True, exist_ok=True) + # Create default profile + profiles = {"default": self._create_default_profile()} + self._save_profiles(profiles) + return profiles + + def _create_default_profile(self) -> Dict[str, Any]: + """Create default profile with timestamp""" + profile = self.DEFAULT_PROFILE.copy() + now = datetime.now().isoformat() + profile["created_at"] = now + profile["updated_at"] = now + return profile + + def _save_profiles(self, profiles: Optional[Dict] = None): + """Save profiles to file""" + if profiles is None: + profiles = self.profiles + try: + print(f"Saving profiles to: {self.profiles_file}") # Agregar debug + with open(self.profiles_file, "w", encoding="utf-8") as f: + json.dump(profiles, f, indent=4) + print("Profiles saved successfully") # Agregar debug + except Exception as e: + print(f"Error saving profiles: {e}") # Agregar debug + raise # Re-lanzar la excepción para que se maneje arriba + + def get_all_profiles(self) -> List[Dict[str, Any]]: + """Get all profiles""" + return list(self.profiles.values()) + + def get_profile(self, profile_id: str) -> Optional[Dict[str, Any]]: + """Get specific profile""" + return self.profiles.get(profile_id) + + def create_profile(self, profile_data: Dict[str, Any]) -> Dict[str, Any]: + """Create new profile""" + if "id" not in profile_data: + raise ValueError("Profile must have an id") + + profile_id = profile_data["id"] + if profile_id in self.profiles: + raise ValueError(f"Profile {profile_id} already exists") + + # Add timestamps + now = datetime.now().isoformat() + profile_data["created_at"] = now + profile_data["updated_at"] = now + + # Ensure required fields + for key in ["name", "llm_settings"]: + if key not in profile_data: + profile_data[key] = self.DEFAULT_PROFILE[key] + + self.profiles[profile_id] = profile_data + self._save_profiles() + return profile_data + + def update_profile( + self, profile_id: str, profile_data: Dict[str, Any] + ) -> Dict[str, Any]: + """Update existing profile""" + try: + print(f"Updating profile {profile_id} with data: {profile_data}") + if profile_id not in self.profiles: + raise ValueError(f"Profile {profile_id} not found") + + if profile_id == "default" and "id" in profile_data: + raise ValueError("Cannot change id of default profile") + + # Update timestamp + profile_data["updated_at"] = datetime.now().isoformat() + + # Update profile + current_profile = self.profiles[profile_id].copy() # Hacer una copia + current_profile.update(profile_data) # Actualizar la copia + self.profiles[profile_id] = current_profile # Asignar la copia actualizada + + print(f"Updated profile: {self.profiles[profile_id]}") + self._save_profiles() + return self.profiles[profile_id] + except Exception as e: + print(f"Error in update_profile: {e}") # Agregar debug + raise + + def delete_profile(self, profile_id: str): + """Delete profile""" + if profile_id == "default": + raise ValueError("Cannot delete default profile") + + if profile_id not in self.profiles: + raise ValueError(f"Profile {profile_id} not found") + + del self.profiles[profile_id] + self._save_profiles() diff --git a/claude/profiles.json b/claude/profiles.json new file mode 100644 index 0000000..3b68f75 --- /dev/null +++ b/claude/profiles.json @@ -0,0 +1,26 @@ +{ + "default": { + "id": "default", + "name": "Default Profile", + "work_dir": "", + "llm_settings": { + "model": "gpt-4", + "temperature": 0.7, + "api_key": "" + }, + "created_at": "2025-02-07T12:47:49.766608", + "updated_at": "2025-02-07T12:47:49.766608" + }, + "1": { + "id": "1", + "name": "Base", + "work_dir": "C:/Estudio", + "llm_settings": { + "api_key": "333333333333", + "model": "gpt-4", + "temperature": 0.7 + }, + "created_at": "2025-02-07T13:00:43.541932", + "updated_at": "2025-02-07T23:34:43.039269" + } +} \ No newline at end of file diff --git a/claude/project_structure.txt b/claude/project_structure.txt new file mode 100644 index 0000000..46cb7fe --- /dev/null +++ b/claude/project_structure.txt @@ -0,0 +1,39 @@ +Estructura del proyecto original: + +└── LocalScriptsWeb + ├── backend + │ ├── core + │ │ ├── __init__.py + │ │ ├── directory_handler.py + │ │ ├── group_settings_manager.py + │ │ ├── profile_manager.py + │ │ ├── script_manager.py + │ │ └── workdir_config.py + │ ├── script_groups + │ │ ├── example_group + │ │ │ ├── __init__.py + │ │ │ ├── config.json + │ │ │ ├── x1.py + │ │ │ └── x2.py + │ │ ├── __init__.py + │ │ ├── base_script.py + │ │ └── config.json + │ ├── __init__.py + │ └── app.py + ├── data + │ └── profiles.json + ├── frontend + │ ├── static + │ │ ├── css + │ │ │ └── style.css + │ │ └── js + │ │ ├── main.js + │ │ ├── modal.js + │ │ ├── profile.js + │ │ ├── scripts.js + │ │ └── workdir_config.js + │ └── templates + │ ├── base.html + │ └── index.html + ├── claude_file_organizer.py + └── files.txt \ No newline at end of file diff --git a/claude/script_manager.py b/claude/script_manager.py new file mode 100644 index 0000000..03d83a6 --- /dev/null +++ b/claude/script_manager.py @@ -0,0 +1,189 @@ +# backend/core/script_manager.py +from pathlib import Path +import importlib.util +import inspect +from typing import Dict, List, Any, Optional +import json +from .group_settings_manager import GroupSettingsManager # Agregar esta importación + + +class ScriptManager: + def __init__(self, script_groups_dir: Path): + self.script_groups_dir = script_groups_dir + self.group_settings = GroupSettingsManager(script_groups_dir) + + def get_group_settings(self, group_id: str) -> Dict[str, Any]: + """Get settings for a script group""" + return self.group_settings.get_group_settings(group_id) + + def update_group_settings(self, group_id: str, settings: Dict[str, Any]): + """Update settings for a script group""" + return self.group_settings.update_group_settings(group_id, settings) + + def get_group_config_schema(self, group_id: str) -> Dict[str, Any]: + """Get configuration schema for a script group""" + config_file = self.script_groups_dir / group_id / "config.json" + print(f"Looking for config file: {config_file}") # Debug + + if config_file.exists(): + try: + with open(config_file, "r", encoding="utf-8") as f: + schema = json.load(f) + print(f"Loaded schema: {schema}") # Debug + return schema + except Exception as e: + print(f"Error loading group config schema: {e}") # Debug + else: + print(f"Config file not found: {config_file}") # Debug + + # Retornar un schema vacío si no existe el archivo + return {"group_name": group_id, "description": "", "config_schema": {}} + + def get_available_groups(self) -> List[Dict[str, Any]]: + """Get list of available script groups""" + groups = [] + + for group_dir in self.script_groups_dir.iterdir(): + if group_dir.is_dir() and not group_dir.name.startswith("_"): + groups.append( + { + "id": group_dir.name, + "name": group_dir.name.replace("_", " ").title(), + "path": str(group_dir), + } + ) + + return groups + + def get_group_scripts(self, group_id: str) -> List[Dict[str, Any]]: + """Get scripts for a specific group""" + group_dir = self.script_groups_dir / group_id + print(f"Looking for scripts in: {group_dir}") # Debug + + if not group_dir.exists() or not group_dir.is_dir(): + print(f"Directory not found: {group_dir}") # Debug + raise ValueError(f"Script group '{group_id}' not found") + + scripts = [] + for script_file in group_dir.glob("x[0-9].py"): + print(f"Found script file: {script_file}") # Debug + script_info = self._analyze_script(script_file) + if script_info: + scripts.append(script_info) + + return sorted(scripts, key=lambda x: x["id"]) + + def discover_groups(self) -> List[Dict[str, Any]]: + """Discover all script groups""" + groups = [] + + for group_dir in self.script_groups_dir.iterdir(): + if group_dir.is_dir() and not group_dir.name.startswith("_"): + group_info = self._analyze_group(group_dir) + if group_info: + groups.append(group_info) + + return groups + + def _analyze_group(self, group_dir: Path) -> Optional[Dict[str, Any]]: + """Analyze a script group directory""" + scripts = [] + + for script_file in group_dir.glob("x[0-9].py"): + try: + script_info = self._analyze_script(script_file) + if script_info: + scripts.append(script_info) + except Exception as e: + print(f"Error analyzing script {script_file}: {e}") + + if scripts: + return { + "id": group_dir.name, + "name": group_dir.name.replace("_", " ").title(), + "scripts": sorted(scripts, key=lambda x: x["id"]), + } + return None + + def _analyze_script(self, script_file: Path) -> Optional[Dict[str, Any]]: + """Analyze a single script file""" + try: + # Import script module + spec = importlib.util.spec_from_file_location(script_file.stem, script_file) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + + # Find script class + script_class = None + for name, obj in inspect.getmembers(module): + if ( + inspect.isclass(obj) + and obj.__module__ == module.__name__ + and hasattr(obj, "run") + ): + script_class = obj + break + + if script_class: + # Extraer la primera línea del docstring como nombre + docstring = inspect.getdoc(script_class) + if docstring: + name, *description = docstring.split("\n", 1) + description = description[0] if description else "" + else: + name = script_file.stem + description = "" + + return { + "id": script_file.stem, + "name": name.strip(), + "description": description.strip(), + "file": str(script_file.relative_to(self.script_groups_dir)), + } + + except Exception as e: + print(f"Error loading script {script_file}: {e}") + + return None + + def execute_script( + self, group_id: str, script_id: str, profile: Dict[str, Any] + ) -> Dict[str, Any]: + """Execute a specific script""" + # Get group settings first + group_settings = self.group_settings.get_group_settings(group_id) + work_dir = group_settings.get("work_dir") + + if not work_dir: + raise ValueError(f"No work directory configured for group {group_id}") + + script_file = self.script_groups_dir / group_id / f"{script_id}.py" + + if not script_file.exists(): + raise ValueError(f"Script {script_id} not found in group {group_id}") + + try: + # Import script module + spec = importlib.util.spec_from_file_location(script_id, script_file) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + + # Find and instantiate script class + script_class = None + for name, obj in inspect.getmembers(module): + if ( + inspect.isclass(obj) + and obj.__module__ == module.__name__ + and hasattr(obj, "run") + ): + script_class = obj + break + + if not script_class: + raise ValueError(f"No valid script class found in {script_id}") + + script = script_class() + return script.run(work_dir, profile) + + except Exception as e: + return {"status": "error", "error": str(e)} diff --git a/claude/scripts.js b/claude/scripts.js new file mode 100644 index 0000000..4f85782 --- /dev/null +++ b/claude/scripts.js @@ -0,0 +1,815 @@ +// frontend/static/js/scripts.js + +// Script groups state +let scriptGroups = []; + +// Load script groups when page loads +document.addEventListener('DOMContentLoaded', async () => { + await loadScriptGroups(); +}); + +// Load script groups when page loads +document.addEventListener('DOMContentLoaded', async () => { + await loadScriptGroups(); +}); + +async function loadScriptGroups() { + try { + // Obtener los grupos desde el servidor + const groups = await apiRequest('/script-groups'); + console.log('Loaded script groups:', groups); + + // Obtener el selector y el último grupo seleccionado + const select = document.getElementById('groupSelect'); + const lastGroupId = localStorage.getItem('lastGroupId'); + console.log('Last group ID:', lastGroupId); + + // Remover event listener anterior si existe + select.removeEventListener('change', handleGroupChange); + + // Construir las opciones + select.innerHTML = ` + + ${groups.map(group => ` + + `).join('')} + `; + + // Agregar event listener para cambios + select.addEventListener('change', handleGroupChange); + console.log('Added change event listener to groupSelect'); + + // Si hay un grupo guardado, cargarlo + if (lastGroupId) { + console.log('Loading last group scripts:', lastGroupId); + await loadGroupScripts(lastGroupId); + } + + } catch (error) { + console.error('Error al cargar grupos de scripts:', error); + showError('Error al cargar grupos de scripts'); + } +} + + +// Función para manejar el cambio de grupo +async function handleGroupChange(event) { + const groupId = event.target.value; + console.log('Group selection changed:', groupId); + + if (groupId) { + localStorage.setItem('lastGroupId', groupId); + console.log('Saved lastGroupId:', groupId); + } else { + localStorage.removeItem('lastGroupId'); + console.log('Removed lastGroupId'); + } + + await loadGroupScripts(groupId); +} + +// Actualizar función de cambio de perfil para mantener la persistencia +async function changeProfile() { + const select = document.getElementById('profileSelect'); + if (select.value) { + await selectProfile(select.value); + localStorage.setItem('lastProfileId', select.value); + + // Al cambiar de perfil, intentamos mantener el último grupo seleccionado + const lastGroupId = localStorage.getItem('lastGroupId'); + if (lastGroupId) { + const groupSelect = document.getElementById('groupSelect'); + if (groupSelect) { + groupSelect.value = lastGroupId; + await loadGroupScripts(lastGroupId); + } + } + } +} + +async function loadGroupScripts(groupId) { + const scriptList = document.getElementById('scriptList'); + + if (!groupId) { + scriptList.style.display = 'none'; + localStorage.removeItem('lastGroupId'); // Limpiar selección + return; + } + + // Guardar grupo seleccionado + localStorage.setItem('lastGroupId', groupId); + console.log('Group saved:', groupId); + + if (!currentProfile?.work_dir) { + scriptList.innerHTML = ` +
+
+
+

+ Por favor, seleccione primero un directorio de trabajo +

+
+
+
+ `; + scriptList.style.display = 'block'; + return; + } + + try { + console.log('Loading data for group:', groupId); + + // Actualizar el selector para reflejar la selección actual + const groupSelect = document.getElementById('groupSelect'); + if (groupSelect && groupSelect.value !== groupId) { + groupSelect.value = groupId; + } + + // Cargar y loguear scripts + let groupScripts, configSchema; + try { + groupScripts = await apiRequest(`/script-groups/${groupId}/scripts`); + console.log('Scripts loaded:', groupScripts); + } catch (e) { + console.error('Error loading scripts:', e); + throw e; + } + + try { + configSchema = await apiRequest(`/script-groups/${groupId}/config-schema`); + console.log('Config schema loaded:', configSchema); + } catch (e) { + console.error('Error loading config schema:', e); + throw e; + } + + // Intentar cargar configuración actual + let currentConfig = {}; + try { + currentConfig = await apiRequest( + `/workdir-config/${encodeURIComponent(currentProfile.work_dir)}/group/${groupId}` + ); + console.log('Current config loaded:', currentConfig); + } catch (e) { + console.warn('No existing configuration found, using defaults'); + } + + // Verificar que tenemos los datos necesarios + if (!groupScripts || !configSchema) { + throw new Error('Failed to load required data'); + } + + console.log('Rendering UI with:', { + groupScripts, + configSchema, + currentConfig + }); + + scriptList.innerHTML = ` + +
+
+
+

+ ${configSchema.group_name || 'Configuración'} +

+

${configSchema.description || ''}

+
+ +
+
+
+ ${Object.entries(configSchema.config_schema || {}).map(([key, field]) => ` +
+ + ${generateFormField(key, field, currentConfig[key])} +
+ `).join('')} +
+ +
+
+
+
+ + +
+ ${groupScripts.map(script => ` +
+
+
+

${script.name || script.id}

+

${script.description || 'Sin descripción disponible'}

+
+ +
+
+ `).join('')} +
`; + + scriptList.style.display = 'block'; + + // Agregar evento para guardar configuración + const form = document.getElementById('groupConfigForm'); + form.addEventListener('submit', async (e) => { + e.preventDefault(); + await saveGroupConfig(groupId, form); + }); + + } catch (error) { + console.error('Error in loadGroupScripts:', error); + showError('Failed to load scripts and configuration'); + } +} + +// Update script groups display +function updateScriptGroupsDisplay() { + const container = document.getElementById('scriptGroups'); + + if (!scriptGroups.length) { + container.innerHTML = '

No script groups available

'; + return; + } + + container.innerHTML = scriptGroups.map(group => ` +
+
+

${group.name}

+ +
+
+ ${group.scripts.map(script => ` +
+
+

${script.name}

+

${script.description || 'No description available'}

+
+
+ +
+
+ `).join('')} +
+
+ `).join(''); +} + +// Run a script +async function runScript(groupId, scriptId) { + if (!currentProfile?.work_dir) { + showError('Please select a work directory first'); + return; + } + + try { + const result = await apiRequest(`/scripts/${groupId}/${scriptId}/run`, { + method: 'POST', + body: JSON.stringify({ + work_dir: currentProfile.work_dir, + profile: currentProfile + }) + }); + + if (result.status === 'error') { + showError(result.error); + } else { + showSuccess(`Script ${scriptId} executed successfully`); + if (result.output) { + const output = document.getElementById('outputArea'); + output.innerHTML += `\n[${new Date().toLocaleTimeString()}] ${result.output}`; + output.scrollTop = output.scrollHeight; + } + } + } catch (error) { + showError(`Failed to run script: ${error.message}`); + } +} + +// Configure script group +async function configureGroup(groupId) { + if (!currentProfile?.work_dir) { + showError('Please select a work directory first'); + return; + } + + try { + const config = await getGroupConfig(groupId); + showGroupConfigEditor(groupId, config); + } catch (error) { + showError('Failed to load group configuration'); + } +} + +// Show group configuration editor +function showGroupConfigEditor(groupId, config) { + const group = scriptGroups.find(g => g.id === groupId); + if (!group) return; + + const modal = document.createElement('div'); + modal.className = 'modal active'; + + modal.innerHTML = ` + + `; + + document.body.appendChild(modal); +} + +function generateFormField(key, field, currentValue) { + switch (field.type) { + case 'string': + return ` + + `; + case 'number': + return ` + + `; + case 'boolean': + return ` + + `; + case 'select': + return ` + + `; + default: + return ``; + } +} + +async function loadGroupScripts(groupId) { + const scriptList = document.getElementById('scriptList'); + + if (!groupId) { + scriptList.style.display = 'none'; + localStorage.removeItem('lastGroupId'); + return; + } + + if (!currentProfile?.work_dir) { + scriptList.innerHTML = ` +
+
+
+

+ Por favor, seleccione primero un directorio de trabajo +

+
+
+
+ `; + scriptList.style.display = 'block'; + return; + } + + try { + console.log('Loading data for group:', groupId); + + // Cargar y loguear scripts + let groupScripts, configSchema; + try { + groupScripts = await apiRequest(`/script-groups/${groupId}/scripts`); + console.log('Scripts loaded:', groupScripts); + } catch (e) { + console.error('Error loading scripts:', e); + throw e; + } + + try { + configSchema = await apiRequest(`/script-groups/${groupId}/config-schema`); + console.log('Config schema loaded:', configSchema); + } catch (e) { + console.error('Error loading config schema:', e); + throw e; + } + + // Intentar cargar configuración actual + let currentConfig = {}; + try { + console.log('Loading current config for work_dir:', currentProfile.work_dir); + currentConfig = await apiRequest( + `/workdir-config/${encodeURIComponent(currentProfile.work_dir)}/group/${groupId}` + ); + console.log('Current config loaded:', currentConfig); + } catch (e) { + console.warn('No existing configuration found, using defaults'); + } + + // Verificar que tenemos los datos necesarios + if (!groupScripts || !configSchema) { + throw new Error('Failed to load required data'); + } + + console.log('Rendering UI with:', { + groupScripts, + configSchema, + currentConfig + }); + + scriptList.innerHTML = ` + +
+
+
+

+ ${configSchema.group_name || 'Configuración'} +

+

${configSchema.description || ''}

+
+ +
+
+
+ ${Object.entries(configSchema.config_schema || {}).map(([key, field]) => ` +
+ + ${generateFormField(key, field, currentConfig[key])} +
+ `).join('')} +
+ +
+
+
+
+ + +
+ ${groupScripts.map(script => ` +
+
+
+

${script.name || script.id}

+

${script.description || 'Sin descripción disponible'}

+
+ +
+
+ `).join('')} +
`; + + scriptList.style.display = 'block'; + + // Agregar evento para guardar configuración + const form = document.getElementById('groupConfigForm'); + form.addEventListener('submit', async (e) => { + e.preventDefault(); + await saveGroupConfig(groupId, form); + }); + + } catch (error) { + console.error('Error in loadGroupScripts:', error); + showError('Failed to load scripts and configuration'); + } +} + +async function editConfigSchema(groupId) { + try { + const schema = await apiRequest(`/script-groups/${groupId}/config-schema`); + const configSection = document.createElement('div'); + configSection.id = 'schemaEditor'; + configSection.className = 'mb-6 bg-white shadow sm:rounded-lg'; + + configSection.innerHTML = ` +
+

Editar Configuración del Esquema

+ +
+
+
+
+
+ + +
+
+ +
+
+
+ + +
+
+
+

Parámetros

+
+ ${Object.entries(schema.config_schema).map(([key, param]) => ` +
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ ${param.type === 'select' ? ` +
+ + +
+ ` : ''} +
+
+ `).join('')} +
+
+ + +
+
+
+ `; + + // Insertamos el editor justo después del botón "Edit Schema" + const scriptList = document.getElementById('scriptList'); + const existingEditor = document.getElementById('schemaEditor'); + if (existingEditor) { + existingEditor.remove(); + } + scriptList.insertBefore(configSection, scriptList.firstChild); + + } catch (error) { + showError('Error al cargar el esquema de configuración'); + } +} + +function createModal(title, content, onSave = null) { + const modal = document.createElement('div'); + modal.className = 'fixed inset-0 bg-gray-500 bg-opacity-75 flex items-center justify-center z-50 p-4'; + + modal.innerHTML = ` +
+
+

${title}

+
+
+ ${content} +
+
+ + ${onSave ? ` + + ` : ''} +
+
+ `; + + document.body.appendChild(modal); + return modal; +} + +function addParameter(button) { + const parametersDiv = button.closest('.space-y-4').querySelector('#parameters'); + const newParam = document.createElement('div'); + newParam.className = 'parameter-item bg-gray-50 p-4 rounded-md relative'; + newParam.innerHTML = ` + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ `; + parametersDiv.appendChild(newParam); +} + +function removeParameter(button) { + button.closest('.parameter-item').remove(); +} + +function handleTypeChange(select) { + const paramItem = select.closest('.parameter-item'); + const optionsDiv = paramItem.querySelector('[name="param_options"]')?.closest('div'); + + if (select.value === 'select') { + if (!optionsDiv) { + const div = document.createElement('div'); + div.className = 'col-span-2'; + div.innerHTML = ` + + + `; + paramItem.querySelector('.grid').appendChild(div); + } + } else { + optionsDiv?.remove(); + } +} + +async function saveConfigSchema(groupId, modal) { + const form = modal.querySelector('div'); + const schema = { + group_name: form.querySelector('[name="group_name"]').value, + description: form.querySelector('[name="description"]').value, + config_schema: {} + }; + + // Recopilar parámetros + form.querySelectorAll('.parameter-item').forEach(item => { + const name = item.querySelector('[name="param_name"]').value; + const type = item.querySelector('[name="param_type"]').value; + const description = item.querySelector('[name="param_description"]').value; + const defaultValue = item.querySelector('[name="param_default"]').value; + + const param = { + type, + description, + default: type === 'boolean' ? defaultValue === 'true' : defaultValue + }; + + if (type === 'select') { + const options = item.querySelector('[name="param_options"]').value + .split(',') + .map(opt => opt.trim()) + .filter(Boolean); + param.options = options; + } + + schema.config_schema[name] = param; + }); + + try { + await apiRequest(`/script-groups/${groupId}/config-schema`, { + method: 'PUT', + body: JSON.stringify(schema) + }); + + closeModal(modal.querySelector('button')); + showSuccess('Configuration schema updated successfully'); + // Recargar la página para mostrar los cambios + loadGroupScripts(groupId); + } catch (error) { + showError('Failed to update configuration schema'); + } +} + +function showScriptForm(script) { + const modal = document.createElement('div'); + modal.className = 'modal active'; + + modal.innerHTML = ` + + `; + + document.body.appendChild(modal); +} \ No newline at end of file diff --git a/claude/style.css b/claude/style.css new file mode 100644 index 0000000..a343b9b --- /dev/null +++ b/claude/style.css @@ -0,0 +1,31 @@ +/* frontend/static/css/style.css */ + +/* Solo mantenemos estilos específicos que no podemos lograr fácilmente con Tailwind */ +.output-area { + white-space: pre-wrap; + word-wrap: break-word; +} + +/* Estilos para modales que no se pueden lograr fácilmente con Tailwind */ +.modal { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.5); + display: flex; + justify-content: center; + align-items: center; + z-index: 1000; +} + +.modal-content { + background: white; + padding: 2rem; + border-radius: 0.5rem; + max-width: 600px; + width: 90%; + max-height: 90vh; + overflow-y: auto; +} \ No newline at end of file diff --git a/claude/workdir_config.js b/claude/workdir_config.js new file mode 100644 index 0000000..8d17144 --- /dev/null +++ b/claude/workdir_config.js @@ -0,0 +1,161 @@ +// frontend/static/js/workdir_config.js + +async function getWorkDirConfig() { + if (!currentProfile?.work_dir) { + showError('No se ha seleccionado un directorio de trabajo'); + return null; + } + + try { + return await apiRequest(`/workdir-config/${encodeURIComponent(currentProfile.work_dir)}`); + } catch (error) { + showError('Error al cargar la configuración del directorio de trabajo'); + return null; + } +} + +async function getGroupConfig(groupId) { + if (!currentProfile?.work_dir) { + showError('No se ha seleccionado un directorio de trabajo'); + return null; + } + + try { + return await apiRequest( + `/workdir-config/${encodeURIComponent(currentProfile.work_dir)}/group/${groupId}` + ); + } catch (error) { + showError('Error al cargar la configuración del grupo'); + return null; + } +} + +async function updateGroupConfig(groupId, settings) { + if (!currentProfile?.work_dir) { + showError('No se ha seleccionado un directorio de trabajo'); + return false; + } + + try { + await apiRequest( + `/workdir-config/${encodeURIComponent(currentProfile.work_dir)}/group/${groupId}`, + { + method: 'PUT', + body: JSON.stringify(settings) + } + ); + showSuccess('Group configuration updated successfully'); + return true; + } catch (error) { + showError('Failed to update group configuration'); + return false; + } +} + +function showConfigEditor(config, schema) { + const modal = document.createElement('div'); + modal.className = 'modal active'; + + const formContent = Object.entries(schema).map(([key, field]) => ` +
+ + ${getInputByType(key, field, config[key])} +
+ `).join(''); + + modal.innerHTML = ` + + `; + + document.body.appendChild(modal); +} + +function getInputByType(key, field, value) { + switch (field.type) { + case 'select': + return ` + `; + case 'boolean': + return ` + `; + case 'number': + return ` + `; + default: + return ` + `; + } +} + +// static/js/workdir_config.js +async function showWorkDirConfig() { + if (!currentProfile?.work_dir) { + showError('No se ha seleccionado un directorio de trabajo'); + return; + } + + try { + const config = await getWorkDirConfig(); + + const content = ` +
+
+

Directory

+

${currentProfile.work_dir}

+
+
+

Version

+

${config.version}

+
+
+

Group Configurations

+
+ ${Object.entries(config.group_settings || {}).map(([groupId, settings]) => ` +
+
${groupId}
+
${JSON.stringify(settings, null, 2)}
+
+ `).join('')} +
+
+
+ `; + + createModal('Work Directory Configuration', content); + } catch (error) { + showError('Error al cargar la configuración del directorio de trabajo'); + } +} + +function closeModal(button) { + const modal = button.closest('.modal'); + if (modal) { + modal.remove(); + } +} \ No newline at end of file diff --git a/claude/workdir_config.py b/claude/workdir_config.py new file mode 100644 index 0000000..cb34d6a --- /dev/null +++ b/claude/workdir_config.py @@ -0,0 +1,72 @@ +# backend/core/workdir_config.py +from pathlib import Path +import json +from typing import Dict, Any, Optional +from datetime import datetime + +class WorkDirConfigManager: + """Manages configuration files in work directories""" + + DEFAULT_CONFIG = { + "version": "1.0", + "created_at": "", + "updated_at": "", + "group_settings": {} + } + + def __init__(self, work_dir: str): + self.work_dir = Path(work_dir) + self.config_file = self.work_dir / "script_config.json" + + def get_config(self) -> Dict[str, Any]: + """Get configuration for work directory""" + if self.config_file.exists(): + try: + with open(self.config_file, 'r', encoding='utf-8') as f: + return json.load(f) + except Exception as e: + print(f"Error loading work dir config: {e}") + return self._create_default_config() + return self._create_default_config() + + def _create_default_config(self) -> Dict[str, Any]: + """Create default configuration""" + config = self.DEFAULT_CONFIG.copy() + now = datetime.now().isoformat() + config["created_at"] = now + config["updated_at"] = now + return config + + def save_config(self, config: Dict[str, Any]): + """Save configuration to file""" + # Ensure work directory exists + self.work_dir.mkdir(parents=True, exist_ok=True) + + # Update timestamp + config["updated_at"] = datetime.now().isoformat() + + # Save config + with open(self.config_file, 'w', encoding='utf-8') as f: + json.dump(config, f, indent=4) + + def get_group_config(self, group_id: str) -> Dict[str, Any]: + """Get configuration for specific script group""" + config = self.get_config() + return config["group_settings"].get(group_id, {}) + + def update_group_config(self, group_id: str, settings: Dict[str, Any]): + """Update configuration for specific script group""" + config = self.get_config() + + if "group_settings" not in config: + config["group_settings"] = {} + + config["group_settings"][group_id] = settings + self.save_config(config) + + def remove_group_config(self, group_id: str): + """Remove configuration for specific script group""" + config = self.get_config() + if group_id in config.get("group_settings", {}): + del config["group_settings"][group_id] + self.save_config(config) \ No newline at end of file diff --git a/claude/x1.py b/claude/x1.py new file mode 100644 index 0000000..257c8de --- /dev/null +++ b/claude/x1.py @@ -0,0 +1,108 @@ +# backend/script_groups/example_group/x1.py +from backend.script_groups.base_script import BaseScript +import os +from pathlib import Path +import json +import csv +from datetime import datetime + +class FileCounter(BaseScript): + """ + File Analysis + Analyzes files in directory with configurable filters and reporting + """ + + def run(self, work_dir: str, profile: dict) -> dict: + try: + # Get configuration + config = self.get_config(work_dir, "example_group") + + # Process configuration values + exclude_dirs = [d.strip() for d in config.get("exclude_dirs", "").split(",") if d.strip()] + count_hidden = config.get("count_hidden", False) + min_size = config.get("min_size", 0) + save_report = config.get("save_report", True) + report_format = config.get("report_format", "json") + + # Initialize counters + extension_counts = {} + total_files = 0 + total_size = 0 + skipped_files = 0 + + # Walk through directory + for root, dirs, files in os.walk(work_dir): + # Skip excluded directories + dirs[:] = [d for d in dirs if d not in exclude_dirs] + + for file in files: + file_path = Path(root) / file + + # Skip hidden files if not counting them + if not count_hidden and file.startswith('.'): + skipped_files += 1 + continue + + # Check file size + try: + file_size = file_path.stat().st_size + if file_size < min_size: + skipped_files += 1 + continue + except: + continue + + # Count file + total_files += 1 + total_size += file_size + ext = file_path.suffix.lower() or 'no extension' + extension_counts[ext] = extension_counts.get(ext, 0) + 1 + + # Prepare results + results = { + "scan_time": datetime.now().isoformat(), + "total_files": total_files, + "total_size": total_size, + "skipped_files": skipped_files, + "extension_counts": extension_counts + } + + # Save report if configured + if save_report: + report_path = Path(work_dir) / f"file_analysis.{report_format}" + if report_format == "json": + with open(report_path, 'w') as f: + json.dump(results, f, indent=2) + elif report_format == "csv": + with open(report_path, 'w', newline='') as f: + writer = csv.writer(f) + writer.writerow(["Extension", "Count"]) + for ext, count in sorted(extension_counts.items()): + writer.writerow([ext, count]) + else: # txt + with open(report_path, 'w') as f: + f.write(f"File Analysis Report\n") + f.write(f"Generated: {results['scan_time']}\n\n") + f.write(f"Total Files: {total_files}\n") + f.write(f"Total Size: {total_size:,} bytes\n") + f.write(f"Skipped Files: {skipped_files}\n\n") + f.write("Extension Counts:\n") + for ext, count in sorted(extension_counts.items()): + f.write(f"{ext}: {count}\n") + + return { + "status": "success", + "data": results, + "output": f"Found {total_files:,} files ({total_size:,} bytes)\n" + + f"Skipped {skipped_files} files\n\n" + + "Extensions:\n" + "\n".join( + f"{ext}: {count:,} files" + for ext, count in sorted(extension_counts.items()) + ) + } + + except Exception as e: + return { + "status": "error", + "error": str(e) + } \ No newline at end of file diff --git a/claude/x2.py b/claude/x2.py new file mode 100644 index 0000000..c3b805b --- /dev/null +++ b/claude/x2.py @@ -0,0 +1,104 @@ +# backend/script_groups/example_group/x2.py +from backend.script_groups.base_script import BaseScript +import psutil +import json +from datetime import datetime +from pathlib import Path + +class SystemInfo(BaseScript): + """ + System Monitor + Collects and analyzes system performance metrics + """ + + def run(self, work_dir: str, profile: dict) -> dict: + try: + # Get configuration from the same config.json + config = self.get_config(work_dir, "example_group") + save_report = config.get("save_report", True) + report_format = config.get("report_format", "json") + + # Collect system information + cpu_freq = psutil.cpu_freq() + memory = psutil.virtual_memory() + disk = psutil.disk_usage(work_dir) + + info = { + "timestamp": datetime.now().isoformat(), + "cpu": { + "cores": psutil.cpu_count(), + "physical_cores": psutil.cpu_count(logical=False), + "frequency": { + "current": round(cpu_freq.current, 2) if cpu_freq else None, + "min": round(cpu_freq.min, 2) if cpu_freq else None, + "max": round(cpu_freq.max, 2) if cpu_freq else None + }, + "usage_percent": psutil.cpu_percent(interval=1) + }, + "memory": { + "total": memory.total, + "available": memory.available, + "used": memory.used, + "percent": memory.percent + }, + "disk": { + "total": disk.total, + "used": disk.used, + "free": disk.free, + "percent": disk.percent + }, + "network": { + "interfaces": list(psutil.net_if_addrs().keys()), + "connections": len(psutil.net_connections()) + } + } + + # Save report if configured + if save_report: + report_path = Path(work_dir) / f"system_info.{report_format}" + if report_format == "json": + with open(report_path, 'w') as f: + json.dump(info, f, indent=2) + elif report_format == "csv": + with open(report_path, 'w', newline='') as f: + writer = csv.writer(f) + writer.writerow(["Metric", "Value"]) + writer.writerow(["CPU Cores", info["cpu"]["cores"]]) + writer.writerow(["CPU Usage", f"{info['cpu']['usage_percent']}%"]) + writer.writerow(["Memory Total", f"{info['memory']['total']:,} bytes"]) + writer.writerow(["Memory Used", f"{info['memory']['percent']}%"]) + writer.writerow(["Disk Total", f"{info['disk']['total']:,} bytes"]) + writer.writerow(["Disk Used", f"{info['disk']['percent']}%"]) + else: # txt + with open(report_path, 'w') as f: + f.write(f"System Information Report\n") + f.write(f"Generated: {info['timestamp']}\n\n") + f.write(f"CPU:\n") + f.write(f" Cores: {info['cpu']['cores']}\n") + f.write(f" Usage: {info['cpu']['usage_percent']}%\n\n") + f.write(f"Memory:\n") + f.write(f" Total: {info['memory']['total']:,} bytes\n") + f.write(f" Used: {info['memory']['percent']}%\n\n") + f.write(f"Disk:\n") + f.write(f" Total: {info['disk']['total']:,} bytes\n") + f.write(f" Used: {info['disk']['percent']}%\n") + + # Format output + output = f"""System Information: +CPU: {info['cpu']['cores']} cores ({info['cpu']['usage_percent']}% usage) +Memory: {info['memory']['percent']}% used ({info['memory']['available']:,} bytes available) +Disk: {info['disk']['percent']}% used ({info['disk']['free']:,} bytes free) +Network Interfaces: {', '.join(info['network']['interfaces'])} +Active Connections: {info['network']['connections']}""" + + return { + "status": "success", + "data": info, + "output": output + } + + except Exception as e: + return { + "status": "error", + "error": str(e) + } \ No newline at end of file