scriptmonkey
Advanced tools
| import os | ||
| from rich.console import Console | ||
| from .utils.tree import create_tree | ||
| from .utils.file_handler import read_file | ||
| from .utils.ui import render_response_with_syntax_highlighting | ||
| from .utils.parsers import remove_code_block_lines | ||
| from .openai_client.client import chatgpt_json, chatgpt | ||
| from .openai_client.basemodels import ProjectStructureResponse | ||
| console = Console() | ||
| def generate_project_structure(description: str) -> ProjectStructureResponse: | ||
| """Generates the project structure based on the user's project description using OpenAI.""" | ||
| instructions = ( | ||
| "Generate a detailed project structure for a multi-level application. The project will be placed directly inside a folder named 'generated_project'." | ||
| "\n- Do NOT include 'generated_project/' as part of the paths. All paths should be relative to the root of the project directory, meaning they should start directly with the file or folder names as if they are inside 'generated_project'." | ||
| "\n- Provide a list of directories and files with their full relative paths." | ||
| "\n- Each directory should end with a '/' to indicate that it is a folder." | ||
| "\n- For each file or directory, include a 'description' that explains its purpose." | ||
| "\n- If the file is a Python code file, also include a 'functions' list. For each function, include:" | ||
| "\n - 'function_name': The name of the function." | ||
| "\n - 'description': A description of what the function does." | ||
| "\n - 'inputs': A list of the function's expected inputs, including data types." | ||
| "\n - 'outputs': A list of the function's expected outputs, including data types." | ||
| "\n- Do not include any extra explanations, commentary, or introductory text. Only provide the structured data as requested." | ||
| ) | ||
| # Call the chatgpt_json function to get structured project plan | ||
| project_structure = chatgpt_json( | ||
| instructions=instructions, content=description, response_format=ProjectStructureResponse | ||
| ) | ||
| return project_structure | ||
| def generate_code_for_file(file_description: dict, project_description: str, project_files: list) -> str: | ||
| """ | ||
| Generates content for a given file based on its description using the chatgpt() function. | ||
| Args: | ||
| file_description (dict): The description of the file for which content is being generated. | ||
| project_description (str): A high-level description of the project's purpose and goals. | ||
| project_files (list): List of all project files for context. | ||
| Returns: | ||
| str: The generated content for the file. | ||
| """ | ||
| # Gather context about the project goal and other files | ||
| context = gather_project_context(project_description, project_files) | ||
| # Extract the file extension to inform the content type | ||
| file_extension = os.path.splitext(file_description["path"])[1].lower().strip(".") | ||
| # Dynamically adjust the content type description based on the file extension | ||
| if file_extension: | ||
| content_type_description = f"{file_extension.upper()} file content" | ||
| else: | ||
| content_type_description = "text content" | ||
| # Prepare instructions for OpenAI to generate content based on the file description and type | ||
| instructions = ( | ||
| f"Write the complete content for a {content_type_description} that fulfills the following requirements. " | ||
| "Consider the context of the entire project when generating the content and make use of imports where available and appropriate." | ||
| "Use relevant imports, references, and appropriate formatting or structure where necessary. Do not add extra commentary or explanation. " | ||
| "Make sure to return the content directly, without wrapping it in any code fences like triple quotes or backticks ." | ||
| "i.e. DO NOT include any triple backtrick wrappers at all for any code, (e.g. ```python<content here>```) just return the code as plain text." | ||
| f"\n\nFile Description: {file_description['description']}" | ||
| f"\n\n{context}\n" | ||
| ) | ||
| # Include functions for code files (if provided) | ||
| if file_description.get("functions"): | ||
| instructions += "\n\nFunctions:\n" | ||
| for function in file_description["functions"]: | ||
| instructions += ( | ||
| f"- {function['function_name']}: {function['description']} " | ||
| f"(Inputs: {function['inputs']}, Outputs: {function['outputs']})\n" | ||
| ) | ||
| # Call the chatgpt function to generate the content | ||
| generated_content = chatgpt(prompt=instructions) | ||
| # Clean up any unintended code blocks | ||
| generated_content = remove_code_block_lines(generated_content) | ||
| return generated_content | ||
| def build_project( | ||
| project_structure_response: dict, project_description: str, base_directory: str = "./generated_project" | ||
| ): | ||
| """Creates the directories and files for the project and generates code content for all file types.""" | ||
| # Extract the list of project files for context | ||
| project_files = project_structure_response["files"] | ||
| # Iterate through each file in the project structure | ||
| for project_file in project_files: | ||
| file_path = os.path.join(base_directory, project_file["path"].lstrip("/")) | ||
| # Check if it's a directory or file (directories end with '/') | ||
| if file_path.endswith("/"): | ||
| os.makedirs(file_path, exist_ok=True) | ||
| print(f"š ScriptMonkey created directory: {file_path}") | ||
| else: | ||
| os.makedirs(os.path.dirname(file_path), exist_ok=True) | ||
| # Generate content for all files, including Python, HTML, JSON, CSS, etc. | ||
| generated_content = generate_code_for_file(project_file, project_description, project_files) | ||
| # Write the generated content to the file if it doesn't already exist | ||
| if not os.path.exists(file_path): | ||
| with open(file_path, "w") as f: | ||
| f.write(generated_content) | ||
| print(f"š ScriptMonkey created file with generated content at: '{file_path}'.") | ||
| else: | ||
| print(f"File already exists, skipping: {file_path}") | ||
| def gather_project_context(project_description: str, project_files: list) -> str: | ||
| """ | ||
| Gathers a summary of the project goal and all existing files with their key functions or classes. | ||
| Args: | ||
| project_description (str): A high-level description of the project's purpose and goals. | ||
| project_files (list): List of project file descriptions. | ||
| Returns: | ||
| str: A summary of the project goal and existing modules, classes, and functions. | ||
| """ | ||
| context = f"Project Goal: {project_description}\n\n" | ||
| context += "Project Context:\n" | ||
| for file in project_files: | ||
| if file["functions"]: | ||
| context += f"- In '{file['path']}', the following functions are defined:\n" | ||
| for function in file["functions"]: | ||
| context += f" - {function['function_name']}: {function['description']} (Inputs: {function['inputs']}, Outputs: {function['outputs']})\n" | ||
| else: | ||
| context += f"- '{file['path']}' is defined with no specific functions listed.\n" | ||
| return context | ||
| def generate_readme(description: str, project_structure: dict) -> str: | ||
| """Generates a README.md content based on the project description and structure.""" | ||
| instructions = ( | ||
| "Write a complete README.md file based on the following project details. " | ||
| "The README should include the project overview, installation instructions, usage guide, file structure summary, key features, and configuration details. " | ||
| "Make sure the README is well-structured and formatted using Markdown without wrapping the entire README in backticks or any other non-readme commentary." | ||
| "Do not include any commentary, explanations, or text outside of the README content." | ||
| f"\n\nProject Description: {description}\n" | ||
| f"\nProject Structure: {project_structure}\n" | ||
| ) | ||
| readme_content = chatgpt(prompt=instructions) | ||
| readme_content = readme_content.strip("```markdown").strip("```") | ||
| return readme_content | ||
| def ask_gpt_with_files(question, file_paths, include_tree=False): | ||
| """ | ||
| Constructs a detailed and flexible prompt for ChatGPT using a question and optionally including content from specified files. | ||
| """ | ||
| prompt = ( | ||
| f"### Question:\n" | ||
| f"{question}\n\n" | ||
| "If I have included any files below, you can use them for additional context for this question. " | ||
| "Please analyze the provided files below (if available) as needed and reference them when forming your answer. " | ||
| "If the answer involves code, please format any code examples using Markdown with properly labeled language-specific code blocks. " | ||
| "Your response should be in Markdown format to preserve readability.\n\n" | ||
| ) | ||
| if file_paths: | ||
| prompt += "### Files Provided:\n" | ||
| for path in file_paths: | ||
| try: | ||
| content = read_file(path) | ||
| prompt += ( | ||
| f"## File: {path}\n" | ||
| f"The content of the file '{path}' is included below. Use this as context for answering the question:\n\n" | ||
| f"```\n{content}\n```\n\n" | ||
| ) | ||
| except FileNotFoundError: | ||
| console.print(f"[bold yellow]Warning: {path} not found. Skipping this file.[/bold yellow]") | ||
| except Exception as e: | ||
| console.print(f"[bold red]Error reading {path}: {e}[/bold red]") | ||
| else: | ||
| prompt += ( | ||
| "No specific files have been provided, so please base your response solely on the question above. " | ||
| "If the response includes any code examples or technical explanations, please use Markdown formatting with language-specific code blocks for clarity.\n" | ||
| ) | ||
| # Include the directory tree if the flag is set | ||
| if include_tree: | ||
| start_directory = os.getcwd() | ||
| tree = create_tree(start_directory) | ||
| prompt += "### Directory Tree:\n" | ||
| prompt += f"The directory tree of the current working directory is included below (up to a depth of 6 levels):\n\n```\n{tree}\n```\n\n" | ||
| console.print("- - Directory Tree - -") | ||
| console.print(tree) | ||
| # Output the constructed prompt to the console for transparency | ||
| console.rule("š ScriptMonkey is Thinking š") | ||
| console.rule() | ||
| # Use the OpenAI API to get a response | ||
| try: | ||
| response = chatgpt(prompt=prompt) | ||
| # Display the response using rich markdown and detect code blocks | ||
| console.rule("š ANSWER š") | ||
| render_response_with_syntax_highlighting(response) | ||
| console.print("\n") | ||
| console.rule() | ||
| except Exception as e: | ||
| console.print(f"[bold red]Error using OpenAI API: {e}[/bold red]") |
| import os | ||
| from .openai_client import chatgpt | ||
| from .utils.parsers import remove_code_block_lines | ||
| def generate_code_for_file(file_description: dict, project_description: str, project_files: list) -> str: | ||
| """ | ||
| Generates content for a given file based on its description using the chatgpt() function. | ||
| Args: | ||
| file_description (dict): The description of the file for which content is being generated. | ||
| project_description (str): A high-level description of the project's purpose and goals. | ||
| project_files (list): List of all project files for context. | ||
| Returns: | ||
| str: The generated content for the file. | ||
| """ | ||
| # Gather context about the project goal and other files | ||
| context = gather_project_context(project_description, project_files) | ||
| # Extract the file extension to inform the content type | ||
| file_extension = os.path.splitext(file_description["path"])[1].lower().strip(".") | ||
| # Dynamically adjust the content type description based on the file extension | ||
| if file_extension: | ||
| content_type_description = f"{file_extension.upper()} file content" | ||
| else: | ||
| content_type_description = "text content" | ||
| # Prepare instructions for OpenAI to generate content based on the file description and type | ||
| instructions = ( | ||
| f"Write the complete content for a {content_type_description} that fulfills the following requirements. " | ||
| "Consider the context of the entire project when generating the content and make use of imports where available and appropriate." | ||
| "Use relevant imports, references, and appropriate formatting or structure where necessary. Do not add extra commentary or explanation. " | ||
| "Make sure to return the content directly, without wrapping it in any code fences like triple quotes or backticks ." | ||
| "i.e. DO NOT include any triple backtrick wrappers at all for any code, (e.g. ```python<content here>```) just return the code as plain text." | ||
| f"\n\nFile Description: {file_description['description']}" | ||
| f"\n\n{context}\n" | ||
| ) | ||
| # Include functions for code files (if provided) | ||
| if file_description.get("functions"): | ||
| instructions += "\n\nFunctions:\n" | ||
| for function in file_description["functions"]: | ||
| instructions += ( | ||
| f"- {function['function_name']}: {function['description']} " | ||
| f"(Inputs: {function['inputs']}, Outputs: {function['outputs']})\n" | ||
| ) | ||
| # Call the chatgpt function to generate the content | ||
| generated_content = chatgpt(prompt=instructions) | ||
| # Clean up any unintended code blocks | ||
| generated_content = remove_code_block_lines(generated_content) | ||
| return generated_content | ||
| def create_project_structure( | ||
| project_structure_response: dict, project_description: str, base_directory: str = "./generated_project" | ||
| ): | ||
| """Creates the directories and files for the project and generates code content for all file types.""" | ||
| # Extract the list of project files for context | ||
| project_files = project_structure_response["files"] | ||
| # Iterate through each file in the project structure | ||
| for project_file in project_files: | ||
| file_path = os.path.join(base_directory, project_file["path"].lstrip("/")) | ||
| # Check if it's a directory or file (directories end with '/') | ||
| if file_path.endswith("/"): | ||
| os.makedirs(file_path, exist_ok=True) | ||
| print(f"š ScriptMonkey created directory: {file_path}") | ||
| else: | ||
| os.makedirs(os.path.dirname(file_path), exist_ok=True) | ||
| # Generate content for all files, including Python, HTML, JSON, CSS, etc. | ||
| generated_content = generate_code_for_file(project_file, project_description, project_files) | ||
| # Write the generated content to the file if it doesn't already exist | ||
| if not os.path.exists(file_path): | ||
| with open(file_path, "w") as f: | ||
| f.write(generated_content) | ||
| print(f"š ScriptMonkey created file with generated content at: '{file_path}'.") | ||
| else: | ||
| print(f"File already exists, skipping: {file_path}") | ||
| def gather_project_context(project_description: str, project_files: list) -> str: | ||
| """ | ||
| Gathers a summary of the project goal and all existing files with their key functions or classes. | ||
| Args: | ||
| project_description (str): A high-level description of the project's purpose and goals. | ||
| project_files (list): List of project file descriptions. | ||
| Returns: | ||
| str: A summary of the project goal and existing modules, classes, and functions. | ||
| """ | ||
| context = f"Project Goal: {project_description}\n\n" | ||
| context += "Project Context:\n" | ||
| for file in project_files: | ||
| if file["functions"]: | ||
| context += f"- In '{file['path']}', the following functions are defined:\n" | ||
| for function in file["functions"]: | ||
| context += f" - {function['function_name']}: {function['description']} (Inputs: {function['inputs']}, Outputs: {function['outputs']})\n" | ||
| else: | ||
| context += f"- '{file['path']}' is defined with no specific functions listed.\n" | ||
| return context |
| fence_enums = [ | ||
| "cucumber", | ||
| "abap", | ||
| "ada", | ||
| "ahk", | ||
| "apacheconf", | ||
| "applescript", | ||
| "as", | ||
| "as3", | ||
| "asy", | ||
| "bash", | ||
| "bat", | ||
| "befunge", | ||
| "blitzmax", | ||
| "boo", | ||
| "brainfuck", | ||
| "c", | ||
| "cfm", | ||
| "cheetah", | ||
| "cl", | ||
| "clojure", | ||
| "cmake", | ||
| "coffeescript", | ||
| "console", | ||
| "control", | ||
| "cpp", | ||
| "csharp", | ||
| "css", | ||
| "cython", | ||
| "d", | ||
| "delphi", | ||
| "diff", | ||
| "dpatch", | ||
| "duel", | ||
| "dylan", | ||
| "erb", | ||
| "erl", | ||
| "erlang", | ||
| "evoque", | ||
| "factor", | ||
| "felix", | ||
| "fortran", | ||
| "gas", | ||
| "genshi", | ||
| "gitignore", | ||
| "glsl", | ||
| "gnuplot", | ||
| "go", | ||
| "groff", | ||
| "haml", | ||
| "haskell", | ||
| "html", | ||
| "hx", | ||
| "hybris", | ||
| "ini", | ||
| "io", | ||
| "ioke", | ||
| "irc", | ||
| "jade", | ||
| "java", | ||
| "js", | ||
| "jsp", | ||
| "lhs", | ||
| "llvm", | ||
| "logtalk", | ||
| "lua", | ||
| "make", | ||
| "mako", | ||
| "maql", | ||
| "mason", | ||
| "markdown", | ||
| "modelica", | ||
| "modula2", | ||
| "moocode", | ||
| "mupad", | ||
| "mxml", | ||
| "myghty", | ||
| "nasm", | ||
| "newspeak", | ||
| "objdump", | ||
| "objectivec", | ||
| "objectivej", | ||
| "ocaml", | ||
| "ooc", | ||
| "perl", | ||
| "php", | ||
| "postscript", | ||
| "pot", | ||
| "pov", | ||
| "prolog", | ||
| "properties", | ||
| "protobuf", | ||
| "py3tb", | ||
| "pytb", | ||
| "python", | ||
| "r", | ||
| "rb", | ||
| "rconsole", | ||
| "rebol", | ||
| "redcode", | ||
| "rhtml", | ||
| "rst", | ||
| "sass", | ||
| "scala", | ||
| "scaml", | ||
| "scheme", | ||
| "scss", | ||
| "smalltalk", | ||
| "smarty", | ||
| "sourceslist", | ||
| "splus", | ||
| "sql", | ||
| "sqlite3", | ||
| "squidconf", | ||
| "ssp", | ||
| "tcl", | ||
| "tcsh", | ||
| "tex", | ||
| "text", | ||
| "v", | ||
| "vala", | ||
| "vbnet", | ||
| "velocity", | ||
| "vim", | ||
| "xml", | ||
| "xquery", | ||
| "xslt", | ||
| "yaml", | ||
| ] |
| import os | ||
| import pyperclip | ||
| from rich.console import Console | ||
| from .tree import create_tree | ||
| console = Console() | ||
| def copy_files_to_clipboard(file_paths, include_tree=True): | ||
| """ | ||
| Reads the content from the specified files and copies it to the clipboard in the specified format. | ||
| Optionally includes a project directory tree. | ||
| """ | ||
| formatted_output = "- - - - - - - - - -\nHere are some details about the project.\n\n" | ||
| for path in file_paths: | ||
| try: | ||
| content = read_file(path) | ||
| formatted_output += f"# {path}\n{content}\n\n- - - - - - - - - -\n" | ||
| except FileNotFoundError: | ||
| console.print(f"[bold yellow]Warning: {path} not found. Skipping this file.[/bold yellow]") | ||
| except Exception as e: | ||
| console.print(f"[bold red]Error reading {path}: {e}[/bold red]") | ||
| # Include the directory tree if requested | ||
| if include_tree: | ||
| start_directory = os.getcwd() | ||
| tree = create_tree(start_directory) | ||
| formatted_output += "- - - - - - - - - -\n\n# PROJECT TREE\n" | ||
| formatted_output += f"{tree}\n\n" | ||
| # Copy the formatted output to the clipboard | ||
| pyperclip.copy(formatted_output) | ||
| console.print("[green]š Content has been copied to the clipboard.[/green]") | ||
| def read_file(path: str) -> str: | ||
| """Loads a file and returns the content. | ||
| Args: | ||
| path (str): Path to the file | ||
| Returns: | ||
| str: The content of the file | ||
| """ | ||
| with open(path, "r") as file: | ||
| return file.read() | ||
| def write_file(path: str, content: str) -> None: | ||
| """Writes string content to a file. | ||
| Args: | ||
| path (str): Path to the file. | ||
| content (str): Content to write to file. | ||
| """ | ||
| with open(path, "w") as file: | ||
| file.write(content) |
| import os | ||
| import sys | ||
| CONFIG_FILE = os.path.expanduser("~/.scriptmonkey_config") | ||
| # - - - - - API KEY MANAGEMENT - - - - - | ||
| def save_api_key(api_key: str): | ||
| """Save the OpenAI API key to the configuration file and environment variable.""" | ||
| with open(CONFIG_FILE, "w") as file: | ||
| file.write(api_key) | ||
| os.environ["OPENAI_API_KEY"] = api_key | ||
| print(f"ā OpenAI API key saved to {CONFIG_FILE}.") | ||
| def get_openai_api_key() -> str: | ||
| """Retrieve the OpenAI API key from environment or configuration file.""" | ||
| api_key = os.getenv("OPENAI_API_KEY") | ||
| if not api_key: | ||
| # Check for API key in the configuration file | ||
| if os.path.exists(CONFIG_FILE): | ||
| with open(CONFIG_FILE, "r") as file: | ||
| api_key = file.read().strip() | ||
| # Prompt user for API key if not found | ||
| if not api_key: | ||
| print("š ScriptMonkey requires an OpenAI API key to function.") | ||
| api_key = input("Please enter your OpenAI API key: ") | ||
| if api_key: | ||
| save_api_key(api_key) | ||
| if not api_key: | ||
| print("ā No API key provided. Exiting ScriptMonkey.") | ||
| sys.exit(1) | ||
| return api_key | ||
| def update_api_key(): | ||
| """Prompt the user to update the OpenAI API key.""" | ||
| api_key = input("Enter the new OpenAI API key: ") | ||
| if api_key: | ||
| save_api_key(api_key) | ||
| print("ā OpenAI API key updated successfully.") | ||
| else: | ||
| print("ā No API key provided. The API key was not updated.") | ||
| # - - - - - NEW FEATURES - - - - - |
| def remove_code_block_lines(input_string): | ||
| """Removes any lines from the input string that contain '```'.""" | ||
| lines = input_string.splitlines() | ||
| filtered_lines = [line for line in lines if "```" not in line] | ||
| return "\n".join(filtered_lines) |
| import os | ||
| from collections import defaultdict | ||
| def create_tree(start_path, prefix="", max_depth=None, current_depth=0, max_files_per_type=5): | ||
| """ | ||
| Generates a directory tree as a string with options to limit files of the same type, | ||
| ignore directories, and handle critical code files. | ||
| """ | ||
| # If max_depth is defined and the current depth exceeds it, stop recursion | ||
| if max_depth is not None and current_depth > max_depth: | ||
| return "" | ||
| tree = "" | ||
| files = sorted(os.listdir(start_path)) | ||
| # Group files by their extensions | ||
| files_by_extension = defaultdict(list) | ||
| for name in files: | ||
| if os.path.isdir(os.path.join(start_path, name)): | ||
| files_by_extension["<dir>"].append(name) | ||
| else: | ||
| _, ext = os.path.splitext(name) | ||
| files_by_extension[ext].append(name) | ||
| # Build a list of files to display | ||
| display_files = [] | ||
| # Add directories first | ||
| display_files.extend(sorted(files_by_extension["<dir>"])) | ||
| # Add files, limiting non-important file types | ||
| for ext, ext_files in files_by_extension.items(): | ||
| if ext == "<dir>": | ||
| continue | ||
| if ext in important_extensions: | ||
| # Include all files of important types | ||
| display_files.extend(sorted(ext_files)) | ||
| else: | ||
| # Limit the number of files to `max_files_per_type` for non-important types | ||
| display_files.extend(sorted(ext_files)[:max_files_per_type]) | ||
| if len(ext_files) > max_files_per_type: | ||
| display_files.append(f"... ({len(ext_files) - max_files_per_type} more {ext} files omitted)") | ||
| # Iterate over the files and directories to build the tree | ||
| for index, name in enumerate(display_files): | ||
| path = os.path.join(start_path, name) | ||
| # Skip ignored directories | ||
| if os.path.isdir(path) and name in ignored_dirs: | ||
| continue | ||
| connector = "āāā " if index == len(display_files) - 1 else "āāā " | ||
| tree += prefix + connector + name + "\n" | ||
| if os.path.isdir(path): | ||
| new_prefix = prefix + (" " if index == len(display_files) - 1 else "ā ") | ||
| tree += create_tree( | ||
| path, | ||
| new_prefix, | ||
| max_depth=max_depth, | ||
| current_depth=current_depth + 1, | ||
| max_files_per_type=max_files_per_type, | ||
| ) | ||
| return tree | ||
| ignored_dirs = { | ||
| "venv", | ||
| ".venv", | ||
| "dist", | ||
| "build", | ||
| "__pycache__", | ||
| "node_modules", | ||
| ".next", | ||
| "out", | ||
| ".nuxt", | ||
| "public", | ||
| "jspm_packages", | ||
| ".parcel-cache", | ||
| ".vercel", | ||
| "target", | ||
| ".gradle", | ||
| ".mvn", | ||
| "bin", | ||
| "obj", | ||
| "coverage", | ||
| "vendor", | ||
| "storage", | ||
| "cache", | ||
| ".git", | ||
| ".idea", | ||
| ".vscode", | ||
| ".DS_Store", | ||
| "logs", | ||
| "log", | ||
| "tmp", | ||
| "temp", | ||
| ".angular", | ||
| ".bundle", | ||
| "vendor/bundle", | ||
| "htmlcov", | ||
| ".mypy_cache", | ||
| ".pytest_cache", | ||
| } | ||
| # Define important file extensions | ||
| important_extensions = { | ||
| # General programming and scripting languages | ||
| ".py", | ||
| ".js", | ||
| ".ts", | ||
| ".tsx", | ||
| ".jsx", | ||
| ".java", | ||
| ".cpp", | ||
| ".c", | ||
| ".h", | ||
| ".hpp", | ||
| ".go", | ||
| ".rb", | ||
| ".rs", | ||
| ".php", | ||
| ".sh", | ||
| ".pl", | ||
| ".swift", | ||
| ".kt", | ||
| ".kts", | ||
| ".dart", | ||
| ".scala", | ||
| ".lua", | ||
| ".r", | ||
| ".jl", | ||
| ".cs", | ||
| ".csx", | ||
| ".m", | ||
| ".mm", | ||
| ".bat", | ||
| ".cmd", | ||
| } |
| import re | ||
| import os | ||
| import sys | ||
| import time | ||
| import itertools | ||
| import platform | ||
| import threading | ||
| import subprocess | ||
| import tempfile | ||
| from rich.syntax import Syntax | ||
| from rich.console import Console | ||
| from rich.markdown import Markdown | ||
| console = Console() | ||
| class Spinner: | ||
| def __init__(self, message="Processing"): | ||
| self.spinner = itertools.cycle(["ā ", "ā ", "ā ¹", "ā ø", "ā ¼", "ā “", "ā ¦", "ā §", "ā ", "ā "]) | ||
| self.stop_running = threading.Event() | ||
| self.spin_thread = None | ||
| self.message = message | ||
| def spin(self): | ||
| while not self.stop_running.is_set(): | ||
| sys.stdout.write(f"\r{self.message} {next(self.spinner)}") | ||
| sys.stdout.flush() | ||
| time.sleep(0.1) | ||
| sys.stdout.write("\r" + " " * (len(self.message) + 2) + "\r") | ||
| sys.stdout.flush() | ||
| def __enter__(self): | ||
| self.spin_thread = threading.Thread(target=self.spin) | ||
| self.spin_thread.start() | ||
| def __exit__(self, exc_type, exc_val, exc_tb): | ||
| self.stop_running.set() | ||
| self.spin_thread.join() | ||
| def cli_text_editor(mode: str) -> str: | ||
| """ | ||
| Opens the user's default text editor for entering multi-line input. | ||
| The user is provided with instructions within the temporary file, | ||
| adjusted based on the detected editor. | ||
| :mode: enums['BUILD', 'ASK'] | ||
| """ | ||
| if mode == "BUILD": | ||
| purpose = "Project Builder" | ||
| user_prompt = "Please describe your project in detail below." | ||
| elif mode == "ASK": | ||
| purpose = "Prompt Editor" | ||
| user_prompt = "Please write your question down below." | ||
| with tempfile.NamedTemporaryFile(suffix=".txt") as temp_file: | ||
| # Detect the editor from the environment or default based on the OS | ||
| editor = os.environ.get("EDITOR") | ||
| # If no editor is set, choose a default based on the platform | ||
| if not editor: | ||
| if platform.system() == "Windows": | ||
| editor = "notepad" | ||
| else: | ||
| editor = "nano" # Default for Unix-like systems | ||
| # Adjust instructions based on the detected editor | ||
| if "vim" in editor.lower(): | ||
| instructions = ( | ||
| f"!# š Welcome to ScriptMonkey's {purpose}!\n" | ||
| f"!# {user_prompt}\n" | ||
| "!# Use 'i' to start editing, and when you're done, press 'Esc',\n" | ||
| "!# type ':wq' to save and exit.\n" | ||
| "!# (Lines starting with '!#' will be ignored.)\n\n" | ||
| ) | ||
| elif "nano" in editor.lower(): | ||
| instructions = ( | ||
| f"!# š Welcome to ScriptMonkey's {purpose}!\n" | ||
| f"!# {user_prompt}\n" | ||
| "!# When you're done, press 'Ctrl+O' to save and 'Ctrl+X' to exit.\n" | ||
| "!# (Lines starting with '!#' will be ignored.)\n\n" | ||
| ) | ||
| elif "notepad" in editor.lower(): | ||
| instructions = ( | ||
| f"!# š Welcome to ScriptMonkey's {purpose}!\n" | ||
| f"!# {user_prompt}\n" | ||
| "!# When you're done, save and close the Notepad window.\n" | ||
| "!# (Lines starting with '!#' will be ignored.)\n\n" | ||
| ) | ||
| elif "code" in editor.lower(): | ||
| instructions = ( | ||
| f"!# š Welcome to ScriptMonkey's {purpose}!\n" | ||
| f"!# {user_prompt}\n" | ||
| "!# When you're done, save the file and close the editor window.\n" | ||
| "!# (Lines starting with '!#' will be ignored.)\n\n" | ||
| ) | ||
| else: | ||
| instructions = ( | ||
| f"!# š Welcome to ScriptMonkey's {purpose}!\n" | ||
| f"!# {user_prompt}\n" | ||
| "!# Save and close the editor when you're done.\n" | ||
| "!# (Lines starting with '!#' will be ignored.)\n\n" | ||
| ) | ||
| # Write the instructions to the temporary file | ||
| temp_file.write(instructions.encode("utf-8")) | ||
| temp_file.flush() | ||
| # Open the temporary file in the detected editor | ||
| subprocess.call([editor, temp_file.name]) | ||
| # Read the user's input, ignoring lines starting with '!#' | ||
| temp_file.seek(0) | ||
| user_input = temp_file.read().decode("utf-8") | ||
| user_input = "\n".join(line for line in user_input.splitlines() if not line.startswith("!#")) | ||
| return user_input.strip() | ||
| def render_response_with_syntax_highlighting(response): | ||
| """ | ||
| Render a ChatGPT response with syntax highlighting for detected code blocks. | ||
| """ | ||
| # Regular expression to detect code blocks with a specified language (e.g., ```python) | ||
| code_block_pattern = re.compile(r"```(\w+)?\n(.*?)```", re.DOTALL) | ||
| last_pos = 0 | ||
| # Iterate over all detected code blocks | ||
| for match in code_block_pattern.finditer(response): | ||
| language = match.group(1) or "text" # Default to 'text' if no language is specified | ||
| code_content = match.group(2) | ||
| # Print any text before the code block as markdown | ||
| if match.start() > last_pos: | ||
| pre_text = response[last_pos : match.start()] | ||
| console.print(Markdown(pre_text)) | ||
| # Print the code block with syntax highlighting | ||
| syntax = Syntax(f"\n{code_content}", language, theme="monokai", line_numbers=False) | ||
| console.print("\n") | ||
| console.print(syntax) | ||
| console.print("\n") | ||
| last_pos = match.end() | ||
| # Print any remaining text after the last code block | ||
| if last_pos < len(response): | ||
| console.print(Markdown(response[last_pos:])) |
+2
-2
| Metadata-Version: 2.1 | ||
| Name: scriptmonkey | ||
| Version: 1.4.2 | ||
| Version: 1.4.4 | ||
| Summary: A Python package that generates complex software projects and fixes errors in your code using OpenAI's GPT API. | ||
@@ -9,3 +9,3 @@ Home-page: https://github.com/lukerbs/ScriptMonkey | ||
| License: MIT | ||
| Keywords: openai,GPT,AI,project generation,multi-file project,complex project generator,code error fixing,code automation,GPT API,error correction,code assistant,AI coding tools,script monkey,automation,development tools,python tools,code analysis,machine learning,project bootstrap,custom code generation,python package | ||
| Keywords: openai,GPT,AI,project generation,multi-file project,complex project generator,code error fixing,code automation,GPT API,error correction,code assistant,AI coding tools,script monkey,automation,development tools,python tools,code analysis,machine learning,project bootstrap,custom code generation,python package,cursor,devin,copilot,automatic code correction | ||
| Classifier: Programming Language :: Python :: 3 | ||
@@ -12,0 +12,0 @@ Classifier: License :: OSI Approved :: MIT License |
| Metadata-Version: 2.1 | ||
| Name: scriptmonkey | ||
| Version: 1.4.2 | ||
| Version: 1.4.4 | ||
| Summary: A Python package that generates complex software projects and fixes errors in your code using OpenAI's GPT API. | ||
@@ -9,3 +9,3 @@ Home-page: https://github.com/lukerbs/ScriptMonkey | ||
| License: MIT | ||
| Keywords: openai,GPT,AI,project generation,multi-file project,complex project generator,code error fixing,code automation,GPT API,error correction,code assistant,AI coding tools,script monkey,automation,development tools,python tools,code analysis,machine learning,project bootstrap,custom code generation,python package | ||
| Keywords: openai,GPT,AI,project generation,multi-file project,complex project generator,code error fixing,code automation,GPT API,error correction,code assistant,AI coding tools,script monkey,automation,development tools,python tools,code analysis,machine learning,project bootstrap,custom code generation,python package,cursor,devin,copilot,automatic code correction | ||
| Classifier: Programming Language :: Python :: 3 | ||
@@ -12,0 +12,0 @@ Classifier: License :: OSI Approved :: MIT License |
@@ -5,4 +5,5 @@ README.md | ||
| scriptmonkey/__main__.py | ||
| scriptmonkey/agents.py | ||
| scriptmonkey/core.py | ||
| scriptmonkey/file_handler.py | ||
| scriptmonkey/scripting.py | ||
| scriptmonkey.egg-info/PKG-INFO | ||
@@ -19,2 +20,9 @@ scriptmonkey.egg-info/SOURCES.txt | ||
| scriptmonkey/openai_client/prompts/fix_error.txt | ||
| scriptmonkey/openai_client/prompts/project_description.txt | ||
| scriptmonkey/openai_client/prompts/project_description.txt | ||
| scriptmonkey/utils/__init__.py | ||
| scriptmonkey/utils/fence.py | ||
| scriptmonkey/utils/file_handler.py | ||
| scriptmonkey/utils/key_manager.py | ||
| scriptmonkey/utils/parsers.py | ||
| scriptmonkey/utils/tree.py | ||
| scriptmonkey/utils/ui.py |
+24
-517
@@ -0,33 +1,28 @@ | ||
| import os | ||
| import sys | ||
| import time | ||
| import argparse | ||
| import platform | ||
| import traceback | ||
| from collections import defaultdict | ||
| import argparse | ||
| from pprint import pprint | ||
| from rich.console import Console | ||
| from .utils.ui import Spinner, cli_text_editor | ||
| from .utils.key_manager import update_api_key | ||
| from .utils.file_handler import read_file, write_file, copy_files_to_clipboard | ||
| from .agents import ( | ||
| ask_gpt_with_files, | ||
| generate_project_structure, | ||
| build_project, | ||
| generate_readme, | ||
| ) | ||
| from .openai_client.basemodels import ScriptMonkeyResponse | ||
| from .openai_client import ( | ||
| chatgpt_json, | ||
| chatgpt, | ||
| ScriptMonkeyResponse, | ||
| ProjectStructureResponse, | ||
| ProjectFile, | ||
| default_prompts, | ||
| ) | ||
| from .openai_client.prompting import load_prompt | ||
| from .file_handler import read_file, write_file, ignored_dirs, important_extensions | ||
| import platform | ||
| import tempfile | ||
| import subprocess | ||
| import threading | ||
| import itertools | ||
| import time | ||
| from pprint import pprint | ||
| import os | ||
| import platform | ||
| import pyperclip | ||
| import re | ||
| from rich.console import Console | ||
| from rich.markdown import Markdown | ||
| from rich.syntax import Syntax | ||
| console = Console() | ||
| CONFIG_FILE = os.path.expanduser("~/.scriptmonkey_config") | ||
@@ -42,27 +37,3 @@ | ||
| class Spinner: | ||
| def __init__(self, message="Processing"): | ||
| self.spinner = itertools.cycle(["ā ", "ā ", "ā ¹", "ā ø", "ā ¼", "ā “", "ā ¦", "ā §", "ā ", "ā "]) | ||
| self.stop_running = threading.Event() | ||
| self.spin_thread = None | ||
| self.message = message | ||
| def spin(self): | ||
| while not self.stop_running.is_set(): | ||
| sys.stdout.write(f"\r{self.message} {next(self.spinner)}") | ||
| sys.stdout.flush() | ||
| time.sleep(0.1) | ||
| sys.stdout.write("\r" + " " * (len(self.message) + 2) + "\r") | ||
| sys.stdout.flush() | ||
| def __enter__(self): | ||
| self.spin_thread = threading.Thread(target=self.spin) | ||
| self.spin_thread.start() | ||
| def __exit__(self, exc_type, exc_val, exc_tb): | ||
| self.stop_running.set() | ||
| self.spin_thread.join() | ||
| def codemonkey_exception_handler(exc_type, exc_value, exc_traceback): | ||
| def scriptmonkey_exception_handler(exc_type, exc_value, exc_traceback): | ||
| if issubclass(exc_type, KeyboardInterrupt): | ||
@@ -98,469 +69,5 @@ sys.__excepthook__(exc_type, exc_value, exc_traceback) | ||
| def run(): | ||
| sys.excepthook = codemonkey_exception_handler | ||
| sys.excepthook = scriptmonkey_exception_handler | ||
| # - - - - - API KEY MANAGEMENT - - - - - | ||
| def save_api_key(api_key: str): | ||
| """Save the OpenAI API key to the configuration file and environment variable.""" | ||
| with open(CONFIG_FILE, "w") as file: | ||
| file.write(api_key) | ||
| os.environ["OPENAI_API_KEY"] = api_key | ||
| print(f"ā OpenAI API key saved to {CONFIG_FILE}.") | ||
| def get_openai_api_key() -> str: | ||
| """Retrieve the OpenAI API key from environment or configuration file.""" | ||
| api_key = os.getenv("OPENAI_API_KEY") | ||
| if not api_key: | ||
| # Check for API key in the configuration file | ||
| if os.path.exists(CONFIG_FILE): | ||
| with open(CONFIG_FILE, "r") as file: | ||
| api_key = file.read().strip() | ||
| # Prompt user for API key if not found | ||
| if not api_key: | ||
| print("š ScriptMonkey requires an OpenAI API key to function.") | ||
| api_key = input("Please enter your OpenAI API key: ") | ||
| if api_key: | ||
| save_api_key(api_key) | ||
| if not api_key: | ||
| print("ā No API key provided. Exiting ScriptMonkey.") | ||
| sys.exit(1) | ||
| return api_key | ||
| def update_api_key(): | ||
| """Prompt the user to update the OpenAI API key.""" | ||
| api_key = input("Enter the new OpenAI API key: ") | ||
| if api_key: | ||
| save_api_key(api_key) | ||
| print("ā OpenAI API key updated successfully.") | ||
| else: | ||
| print("ā No API key provided. The API key was not updated.") | ||
| # - - - - - NEW FEATURES - - - - - | ||
| def get_multiline_input_with_editor(mode: str) -> str: | ||
| """ | ||
| Opens the user's default text editor for entering multi-line input. | ||
| The user is provided with instructions within the temporary file, | ||
| adjusted based on the detected editor. | ||
| :mode: enums['BUILD', 'ASK'] | ||
| """ | ||
| if mode == "BUILD": | ||
| purpose = "Project Builder" | ||
| user_prompt = "Please describe your project in detail below." | ||
| elif mode == "ASK": | ||
| purpose = "Prompt Editor" | ||
| user_prompt = "Please write your question down below." | ||
| with tempfile.NamedTemporaryFile(suffix=".txt") as temp_file: | ||
| # Detect the editor from the environment or default based on the OS | ||
| editor = os.environ.get("EDITOR") | ||
| # If no editor is set, choose a default based on the platform | ||
| if not editor: | ||
| if platform.system() == "Windows": | ||
| editor = "notepad" | ||
| else: | ||
| editor = "nano" # Default for Unix-like systems | ||
| # Adjust instructions based on the detected editor | ||
| if "vim" in editor.lower(): | ||
| instructions = ( | ||
| f"!# š Welcome to ScriptMonkey's {purpose}!\n" | ||
| f"!# {user_prompt}\n" | ||
| "!# Use 'i' to start editing, and when you're done, press 'Esc',\n" | ||
| "!# type ':wq' to save and exit.\n" | ||
| "!# (Lines starting with '!#' will be ignored.)\n\n" | ||
| ) | ||
| elif "nano" in editor.lower(): | ||
| instructions = ( | ||
| f"!# š Welcome to ScriptMonkey's {purpose}!\n" | ||
| f"!# {user_prompt}\n" | ||
| "!# When you're done, press 'Ctrl+O' to save and 'Ctrl+X' to exit.\n" | ||
| "!# (Lines starting with '!#' will be ignored.)\n\n" | ||
| ) | ||
| elif "notepad" in editor.lower(): | ||
| instructions = ( | ||
| f"!# š Welcome to ScriptMonkey's {purpose}!\n" | ||
| f"!# {user_prompt}\n" | ||
| "!# When you're done, save and close the Notepad window.\n" | ||
| "!# (Lines starting with '!#' will be ignored.)\n\n" | ||
| ) | ||
| elif "code" in editor.lower(): | ||
| instructions = ( | ||
| f"!# š Welcome to ScriptMonkey's {purpose}!\n" | ||
| f"!# {user_prompt}\n" | ||
| "!# When you're done, save the file and close the editor window.\n" | ||
| "!# (Lines starting with '!#' will be ignored.)\n\n" | ||
| ) | ||
| else: | ||
| instructions = ( | ||
| f"!# š Welcome to ScriptMonkey's {purpose}!\n" | ||
| f"!# {user_prompt}\n" | ||
| "!# Save and close the editor when you're done.\n" | ||
| "!# (Lines starting with '!#' will be ignored.)\n\n" | ||
| ) | ||
| # Write the instructions to the temporary file | ||
| temp_file.write(instructions.encode("utf-8")) | ||
| temp_file.flush() | ||
| # Open the temporary file in the detected editor | ||
| subprocess.call([editor, temp_file.name]) | ||
| # Read the user's input, ignoring lines starting with '!#' | ||
| temp_file.seek(0) | ||
| user_input = temp_file.read().decode("utf-8") | ||
| user_input = "\n".join(line for line in user_input.splitlines() if not line.startswith("!#")) | ||
| return user_input.strip() | ||
| def generate_project_structure(description: str) -> ProjectStructureResponse: | ||
| """Generates the project structure based on the user's project description using OpenAI.""" | ||
| instructions = ( | ||
| "Generate a detailed project structure for a multi-level application. The project will be placed directly inside a folder named 'generated_project'." | ||
| "\n- Do NOT include 'generated_project/' as part of the paths. All paths should be relative to the root of the project directory, meaning they should start directly with the file or folder names as if they are inside 'generated_project'." | ||
| "\n- Provide a list of directories and files with their full relative paths." | ||
| "\n- Each directory should end with a '/' to indicate that it is a folder." | ||
| "\n- For each file or directory, include a 'description' that explains its purpose." | ||
| "\n- If the file is a Python code file, also include a 'functions' list. For each function, include:" | ||
| "\n - 'function_name': The name of the function." | ||
| "\n - 'description': A description of what the function does." | ||
| "\n - 'inputs': A list of the function's expected inputs, including data types." | ||
| "\n - 'outputs': A list of the function's expected outputs, including data types." | ||
| "\n- Do not include any extra explanations, commentary, or introductory text. Only provide the structured data as requested." | ||
| ) | ||
| # Call the chatgpt_json function to get structured project plan | ||
| project_structure = chatgpt_json( | ||
| instructions=instructions, content=description, response_format=ProjectStructureResponse | ||
| ) | ||
| return project_structure | ||
| def create_project_structure( | ||
| project_structure_response: dict, project_description: str, base_directory: str = "./generated_project" | ||
| ): | ||
| """Creates the directories and files for the project and generates code content for all file types.""" | ||
| # Extract the list of project files for context | ||
| project_files = project_structure_response["files"] | ||
| # Iterate through each file in the project structure | ||
| for project_file in project_files: | ||
| file_path = os.path.join(base_directory, project_file["path"].lstrip("/")) | ||
| # Check if it's a directory or file (directories end with '/') | ||
| if file_path.endswith("/"): | ||
| os.makedirs(file_path, exist_ok=True) | ||
| print(f"š ScriptMonkey created directory: {file_path}") | ||
| else: | ||
| os.makedirs(os.path.dirname(file_path), exist_ok=True) | ||
| # Generate content for all files, including Python, HTML, JSON, CSS, etc. | ||
| generated_content = generate_code_for_file(project_file, project_description, project_files) | ||
| # Write the generated content to the file if it doesn't already exist | ||
| if not os.path.exists(file_path): | ||
| with open(file_path, "w") as f: | ||
| f.write(generated_content) | ||
| print(f"š ScriptMonkey created file with generated content at: '{file_path}'.") | ||
| else: | ||
| print(f"File already exists, skipping: {file_path}") | ||
| def gather_project_context(project_description: str, project_files: list) -> str: | ||
| """ | ||
| Gathers a summary of the project goal and all existing files with their key functions or classes. | ||
| Args: | ||
| project_description (str): A high-level description of the project's purpose and goals. | ||
| project_files (list): List of project file descriptions. | ||
| Returns: | ||
| str: A summary of the project goal and existing modules, classes, and functions. | ||
| """ | ||
| context = f"Project Goal: {project_description}\n\n" | ||
| context += "Project Context:\n" | ||
| for file in project_files: | ||
| if file["functions"]: | ||
| context += f"- In '{file['path']}', the following functions are defined:\n" | ||
| for function in file["functions"]: | ||
| context += f" - {function['function_name']}: {function['description']} (Inputs: {function['inputs']}, Outputs: {function['outputs']})\n" | ||
| else: | ||
| context += f"- '{file['path']}' is defined with no specific functions listed.\n" | ||
| return context | ||
| def remove_code_block_lines(input_string): | ||
| """Removes any lines from the input string that contain '```'.""" | ||
| lines = input_string.splitlines() | ||
| filtered_lines = [line for line in lines if "```" not in line] | ||
| return "\n".join(filtered_lines) | ||
| def generate_code_for_file(file_description: dict, project_description: str, project_files: list) -> str: | ||
| """ | ||
| Generates content for a given file based on its description using the chatgpt() function. | ||
| Args: | ||
| file_description (dict): The description of the file for which content is being generated. | ||
| project_description (str): A high-level description of the project's purpose and goals. | ||
| project_files (list): List of all project files for context. | ||
| Returns: | ||
| str: The generated content for the file. | ||
| """ | ||
| # Gather context about the project goal and other files | ||
| context = gather_project_context(project_description, project_files) | ||
| # Extract the file extension to inform the content type | ||
| file_extension = os.path.splitext(file_description["path"])[1].lower().strip(".") | ||
| # Dynamically adjust the content type description based on the file extension | ||
| if file_extension: | ||
| content_type_description = f"{file_extension.upper()} file content" | ||
| else: | ||
| content_type_description = "text content" | ||
| # Prepare instructions for OpenAI to generate content based on the file description and type | ||
| instructions = ( | ||
| f"Write the complete content for a {content_type_description} that fulfills the following requirements. " | ||
| "Consider the context of the entire project when generating the content and make use of imports where available and appropriate." | ||
| "Use relevant imports, references, and appropriate formatting or structure where necessary. Do not add extra commentary or explanation. " | ||
| "Make sure to return the content directly, without wrapping it in any code fences like triple quotes or backticks ." | ||
| "i.e. DO NOT include any triple backtrick wrappers at all for any code, (e.g. ```python<content here>```) just return the code as plain text." | ||
| f"\n\nFile Description: {file_description['description']}" | ||
| f"\n\n{context}\n" | ||
| ) | ||
| # Include functions for code files (if provided) | ||
| if file_description.get("functions"): | ||
| instructions += "\n\nFunctions:\n" | ||
| for function in file_description["functions"]: | ||
| instructions += ( | ||
| f"- {function['function_name']}: {function['description']} " | ||
| f"(Inputs: {function['inputs']}, Outputs: {function['outputs']})\n" | ||
| ) | ||
| # Call the chatgpt function to generate the content | ||
| generated_content = chatgpt(prompt=instructions) | ||
| # Clean up any unintended code blocks | ||
| generated_content = remove_code_block_lines(generated_content) | ||
| return generated_content | ||
| def generate_readme(description: str, project_structure: dict) -> str: | ||
| """Generates a README.md content based on the project description and structure.""" | ||
| instructions = ( | ||
| "Write a complete README.md file based on the following project details. " | ||
| "The README should include the project overview, installation instructions, usage guide, file structure summary, key features, and configuration details. " | ||
| "Make sure the README is well-structured and formatted using Markdown without wrapping the entire README in backticks or any other non-readme commentary." | ||
| "Do not include any commentary, explanations, or text outside of the README content." | ||
| f"\n\nProject Description: {description}\n" | ||
| f"\nProject Structure: {project_structure}\n" | ||
| ) | ||
| readme_content = chatgpt(prompt=instructions) | ||
| readme_content = readme_content.strip("```markdown").strip("```") | ||
| return readme_content | ||
| def generate_directory_tree(start_path, prefix="", max_depth=None, current_depth=0, max_files_per_type=5): | ||
| """ | ||
| Generates a directory tree as a string with options to limit files of the same type, | ||
| ignore directories, and handle critical code files. | ||
| """ | ||
| # If max_depth is defined and the current depth exceeds it, stop recursion | ||
| if max_depth is not None and current_depth > max_depth: | ||
| return "" | ||
| tree = "" | ||
| files = sorted(os.listdir(start_path)) | ||
| # Group files by their extensions | ||
| files_by_extension = defaultdict(list) | ||
| for name in files: | ||
| if os.path.isdir(os.path.join(start_path, name)): | ||
| files_by_extension["<dir>"].append(name) | ||
| else: | ||
| _, ext = os.path.splitext(name) | ||
| files_by_extension[ext].append(name) | ||
| # Build a list of files to display | ||
| display_files = [] | ||
| # Add directories first | ||
| display_files.extend(sorted(files_by_extension["<dir>"])) | ||
| # Add files, limiting non-important file types | ||
| for ext, ext_files in files_by_extension.items(): | ||
| if ext == "<dir>": | ||
| continue | ||
| if ext in important_extensions: | ||
| # Include all files of important types | ||
| display_files.extend(sorted(ext_files)) | ||
| else: | ||
| # Limit the number of files to `max_files_per_type` for non-important types | ||
| display_files.extend(sorted(ext_files)[:max_files_per_type]) | ||
| if len(ext_files) > max_files_per_type: | ||
| display_files.append(f"... ({len(ext_files) - max_files_per_type} more {ext} files omitted)") | ||
| # Iterate over the files and directories to build the tree | ||
| for index, name in enumerate(display_files): | ||
| path = os.path.join(start_path, name) | ||
| # Skip ignored directories | ||
| if os.path.isdir(path) and name in ignored_dirs: | ||
| continue | ||
| connector = "āāā " if index == len(display_files) - 1 else "āāā " | ||
| tree += prefix + connector + name + "\n" | ||
| if os.path.isdir(path): | ||
| new_prefix = prefix + (" " if index == len(display_files) - 1 else "ā ") | ||
| tree += generate_directory_tree( | ||
| path, | ||
| new_prefix, | ||
| max_depth=max_depth, | ||
| current_depth=current_depth + 1, | ||
| max_files_per_type=max_files_per_type, | ||
| ) | ||
| return tree | ||
| def ask_gpt_with_files(question, file_paths, include_tree=False): | ||
| """ | ||
| Constructs a detailed and flexible prompt for ChatGPT using a question and optionally including content from specified files. | ||
| """ | ||
| prompt = ( | ||
| f"### Question:\n" | ||
| f"{question}\n\n" | ||
| "If I have included any files below, you can use them for additional context for this question. " | ||
| "Please analyze the provided files below (if available) as needed and reference them when forming your answer. " | ||
| "If the answer involves code, please format any code examples using Markdown with properly labeled language-specific code blocks. " | ||
| "Your response should be in Markdown format to preserve readability.\n\n" | ||
| ) | ||
| if file_paths: | ||
| prompt += "### Files Provided:\n" | ||
| for path in file_paths: | ||
| try: | ||
| content = read_file(path) | ||
| prompt += ( | ||
| f"## File: {path}\n" | ||
| f"The content of the file '{path}' is included below. Use this as context for answering the question:\n\n" | ||
| f"```\n{content}\n```\n\n" | ||
| ) | ||
| except FileNotFoundError: | ||
| console.print(f"[bold yellow]Warning: {path} not found. Skipping this file.[/bold yellow]") | ||
| except Exception as e: | ||
| console.print(f"[bold red]Error reading {path}: {e}[/bold red]") | ||
| else: | ||
| prompt += ( | ||
| "No specific files have been provided, so please base your response solely on the question above. " | ||
| "If the response includes any code examples or technical explanations, please use Markdown formatting with language-specific code blocks for clarity.\n" | ||
| ) | ||
| # Include the directory tree if the flag is set | ||
| if include_tree: | ||
| start_directory = os.getcwd() | ||
| tree = generate_directory_tree(start_directory) | ||
| prompt += "### Directory Tree:\n" | ||
| prompt += f"The directory tree of the current working directory is included below (up to a depth of 6 levels):\n\n```\n{tree}\n```\n\n" | ||
| console.print("- - Directory Tree - -") | ||
| console.print(tree) | ||
| # Output the constructed prompt to the console for transparency | ||
| console.rule("š ScriptMonkey is Thinking š") | ||
| console.rule() | ||
| # Use the OpenAI API to get a response | ||
| try: | ||
| response = chatgpt(prompt=prompt) | ||
| # Display the response using rich markdown and detect code blocks | ||
| console.rule("š ANSWER š") | ||
| render_response_with_syntax_highlighting(response) | ||
| console.print("\n") | ||
| console.rule() | ||
| except Exception as e: | ||
| console.print(f"[bold red]Error using OpenAI API: {e}[/bold red]") | ||
| def render_response_with_syntax_highlighting(response): | ||
| """ | ||
| Render a ChatGPT response with syntax highlighting for detected code blocks. | ||
| """ | ||
| # Regular expression to detect code blocks with a specified language (e.g., ```python) | ||
| code_block_pattern = re.compile(r"```(\w+)?\n(.*?)```", re.DOTALL) | ||
| last_pos = 0 | ||
| # Iterate over all detected code blocks | ||
| for match in code_block_pattern.finditer(response): | ||
| language = match.group(1) or "text" # Default to 'text' if no language is specified | ||
| code_content = match.group(2) | ||
| # Print any text before the code block as markdown | ||
| if match.start() > last_pos: | ||
| pre_text = response[last_pos : match.start()] | ||
| console.print(Markdown(pre_text)) | ||
| # Print the code block with syntax highlighting | ||
| syntax = Syntax(f"\n{code_content}", language, theme="monokai", line_numbers=False) | ||
| console.print("\n") | ||
| console.print(syntax) | ||
| console.print("\n") | ||
| last_pos = match.end() | ||
| # Print any remaining text after the last code block | ||
| if last_pos < len(response): | ||
| console.print(Markdown(response[last_pos:])) | ||
| def copy_files_to_clipboard(file_paths, include_tree=True): | ||
| """ | ||
| Reads the content from the specified files and copies it to the clipboard in the specified format. | ||
| Optionally includes a project directory tree. | ||
| """ | ||
| formatted_output = "- - - - - - - - - -\nHere are some details about the project.\n\n" | ||
| for path in file_paths: | ||
| try: | ||
| content = read_file(path) | ||
| formatted_output += f"# {path}\n{content}\n\n- - - - - - - - - -\n" | ||
| except FileNotFoundError: | ||
| console.print(f"[bold yellow]Warning: {path} not found. Skipping this file.[/bold yellow]") | ||
| except Exception as e: | ||
| console.print(f"[bold red]Error reading {path}: {e}[/bold red]") | ||
| # Include the directory tree if requested | ||
| if include_tree: | ||
| start_directory = os.getcwd() | ||
| tree = generate_directory_tree(start_directory) | ||
| formatted_output += "- - - - - - - - - -\n\n# PROJECT TREE\n" | ||
| formatted_output += f"{tree}\n\n" | ||
| # Copy the formatted output to the clipboard | ||
| pyperclip.copy(formatted_output) | ||
| console.print("[green]š Content has been copied to the clipboard.[/green]") | ||
| def handle_no_prompt(): | ||
@@ -603,3 +110,3 @@ print(f"\nNo Prompt Provided (Tip: Did you save before closing the editor?).\nš Quitting ScriptMonkey...\n") | ||
| if args.ask is True: | ||
| question = get_multiline_input_with_editor(mode="ASK") | ||
| question = cli_text_editor(mode="ASK") | ||
| if not question: | ||
@@ -620,3 +127,3 @@ handle_no_prompt() | ||
| # Step 1: Get multi-line project description from user | ||
| project_description = get_multiline_input_with_editor(mode="BUILD") | ||
| project_description = cli_text_editor(mode="BUILD") | ||
| if not project_description: | ||
@@ -634,3 +141,3 @@ handle_no_prompt() | ||
| print(f"\nš ScriptMonkey is coding...") | ||
| create_project_structure(project_structure_response=project_structure, project_description=project_description) | ||
| build_project(project_structure_response=project_structure, project_description=project_description) | ||
| print("\nProject structure creation complete.") | ||
@@ -637,0 +144,0 @@ |
| from .prompting import DefaultPrompts | ||
| from .client import chatgpt_json, chatgpt | ||
| from pydantic import BaseModel | ||
| from typing import List, Optional | ||
| class FunctionDetails(BaseModel): | ||
| function_name: str | ||
| description: str | ||
| inputs: Optional[List[str]] # List of inputs with data types | ||
| outputs: Optional[List[str]] # List of outputs with data types | ||
| class ProjectFile(BaseModel): | ||
| path: str # The full path to the file or directory | ||
| description: str # High-level purpose of the directory or file | ||
| functions: Optional[List[FunctionDetails]] # List of functions/classes if it's a code file | ||
| class ProjectStructureResponse(BaseModel): | ||
| files: List[ProjectFile] # List of all files and directories in the project | ||
| class ScriptMonkeyResponse(BaseModel): | ||
| problem: str # A description of the error/problem | ||
| solution: str # The solution to the problem | ||
| corrected_code: str # The corrected version of the Python code | ||
| default_prompts = DefaultPrompts() |
| from pydantic import BaseModel | ||
| from typing import List, Optional | ||
| class CodemonkeyResponse(BaseModel): | ||
| class FunctionDetails(BaseModel): | ||
| function_name: str | ||
| description: str | ||
| inputs: Optional[List[str]] # List of inputs with data types | ||
| outputs: Optional[List[str]] # List of outputs with data types | ||
| class ProjectFile(BaseModel): | ||
| path: str # The full path to the file or directory | ||
| description: str # High-level purpose of the directory or file | ||
| functions: Optional[List[FunctionDetails]] # List of functions/classes if it's a code file | ||
| class ProjectStructureResponse(BaseModel): | ||
| files: List[ProjectFile] # List of all files and directories in the project | ||
| class ScriptMonkeyResponse(BaseModel): | ||
| problem: str # A description of the error/problem | ||
| solution: str # The solution to the problem | ||
| corrected_code: str # The corrected version of the Python code |
+5
-1
@@ -5,3 +5,3 @@ from setuptools import setup, find_packages | ||
| name="scriptmonkey", | ||
| version="1.4.2", | ||
| version="1.4.4", | ||
| description="A Python package that generates complex software projects and fixes errors in your code using OpenAI's GPT API.", | ||
@@ -49,2 +49,6 @@ long_description=open("README.md", "r").read(), | ||
| "python package", | ||
| "cursor", | ||
| "devin", | ||
| "copilot", | ||
| "automatic code correction", | ||
| ], | ||
@@ -51,0 +55,0 @@ package_data={"scriptmonkey.openai_client": ["prompts/*.txt"]}, |
| def read_file(path: str) -> str: | ||
| """Loads a file and returns the content. | ||
| Args: | ||
| path (str): Path to the file | ||
| Returns: | ||
| str: The content of the file | ||
| """ | ||
| with open(path, "r") as file: | ||
| return file.read() | ||
| def write_file(path: str, content: str) -> None: | ||
| """Writes string content to a file. | ||
| Args: | ||
| path (str): Path to the file. | ||
| content (str): Content to write to file. | ||
| """ | ||
| with open(path, "w") as file: | ||
| file.write(content) | ||
| ignored_dirs = { | ||
| "venv", | ||
| ".venv", | ||
| "dist", | ||
| "build", | ||
| "__pycache__", | ||
| "node_modules", | ||
| ".next", | ||
| "out", | ||
| ".nuxt", | ||
| "public", | ||
| "jspm_packages", | ||
| ".parcel-cache", | ||
| ".vercel", | ||
| "target", | ||
| ".gradle", | ||
| ".mvn", | ||
| "bin", | ||
| "obj", | ||
| "coverage", | ||
| "vendor", | ||
| "storage", | ||
| "cache", | ||
| ".git", | ||
| ".idea", | ||
| ".vscode", | ||
| ".DS_Store", | ||
| "logs", | ||
| "log", | ||
| "tmp", | ||
| "temp", | ||
| ".angular", | ||
| ".bundle", | ||
| "vendor/bundle", | ||
| "htmlcov", | ||
| ".mypy_cache", | ||
| ".pytest_cache", | ||
| } | ||
| # Define important file extensions | ||
| important_extensions = { | ||
| # General programming and scripting languages | ||
| ".py", | ||
| ".js", | ||
| ".ts", | ||
| ".tsx", | ||
| ".jsx", | ||
| ".java", | ||
| ".cpp", | ||
| ".c", | ||
| ".h", | ||
| ".hpp", | ||
| ".go", | ||
| ".rb", | ||
| ".rs", | ||
| ".php", | ||
| ".sh", | ||
| ".pl", | ||
| ".swift", | ||
| ".kt", | ||
| ".kts", | ||
| ".dart", | ||
| ".scala", | ||
| ".lua", | ||
| ".r", | ||
| ".jl", | ||
| ".cs", | ||
| ".csx", | ||
| ".m", | ||
| ".mm", | ||
| ".bat", | ||
| ".cmd", | ||
| } |
Alert delta unavailable
Currently unable to show alert delta for PyPI packages.
76455
11.26%28
40%998
32.01%