Initial commit

This commit is contained in:
2025-04-09 09:34:15 +02:00
commit c19fb93ec5
47 changed files with 5174 additions and 0 deletions
+4
View File
@@ -0,0 +1,4 @@
"""
Utility functions for the Edison application.
"""
from . import markdown_utils
+71
View File
@@ -0,0 +1,71 @@
"""
Logging utilities for the Edison application.
"""
import logging
import os
import sys
def setup_logging(verbose=False):
"""
Configure logging for the application.
Args:
verbose: If True, enable DEBUG level messages. If False, show only WARNING and above.
Returns:
logging.Logger: The configured logger.
"""
# Create logs directory if it doesn't exist
script_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
logs_dir = os.path.join(script_dir, "logs")
os.makedirs(logs_dir, exist_ok=True)
# Determine log levels based on verbose flag
# Always save INFO+ logs to file, but console output depends on verbose flag
file_level = logging.INFO
console_level = logging.DEBUG if verbose else logging.WARNING
# Configure file and console handlers with different levels
file_handler = logging.FileHandler(os.path.join(logs_dir, 'edison.log'))
file_handler.setLevel(file_level)
console_handler = logging.StreamHandler(sys.stderr)
console_handler.setLevel(console_level)
# We need to set the root logger level to the lowest of our handlers
root_level = logging.DEBUG if verbose else logging.INFO
# Basic config with handlers
logging.basicConfig(
level=root_level,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[file_handler, console_handler]
)
# Always disable httpx and urllib3 debug logs unless in verbose mode
if not verbose:
logging.getLogger("httpx").setLevel(logging.WARNING)
logging.getLogger("urllib3").setLevel(logging.WARNING)
# Get the main application logger
logger = logging.getLogger('edison')
# Log setup confirmation at appropriate level
if verbose:
logger.debug("Verbose logging enabled (DEBUG level)")
else:
logger.info("Standard logging configuration applied (INFO to file, WARNING to console)")
return logger
def get_logger(name):
"""
Get a logger for the specified name.
Args:
name: The name of the logger.
Returns:
logging.Logger: The logger.
"""
return logging.getLogger(name)
+216
View File
@@ -0,0 +1,216 @@
"""
Markdown formatting utilities for terminal display.
"""
import re
import io
import sys
from termcolor import colored
from rich.console import Console
from rich.markdown import Markdown
from rich.syntax import Syntax
from rich.panel import Panel
def convert_markdown_to_terminal(text):
"""
Convert markdown formatting to terminal-friendly colored text.
Args:
text (str): Markdown text to convert
Returns:
str: Terminal-formatted text with ANSI color codes
"""
if not text:
return text
# Handle code blocks with triple backticks
text = re.sub(
r'```(?:\w+)?\n(.*?)\n```',
lambda m: '\n' + colored(m.group(1), 'white', attrs=['bold', 'dark']) + '\n',
text,
flags=re.DOTALL
)
# Handle inline code with single backticks
text = re.sub(
r'`([^`]+)`',
lambda m: colored(m.group(1), 'white', attrs=['bold']),
text
)
# Handle bold text with ** or __
text = re.sub(
r'\*\*([^*]+)\*\*|__([^_]+)__',
lambda m: colored(m.group(1) or m.group(2), attrs=['bold']),
text
)
# Handle italic text with * or _
text = re.sub(
r'\*([^*]+)\*|_([^_]+)_',
lambda m: colored(m.group(1) or m.group(2), attrs=['underline']),
text
)
# Handle headers
text = re.sub(
r'^#{1,6}\s+(.+)$',
lambda m: colored(m.group(1), 'cyan', attrs=['bold']),
text,
flags=re.MULTILINE
)
# Handle unordered lists
text = re.sub(
r'^(\s*[-*•]\s+)(.+)$',
lambda m: m.group(1) + colored(m.group(2), 'white'),
text,
flags=re.MULTILINE
)
# Handle ordered lists
text = re.sub(
r'^(\s*\d+\.\s+)(.+)$',
lambda m: m.group(1) + colored(m.group(2), 'white'),
text,
flags=re.MULTILINE
)
# Handle links [text](url) - keep only the text part
text = re.sub(
r'\[([^\]]+)\]\([^)]+\)',
lambda m: colored(m.group(1), 'blue', attrs=['underline']),
text
)
# Handle horizontal rules
text = re.sub(
r'^-{3,}$|^\*{3,}$|^_{3,}$',
lambda m: colored('-' * 50, 'white', attrs=['dark']),
text,
flags=re.MULTILINE
)
# Handle blockquotes
text = re.sub(
r'^>\s+(.+)$',
lambda m: colored('', 'cyan') + colored(m.group(1), 'cyan'),
text,
flags=re.MULTILINE
)
return text
def format_command_explanation(explanation, use_rich=False):
"""
Format a command explanation with appropriate terminal styling.
Args:
explanation (str): The explanation text in markdown format
use_rich (bool): Whether to use rich formatting
Returns:
str: Formatted explanation with terminal-friendly styling
"""
if use_rich:
return format_command_explanation_rich(explanation)
# First convert markdown to terminal format
formatted_text = convert_markdown_to_terminal(explanation)
# Add additional formatting for common command explanation patterns
# Highlight command parts and common technical terms
formatted_text = re.sub(
r'\b(command|option|flag|argument|parameter|switch)\b',
lambda m: colored(m.group(0), 'yellow'),
formatted_text,
flags=re.IGNORECASE
)
# Highlight file paths and patterns
# More specific pattern to avoid matching things like "Unix/Linux"
formatted_text = re.sub(
r'(?:^|\s)(/(?:[\w.-]+/?)+)(?=\s|$|[,.;:])', # Path must start with / and have at least one directory component
lambda m: colored(m.group(1), 'green'),
formatted_text
)
# Highlight command names and utilities
# Common Unix/Linux commands
formatted_text = re.sub(
r'\b(ls|grep|find|awk|sed|cat|cp|mv|rm|mkdir|chmod|chown|ps|kill|top|df|du|tar|gzip|ssh|scp|curl|wget)\b',
lambda m: colored(m.group(0), 'green'),
formatted_text
)
# Highlight "This command" starts of sentences
formatted_text = re.sub(
r'(^|[.!?]\s+)(This command|The command)',
lambda m: m.group(1) + colored(m.group(2), 'cyan', attrs=['bold']),
formatted_text
)
return formatted_text
def format_command_explanation_rich(explanation):
"""
Format a command explanation with rich formatting.
Args:
explanation (str): The explanation text in markdown format
Returns:
str: Rich-formatted explanation as a string
"""
# Capture the rich output as a string
str_io = io.StringIO()
console = Console(file=str_io, width=80)
# Create a markdown object
md = Markdown(explanation)
# Render in a panel with a title
panel = Panel(md, title="Command Explanation", border_style="green")
console.print(panel)
return str_io.getvalue()
def format_command_rich(command, shell="bash", theme="monokai"):
"""
Format a command with syntax highlighting using Rich.
Args:
command (str): The command to format
shell (str): The shell language for syntax highlighting
theme (str): The color theme to use
Returns:
str: Rich-formatted command as a string
"""
# Capture the rich output as a string
str_io = io.StringIO()
console = Console(file=str_io, width=80)
# Create a syntax object
syntax = Syntax(command, shell, theme=theme, word_wrap=True)
# Render in a panel with a title
panel = Panel(syntax, title="Command", border_style="blue")
console.print(panel)
return str_io.getvalue()
def print_command_rich(command, shell="bash", theme="monokai"):
"""
Print a command with syntax highlighting using Rich.
Args:
command (str): The command to print
shell (str): The shell language for syntax highlighting
theme (str): The color theme to use
"""
console = Console()
syntax = Syntax(command, shell, theme=theme, word_wrap=True)
panel = Panel(syntax, title="Command", border_style="blue")
console.print(panel)
+59
View File
@@ -0,0 +1,59 @@
"""
Operating system utilities for the Edison application.
"""
import os
import platform
import subprocess
import distro
def get_os_friendly_name():
"""
Returns a friendly name of the user's operating system.
The function retrieves the current system platform name using the `platform.system()` function.
For Linux, it appends the distribution name retrieved from `distro.name(pretty=True)` to give a
more descriptive representation. For Darwin (Apple's macOS), it appends "macOS" to "Darwin" to
make the output clearer to the user.
Returns:
str: A friendly name for the user's operating system. It will be one of the following:
- "Linux/<distribution name>"
- "Darwin/macOS"
- The system string returned by `platform.system()` if it's not Linux or Darwin.
"""
os_name = platform.system()
if os_name == "Linux":
os_name = "Linux/" + distro.name(pretty=True)
elif os_name == "Darwin":
os_name = "Darwin/macOS"
return os_name
def missing_posix_display():
"""
Checks if the DISPLAY environment variable is set in a POSIX-compliant shell.
This function runs a shell subprocess that outputs the value of the DISPLAY environment
variable. It then checks if this value is unset (i.e., equals a newline 'b'\\n'') in the
current shell environment. If the DISPLAY variable is unset, the function returns `True`
indicating a "missing" display; otherwise, it returns `False`.
Returns:
bool: `True` if the DISPLAY environment variable is unset or empty, `False` otherwise.
"""
if os.name != "posix":
return False
display = subprocess.check_output("echo $DISPLAY", shell=True)
return display == b'\n'
def get_default_shell():
"""
Get the default shell for the current operating system.
Returns:
str: The default shell.
"""
# Unix based SHELL (/bin/bash, /bin/zsh), otherwise assuming it's Windows
return os.environ.get("SHELL", "powershell.exe")
+60
View File
@@ -0,0 +1,60 @@
"""
Validation utilities for the Edison application.
"""
import re
import logging
logger = logging.getLogger(__name__)
def is_dangerous_command(command):
"""
Check if a command contains potentially dangerous operations.
Args:
command (str): The command to check.
Returns:
bool: True if the command is potentially dangerous, False otherwise.
"""
dangerous_patterns = [
r"rm\s+-rf\s+/", # Remove recursively from root
r"sudo\s+rm", # Sudo remove
r"chmod\s+777", # Chmod 777 (too permissive)
r">\s+/dev/", # Redirect to device
r">\s+/etc/", # Redirect to system config
r"mkfs", # Format filesystem
r"dd\s+if=", # Disk destroyer
r":(){:\|:};:", # Fork bomb
]
for pattern in dangerous_patterns:
if re.search(pattern, command, re.IGNORECASE):
logger.warning(f"Potentially dangerous command detected: {command}")
return True
return False
def check_for_issue(response):
"""
Checks the given response for any issues.
Args:
response (str): The response to check.
Returns:
bool: True if there's an issue, False otherwise.
"""
prefixes = ("sorry", "i'm sorry", "the question is not clear", "i'm", "i am")
return response.lower().startswith(prefixes)
def check_for_markdown(response):
"""
Checks for the presence of markdown formatting in the response.
Args:
response (str): The response to check.
Returns:
bool: True if markdown is detected, False otherwise.
"""
return response.count("```", 2)