Initial commit
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
# Edison
|
||||
|
||||
Edison is an AI-powered command-line tool that translates natural language queries into shell commands. It leverages OpenAI's GPT models to generate accurate and useful commands based on your descriptions.
|
||||
|
||||
## Features
|
||||
|
||||
- Translate natural language to shell commands
|
||||
- Support for multiple shells (bash, zsh, powershell)
|
||||
- Safety checks for potentially dangerous commands
|
||||
- Command modification and clipboard support
|
||||
- Configurable via YAML file
|
||||
- Rich terminal UI with syntax highlighting
|
||||
- Interactive shell mode with command history
|
||||
- Command explanations
|
||||
- Progress indicators and spinners
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
# Clone the repository
|
||||
git clone https://github.com/yourusername/edison.git
|
||||
cd edison
|
||||
|
||||
# Install the package
|
||||
pip install -e .
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
# Basic usage
|
||||
edison how to list all files in the current directory
|
||||
|
||||
# Enable safety mode (confirmation before execution)
|
||||
edison -s how to find all python files recursively
|
||||
|
||||
# Print current configuration
|
||||
edison -c
|
||||
|
||||
# Enable verbose logging
|
||||
edison -v how to check disk space
|
||||
|
||||
# Use rich terminal UI
|
||||
edison -r how to find large files
|
||||
|
||||
# Get command explanations
|
||||
edison -e how to compress a directory
|
||||
|
||||
# Start interactive shell mode
|
||||
edison -i
|
||||
```
|
||||
|
||||
## Interactive Shell
|
||||
|
||||
Edison includes an interactive shell mode that provides a more user-friendly experience:
|
||||
|
||||
```bash
|
||||
# Start interactive shell
|
||||
edison -i
|
||||
```
|
||||
|
||||
In interactive mode, you can:
|
||||
- Type natural language queries
|
||||
- Navigate command history with up/down arrows
|
||||
- Get auto-suggestions based on previous commands
|
||||
- Execute, modify, copy, or get explanations for commands
|
||||
- Use special commands like `!help` and `!exit`
|
||||
|
||||
## Rich Terminal UI
|
||||
|
||||
The rich terminal UI provides:
|
||||
- Syntax highlighting for commands
|
||||
- Colorful output
|
||||
- Progress indicators during API calls
|
||||
- Command explanations with detailed breakdowns
|
||||
- Success/error messages with appropriate styling
|
||||
|
||||
## Configuration
|
||||
|
||||
Edison can be configured via the `edison.yaml` file. Here's an example configuration:
|
||||
|
||||
```yaml
|
||||
model: gpt-3.5-turbo
|
||||
temperature: 0
|
||||
max_tokens: 500
|
||||
safety: true
|
||||
```
|
||||
|
||||
## API Key
|
||||
|
||||
Edison requires an OpenAI API key to function. You can provide it in one of the following ways:
|
||||
|
||||
1. Environment variable: `OPENAI_API_KEY="your-api-key"`
|
||||
2. `.env` file in the same directory as the script
|
||||
3. `.openai.apikey` file in your home directory
|
||||
4. In the `edison.yaml` configuration file: `openai_api_key: "your-api-key"`
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
@@ -0,0 +1,8 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Entry point for running the edison package as a module.
|
||||
"""
|
||||
from edison.cli import main
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
"""
|
||||
Command-line interface for the Edison application.
|
||||
"""
|
||||
import argparse
|
||||
import logging
|
||||
from colorama import init
|
||||
from edison.config import config_manager
|
||||
from edison.core import api_client
|
||||
from edison.ui import console, interactive
|
||||
from edison.utils import logging_utils, os_utils
|
||||
|
||||
# Logger will be initialized after parsing args
|
||||
logger = None
|
||||
|
||||
def parse_arguments():
|
||||
"""
|
||||
Parse command line arguments.
|
||||
|
||||
Returns:
|
||||
argparse.Namespace: The parsed arguments.
|
||||
"""
|
||||
parser = argparse.ArgumentParser(
|
||||
description='AI bot that translates your question to a command.'
|
||||
)
|
||||
parser.add_argument('text', nargs='*',
|
||||
help='A sequence of strings')
|
||||
parser.add_argument("-s", "--safety", action='store_true',
|
||||
help='Enable safety mode (only useful when safety is off)')
|
||||
parser.add_argument("-c", "--config", action='store_true',
|
||||
help='Print current configuration')
|
||||
parser.add_argument("-v", "--verbose", action='store_true',
|
||||
help='Enable verbose logging (DEBUG level)')
|
||||
parser.add_argument("-i", "--interactive", action='store_true',
|
||||
help='Start interactive shell mode')
|
||||
parser.add_argument("-e", "--explain", action='store_true',
|
||||
help='Explain the generated command')
|
||||
parser.add_argument("--no-streaming", action='store_true',
|
||||
help='Disable streaming output for command generation')
|
||||
parser.add_argument("--no-rich", action='store_true',
|
||||
help='Disable rich text formatting')
|
||||
parser.add_argument("--theme", type=str,
|
||||
help='Set syntax highlighting theme (e.g., monokai, github-dark)')
|
||||
return parser.parse_args()
|
||||
|
||||
def main():
|
||||
"""
|
||||
Main entry point for the application.
|
||||
"""
|
||||
# Parse command line arguments
|
||||
args = parse_arguments()
|
||||
|
||||
# Initialize logging based on the verbose flag
|
||||
global logger
|
||||
logger = logging_utils.setup_logging(verbose=args.verbose)
|
||||
|
||||
try:
|
||||
# Load configuration
|
||||
config = config_manager.load_config()
|
||||
logger.debug("Configuration loaded")
|
||||
|
||||
# Process command-line arguments
|
||||
if args.safety:
|
||||
config["safety"] = args.safety
|
||||
|
||||
# Set streaming and UI options based on command-line arguments
|
||||
if args.no_streaming:
|
||||
config["streaming"] = False
|
||||
|
||||
# Initialize UI config if not present
|
||||
if "ui" not in config:
|
||||
config["ui"] = {}
|
||||
|
||||
# Set rich formatting option
|
||||
if args.no_rich:
|
||||
config["ui"]["rich_formatting"] = False
|
||||
|
||||
# Set theme if specified
|
||||
if args.theme:
|
||||
config["ui"]["theme"] = args.theme
|
||||
|
||||
# Set default shell if not specified
|
||||
if "shell" not in config:
|
||||
config["shell"] = os_utils.get_default_shell()
|
||||
|
||||
# Print configuration if requested
|
||||
if args.config:
|
||||
config_manager.print_config(config)
|
||||
return
|
||||
|
||||
# Enable color output on Windows using colorama
|
||||
init()
|
||||
|
||||
# Initialize API client
|
||||
try:
|
||||
client = api_client.create_client(config)
|
||||
logger.debug("API client initialized")
|
||||
|
||||
# Start interactive mode if requested
|
||||
if args.interactive:
|
||||
interactive.interactive_mode(client, config)
|
||||
return
|
||||
|
||||
# Check if we have a query
|
||||
if not args.text:
|
||||
print("No query provided. Use -i for interactive mode.")
|
||||
return
|
||||
|
||||
# Join the text arguments
|
||||
user_prompt = " ".join(args.text)
|
||||
|
||||
# Generate command with or without streaming based on config
|
||||
if config.get("streaming", True):
|
||||
# Use streaming version
|
||||
console.handle_command_execution_streaming(client, config, user_prompt, explain=args.explain)
|
||||
else:
|
||||
# Use non-streaming version
|
||||
command = api_client.generate_command(client, config, user_prompt)
|
||||
logger.debug("Command generated")
|
||||
console.handle_command_execution(client, config, command, explain=args.explain)
|
||||
except Exception as e:
|
||||
logger.error(f"Error: {e}")
|
||||
print(f"Error: {e}")
|
||||
except Exception as e:
|
||||
logger.error(f"Unhandled exception: {e}")
|
||||
print(f"An unexpected error occurred: {e}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,60 @@
|
||||
"""
|
||||
Configuration management for the Edison application.
|
||||
"""
|
||||
import os
|
||||
import yaml
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def get_config_path():
|
||||
"""
|
||||
Get the path to the configuration file.
|
||||
|
||||
Returns:
|
||||
str: The path to the configuration file.
|
||||
"""
|
||||
script_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
return os.path.join(script_dir, "edison.yaml")
|
||||
|
||||
def load_config():
|
||||
"""
|
||||
Load configuration from file.
|
||||
|
||||
Returns:
|
||||
dict: The loaded configuration.
|
||||
"""
|
||||
config_path = get_config_path()
|
||||
try:
|
||||
with open(config_path, 'r') as file:
|
||||
config = yaml.safe_load(file)
|
||||
logger.debug(f"Configuration loaded from {config_path}")
|
||||
return config
|
||||
except Exception as e:
|
||||
logger.error(f"Error loading configuration: {e}")
|
||||
return {}
|
||||
|
||||
def print_config(config):
|
||||
"""
|
||||
Print configuration information.
|
||||
|
||||
Args:
|
||||
config (dict): The configuration dictionary.
|
||||
"""
|
||||
print("Current configuration per edison.yaml:")
|
||||
print("— Model : " + str(config.get("model", "Not specified")))
|
||||
print("— Temperature : " + str(config.get("temperature", "Not specified")))
|
||||
print("— Max. Tokens : " + str(config.get("max_tokens", "Not specified")))
|
||||
print("— Safety : " + str(bool(config.get("safety", False))))
|
||||
print("— Streaming : " + str(bool(config.get("streaming", True))))
|
||||
print("— Show Prefix : " + str(bool(config.get("show_generating_prefix", True))))
|
||||
print("— Shell : " + str(config.get("shell", "Not specified")))
|
||||
|
||||
# Print UI configuration if available
|
||||
ui_config = config.get("ui", {})
|
||||
if ui_config:
|
||||
print("\nUI Configuration:")
|
||||
print("— Rich Formatting : " + str(bool(ui_config.get("rich_formatting", True))))
|
||||
print("— Theme : " + str(ui_config.get("theme", "monokai")))
|
||||
print("— Command Style : " + str(ui_config.get("command_style", "panel")))
|
||||
print("— Structured Expl.: " + str(bool(ui_config.get("structured_explanations", True))))
|
||||
@@ -0,0 +1,218 @@
|
||||
"""
|
||||
OpenAI API client for the Edison application.
|
||||
"""
|
||||
import os
|
||||
import time
|
||||
import logging
|
||||
import dotenv
|
||||
from openai import OpenAI
|
||||
from edison.core import prompt_manager
|
||||
from edison.utils import logging_utils
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def get_api_key(config):
|
||||
"""
|
||||
Get the OpenAI API key from various sources.
|
||||
|
||||
This function first tries to grab the OpenAI API key from environment variables,
|
||||
if not found, it then looks for the key in the `.openai.apikey` in the home directory,
|
||||
and lastly, it will look in the provided config dictionary.
|
||||
|
||||
Args:
|
||||
config (dict): A dictionary containing configuration values.
|
||||
It may contain `openai_api_key` as one of the keys.
|
||||
|
||||
Returns:
|
||||
str: The OpenAI API key
|
||||
"""
|
||||
dotenv.load_dotenv()
|
||||
|
||||
# Method 1: Read API key from environment variable
|
||||
# The user can set their OpenAI API key by creating a ".env" file in the same
|
||||
# directory as this script or by exporting it to their environment variables.
|
||||
# The file or environment variable should contain the line `OPENAI_API_KEY="<yourkey>"`.
|
||||
api_key = os.getenv("OPENAI_API_KEY")
|
||||
|
||||
# Method 2: Read API key from a file in the home directory
|
||||
# The user can also place a file named ".openai.apikey" in their home directory,
|
||||
# which includes the API key in raw format. This method might be deprecated in future versions.
|
||||
if not api_key:
|
||||
home_path = os.path.expanduser("~")
|
||||
api_key_path = os.path.join(home_path, ".openai.apikey")
|
||||
if os.path.exists(api_key_path):
|
||||
with open(api_key_path, 'r') as f:
|
||||
api_key = f.read().strip()
|
||||
|
||||
# Method 3: Read API key from the provided config dictionary
|
||||
# The final method to set the API key is by providing it in the 'config' dictionary under the
|
||||
# key 'openai_api_key'. For instance, in a `edison.yaml` config file, it would appear as
|
||||
# `openai_apikey: <yourkey>`.
|
||||
if not api_key:
|
||||
api_key = config.get("openai_api_key")
|
||||
|
||||
if not api_key:
|
||||
logger.error("No OpenAI API key found. Please set it in your environment, .env file, or config.")
|
||||
raise ValueError("No OpenAI API key found")
|
||||
|
||||
return api_key
|
||||
|
||||
def create_client(config):
|
||||
"""
|
||||
Create and initialize an OpenAI client.
|
||||
|
||||
Args:
|
||||
config (dict): The configuration dictionary.
|
||||
|
||||
Returns:
|
||||
OpenAI: An initialized OpenAI client.
|
||||
"""
|
||||
api_key = get_api_key(config)
|
||||
return OpenAI(api_key=api_key)
|
||||
|
||||
def call_api(client, config, query):
|
||||
"""
|
||||
Call the OpenAI API with the given query.
|
||||
|
||||
Args:
|
||||
client (OpenAI): The OpenAI client instance.
|
||||
config (dict): Configuration dictionary containing model and parameters.
|
||||
query (str): The user's query string.
|
||||
|
||||
Returns:
|
||||
str: The generated command as a string.
|
||||
"""
|
||||
if not query:
|
||||
logger.error("No user prompt specified.")
|
||||
raise ValueError("No user prompt specified")
|
||||
|
||||
# Load the correct prompt based on shell and OS and append the user's prompt
|
||||
prompt = prompt_manager.get_full_prompt(query, config.get("shell", "bash"))
|
||||
|
||||
# Extract the system prompt from the first line
|
||||
system_prompt = prompt.split('\n')[0] if '\n' in prompt else prompt
|
||||
|
||||
try:
|
||||
# Use the modern API pattern
|
||||
response = client.chat.completions.create(
|
||||
model=config.get("model", "gpt-4o-mini"),
|
||||
messages=[
|
||||
{"role": "system", "content": system_prompt},
|
||||
{"role": "user", "content": prompt}
|
||||
],
|
||||
temperature=config.get("temperature", 0),
|
||||
max_tokens=config.get("max_tokens", 500),
|
||||
)
|
||||
|
||||
# Extract the content from the new response structure
|
||||
return response.choices[0].message.content.strip()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error calling OpenAI API: {str(e)}")
|
||||
raise
|
||||
|
||||
def call_api_streaming(client, config, query, callback):
|
||||
"""
|
||||
Call the OpenAI API with streaming enabled.
|
||||
|
||||
Args:
|
||||
client (OpenAI): The OpenAI client instance.
|
||||
config (dict): Configuration dictionary containing model and parameters.
|
||||
query (str): The user's query string.
|
||||
callback (callable): Function to call with each token as it arrives.
|
||||
|
||||
Returns:
|
||||
str: The complete generated command as a string.
|
||||
"""
|
||||
if not query:
|
||||
logger.error("No user prompt specified.")
|
||||
raise ValueError("No user prompt specified")
|
||||
|
||||
# Load the correct prompt based on shell and OS and append the user's prompt
|
||||
prompt = prompt_manager.get_full_prompt(query, config.get("shell", "bash"))
|
||||
|
||||
# Extract the system prompt from the first line
|
||||
system_prompt = prompt.split('\n')[0] if '\n' in prompt else prompt
|
||||
|
||||
try:
|
||||
# Use the streaming API pattern
|
||||
response = client.chat.completions.create(
|
||||
model=config.get("model", "gpt-4o-mini"),
|
||||
messages=[
|
||||
{"role": "system", "content": system_prompt},
|
||||
{"role": "user", "content": prompt}
|
||||
],
|
||||
temperature=config.get("temperature", 0),
|
||||
max_tokens=config.get("max_tokens", 500),
|
||||
stream=True # Enable streaming
|
||||
)
|
||||
|
||||
# Initialize an empty string to collect the full response
|
||||
full_response = ""
|
||||
|
||||
# Process the streaming response
|
||||
for chunk in response:
|
||||
if hasattr(chunk.choices[0].delta, 'content'):
|
||||
content = chunk.choices[0].delta.content
|
||||
if content:
|
||||
# Call the callback with the new content
|
||||
callback(content)
|
||||
# Append to the full response
|
||||
full_response += content
|
||||
|
||||
return full_response.strip()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error calling OpenAI API: {str(e)}")
|
||||
raise
|
||||
|
||||
def generate_command_streaming(client, config, query, callback, max_retries=3):
|
||||
"""
|
||||
Generate a command using the OpenAI API with streaming and retry logic.
|
||||
|
||||
Args:
|
||||
client (OpenAI): The OpenAI client instance.
|
||||
config (dict): Configuration dictionary containing model and parameters.
|
||||
query (str): The user's query string.
|
||||
callback (callable): Function to call with each token as it arrives.
|
||||
max_retries (int): Maximum number of retry attempts.
|
||||
|
||||
Returns:
|
||||
str: The generated command as a string.
|
||||
"""
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
return call_api_streaming(client, config, query, callback)
|
||||
except Exception as e:
|
||||
if "rate limit" in str(e).lower() and attempt < max_retries - 1:
|
||||
wait_time = 2 ** attempt # Exponential backoff
|
||||
logger.warning(f"Rate limited. Retrying in {wait_time} seconds...")
|
||||
time.sleep(wait_time)
|
||||
else:
|
||||
logger.error(f"Error after {attempt+1} attempts: {str(e)}")
|
||||
raise
|
||||
|
||||
def generate_command(client, config, query, max_retries=3):
|
||||
"""
|
||||
Generate a command using the OpenAI API with retry logic.
|
||||
|
||||
Args:
|
||||
client (OpenAI): The OpenAI client instance.
|
||||
config (dict): Configuration dictionary containing model and parameters.
|
||||
query (str): The user's query string.
|
||||
max_retries (int): Maximum number of retry attempts.
|
||||
|
||||
Returns:
|
||||
str: The generated command as a string.
|
||||
"""
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
return call_api(client, config, query)
|
||||
except Exception as e:
|
||||
if "rate limit" in str(e).lower() and attempt < max_retries - 1:
|
||||
wait_time = 2 ** attempt # Exponential backoff
|
||||
logger.warning(f"Rate limited. Retrying in {wait_time} seconds...")
|
||||
time.sleep(wait_time)
|
||||
else:
|
||||
logger.error(f"Error after {attempt+1} attempts: {str(e)}")
|
||||
raise
|
||||
@@ -0,0 +1,39 @@
|
||||
"""
|
||||
Command execution for the Edison application.
|
||||
"""
|
||||
import subprocess
|
||||
import logging
|
||||
from edison.utils import validation
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def execute_command(shell, command):
|
||||
"""
|
||||
Execute a shell command.
|
||||
|
||||
Args:
|
||||
shell (str): The shell to use.
|
||||
command (str): The command to execute.
|
||||
|
||||
Returns:
|
||||
subprocess.CompletedProcess: The result of the command execution.
|
||||
|
||||
Raises:
|
||||
subprocess.CalledProcessError: If the command execution fails.
|
||||
"""
|
||||
if validation.is_dangerous_command(command):
|
||||
logger.warning(f"Potentially dangerous command detected: {command}")
|
||||
# We still allow execution but log a warning
|
||||
|
||||
try:
|
||||
if shell == "powershell.exe":
|
||||
result = subprocess.run([shell, "/c", command], shell=False, check=True)
|
||||
else:
|
||||
# Unix: /bin/bash /bin/zsh: uses -c both Ubuntu and macOS should work, others might not
|
||||
result = subprocess.run([shell, "-c", command], shell=False, check=True)
|
||||
|
||||
logger.debug(f"Command executed successfully: {command}")
|
||||
return result
|
||||
except subprocess.CalledProcessError as e:
|
||||
logger.error(f"Command execution failed: {e}")
|
||||
raise
|
||||
@@ -0,0 +1,54 @@
|
||||
"""
|
||||
Prompt management for the Edison application.
|
||||
"""
|
||||
import os
|
||||
import logging
|
||||
from edison.utils import os_utils
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def get_prompt_template_path():
|
||||
"""
|
||||
Get the path to the prompt template file.
|
||||
|
||||
Returns:
|
||||
str: The path to the prompt template file.
|
||||
"""
|
||||
script_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
return os.path.join(script_dir, "edison.prompt")
|
||||
|
||||
def get_full_prompt(user_prompt, shell):
|
||||
"""
|
||||
Construct a full prompt from the template and user input.
|
||||
|
||||
Args:
|
||||
user_prompt (str): The user's prompt.
|
||||
shell (str): The shell to use.
|
||||
|
||||
Returns:
|
||||
str: The full prompt.
|
||||
"""
|
||||
# Get the path to the prompt template
|
||||
prompt_file = get_prompt_template_path()
|
||||
|
||||
try:
|
||||
# Load the prompt template
|
||||
with open(prompt_file, "r") as f:
|
||||
pre_prompt = f.read()
|
||||
|
||||
# Replace placeholders
|
||||
pre_prompt = pre_prompt.replace("{shell}", shell)
|
||||
pre_prompt = pre_prompt.replace("{os}", os_utils.get_os_friendly_name())
|
||||
|
||||
# Append the user prompt
|
||||
prompt = pre_prompt + user_prompt
|
||||
|
||||
# Make it a question if it's not already
|
||||
if prompt[-1:] != "?" and prompt[-1:] != ".":
|
||||
prompt += "?"
|
||||
|
||||
return prompt
|
||||
except Exception as e:
|
||||
logger.error(f"Error loading prompt template: {e}")
|
||||
# Fallback to a simple prompt
|
||||
return f"Act as a natural language to {shell} command translation engine on {os_utils.get_os_friendly_name()}. {user_prompt}"
|
||||
@@ -0,0 +1,31 @@
|
||||
Act as a natural language to {shell} command translation engine on {os}.
|
||||
|
||||
You are an expert in {shell} on {os} and translate the question at the end to valid syntax.
|
||||
|
||||
Follow these rules:
|
||||
Construct valid {shell} command that solve the question
|
||||
Leverage help and man pages to ensure valid syntax and an optimal solution
|
||||
Be concise
|
||||
Just show the commands
|
||||
Return only plaintext
|
||||
Only show a single answer, but you can always chain commands together
|
||||
Think step by step
|
||||
Only create valid syntax (you can use comments if it makes sense)
|
||||
If python is installed you can use it to solve problems
|
||||
if python3 is installed you can use it to solve problems
|
||||
Even if there is a lack of details, attempt to find the most logical solution by going about it step by step
|
||||
Do not return multiple solutions
|
||||
Do not show html, styled, colored formatting
|
||||
Do not creating invalid syntax
|
||||
Do not add unnecessary text in the response
|
||||
Do not add notes or intro sentences
|
||||
Do not show multiple distinct solutions to the question
|
||||
Do not add explanations on what the commands do
|
||||
Do not return what the question was
|
||||
Do not repeat or paraphrase the question in your response
|
||||
Do not cause syntax errors
|
||||
Do not rush to a conclusion
|
||||
|
||||
Follow all of the above rules. This is important you MUST follow the above rules. There are no exceptions to these rules. You must always follow them. No exceptions.
|
||||
|
||||
Question:
|
||||
@@ -0,0 +1,31 @@
|
||||
model: gpt-4o-mini # Default model for command generation
|
||||
temperature: 0
|
||||
max_tokens: 500
|
||||
|
||||
# Safety: If set to False, commands returned from the AI will be run *without* prompting the user.
|
||||
safety: True
|
||||
|
||||
# Streaming: If set to True, command generation will be displayed in real-time as tokens arrive.
|
||||
# Set to False for traditional non-streaming behavior.
|
||||
streaming: True
|
||||
|
||||
# Show a "Generating command: " prefix during streaming
|
||||
# Set to False to remove the prefix and only show the command
|
||||
show_generating_prefix: True
|
||||
|
||||
# UI Configuration: Controls the appearance and behavior of the user interface
|
||||
ui:
|
||||
# Enable rich text formatting for command explanations and output
|
||||
rich_formatting: True
|
||||
|
||||
# Theme for syntax highlighting (options: monokai, github-dark, solarized-dark, etc.)
|
||||
theme: "monokai"
|
||||
|
||||
# Command display style (options: simple, highlighted, panel)
|
||||
command_style: "panel"
|
||||
|
||||
# Structured explanations: Generate more structured command explanations
|
||||
structured_explanations: True
|
||||
|
||||
# Open AI API Key (optional): The key can aso be provided via environment variable (OPENAI_API_KEY), .env, or ~/.openai.apikey file
|
||||
openai_api_key:
|
||||
@@ -0,0 +1,245 @@
|
||||
"""
|
||||
Console UI components for the Edison application.
|
||||
"""
|
||||
import logging
|
||||
import pyperclip
|
||||
from termcolor import colored
|
||||
from edison.core import api_client, command_executor
|
||||
from edison.utils import os_utils, validation, markdown_utils
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def handle_streaming_output(token):
|
||||
"""
|
||||
Handle a token from the streaming API response.
|
||||
|
||||
Args:
|
||||
token (str): A token from the streaming response.
|
||||
"""
|
||||
# Print the token without a newline and flush immediately
|
||||
print(token, end='', flush=True)
|
||||
|
||||
def print_command(command, config=None):
|
||||
"""
|
||||
Print a command to the console.
|
||||
|
||||
Args:
|
||||
command (str): The command to print.
|
||||
config (dict, optional): Configuration dictionary.
|
||||
"""
|
||||
if config and config.get("ui", {}).get("rich_formatting", True):
|
||||
command_style = config.get("ui", {}).get("command_style", "panel")
|
||||
theme = config.get("ui", {}).get("theme", "monokai")
|
||||
shell = config.get("shell", "bash")
|
||||
|
||||
if command_style == "panel":
|
||||
# Use rich panel style
|
||||
from edison.utils.markdown_utils import print_command_rich
|
||||
print_command_rich(command, shell=shell, theme=theme)
|
||||
else:
|
||||
# Use simple highlighting
|
||||
print("Command: " + colored(command, 'blue'))
|
||||
else:
|
||||
# Use traditional styling
|
||||
print("Command: " + colored(command, 'blue'))
|
||||
|
||||
def prompt_user_input(config, response):
|
||||
"""
|
||||
Prompt the user for input on what to do with the generated command.
|
||||
|
||||
Args:
|
||||
config (dict): The configuration dictionary.
|
||||
response (str): The generated command.
|
||||
|
||||
Returns:
|
||||
str: The user's input.
|
||||
"""
|
||||
print_command(response, config)
|
||||
|
||||
if config.get("safety", True):
|
||||
prompt_text = "Execute command? [Y]es [n]o [m]odify (prompt/command) [c]opy to clipboard ==> "
|
||||
|
||||
if os_utils.missing_posix_display():
|
||||
prompt_text = "Execute command? [Y]es [n]o [m]odify (prompt/command) ==> "
|
||||
|
||||
print(prompt_text, end='')
|
||||
user_input = input()
|
||||
else:
|
||||
user_input = "Y"
|
||||
|
||||
return user_input
|
||||
|
||||
def handle_command_execution_streaming(client, config, query, explain=False):
|
||||
"""
|
||||
Handle the execution of a command with streaming output.
|
||||
|
||||
Args:
|
||||
client (OpenAI): The OpenAI client.
|
||||
config (dict): The configuration dictionary.
|
||||
query (str): The user's query.
|
||||
explain (bool): Whether to explain the command.
|
||||
"""
|
||||
# Check if we should show the "Generating command: " prefix
|
||||
if config.get("show_generating_prefix", True):
|
||||
print(colored("Generating command: ", 'yellow'), end='', flush=True)
|
||||
|
||||
try:
|
||||
# Call the streaming API
|
||||
command = api_client.generate_command_streaming(client, config, query, handle_streaming_output)
|
||||
|
||||
# Print a newline after streaming is complete
|
||||
print()
|
||||
|
||||
# Continue with the regular command execution flow
|
||||
handle_command_execution(client, config, command, explain)
|
||||
except KeyboardInterrupt:
|
||||
print("\nCommand generation cancelled.")
|
||||
return
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating command: {e}")
|
||||
print(colored(f"\nError generating command: {e}", 'red'))
|
||||
|
||||
def handle_command_execution(client, config, command, explain=False):
|
||||
"""
|
||||
Handle the execution of a command based on user input.
|
||||
|
||||
Args:
|
||||
client (OpenAI): The OpenAI client.
|
||||
config (dict): The configuration dictionary.
|
||||
command (str): The command to execute.
|
||||
explain (bool): Whether to explain the command.
|
||||
"""
|
||||
# Check for issues in the response
|
||||
if validation.check_for_issue(command):
|
||||
print(colored("There was an issue: " + command, 'red'))
|
||||
return
|
||||
|
||||
# Check for markdown in the response
|
||||
if validation.check_for_markdown(command):
|
||||
print(colored(
|
||||
"The proposed command contains markdown, response not executed directly: \n", 'red'
|
||||
) + command)
|
||||
return
|
||||
|
||||
# Get explanation if requested
|
||||
if explain:
|
||||
try:
|
||||
from edison.ui.interactive import get_command_explanation
|
||||
|
||||
# Get UI configuration
|
||||
ui_config = config.get("ui", {})
|
||||
structured = ui_config.get("structured_explanations", True)
|
||||
use_rich = ui_config.get("rich_formatting", True)
|
||||
|
||||
# Generate explanation
|
||||
explanation = get_command_explanation(client, command, structured=structured)
|
||||
|
||||
if use_rich:
|
||||
print() # Add a blank line before the explanation
|
||||
# Format the explanation with rich formatting
|
||||
formatted_explanation = markdown_utils.format_command_explanation(explanation, use_rich=True)
|
||||
print(formatted_explanation)
|
||||
else:
|
||||
print(colored("\nExplanation:", "green", attrs=["bold"]))
|
||||
# Format the explanation with terminal-friendly markdown
|
||||
formatted_explanation = markdown_utils.format_command_explanation(explanation)
|
||||
print(formatted_explanation)
|
||||
|
||||
print()
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating explanation: {e}")
|
||||
print(colored(f"Error generating explanation: {e}", 'red'))
|
||||
|
||||
# Get user input
|
||||
user_input = prompt_user_input(config, command)
|
||||
print()
|
||||
|
||||
# Handle user input
|
||||
handle_user_input(client, config, user_input, command)
|
||||
|
||||
def handle_user_input(client, config, user_input, command):
|
||||
"""
|
||||
Handle user input for command execution.
|
||||
|
||||
Args:
|
||||
client (OpenAI): The OpenAI client.
|
||||
config (dict): The configuration dictionary.
|
||||
user_input (str): The user's input.
|
||||
command (str): The command to execute.
|
||||
"""
|
||||
if user_input.upper() == "Y" or user_input == "":
|
||||
try:
|
||||
command_executor.execute_command(config.get("shell", os_utils.get_default_shell()), command)
|
||||
except Exception as e:
|
||||
logger.error(f"Error executing command: {e}")
|
||||
print(colored(f"Error executing command: {e}", 'red'))
|
||||
|
||||
elif user_input.upper() == "M":
|
||||
print("Modify [p]rompt or [c]ommand? [c] ==> ", end='')
|
||||
mod_choice = input().lower()
|
||||
|
||||
if mod_choice in ["p", "prompt"]:
|
||||
# Modify the prompt (natural language description)
|
||||
print("Modify prompt: ", end='')
|
||||
modded_query = input()
|
||||
|
||||
if not modded_query.strip():
|
||||
print(colored("Empty prompt. Command execution cancelled.", "yellow"))
|
||||
return
|
||||
|
||||
# Use streaming if enabled in config
|
||||
if config.get("streaming", True):
|
||||
try:
|
||||
handle_command_execution_streaming(client, config, modded_query)
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating modified command: {e}")
|
||||
print(colored(f"Error generating modified command: {e}", 'red'))
|
||||
else:
|
||||
try:
|
||||
modded_response = api_client.generate_command(client, config, modded_query)
|
||||
handle_command_execution(client, config, modded_response)
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating modified command: {e}")
|
||||
print(colored(f"Error generating modified command: {e}", 'red'))
|
||||
else:
|
||||
# Modify the command directly
|
||||
print("Modify command: ", end='')
|
||||
# Pre-fill with current command
|
||||
import readline
|
||||
readline.set_startup_hook(lambda: readline.insert_text(command))
|
||||
try:
|
||||
modded_command = input()
|
||||
finally:
|
||||
readline.set_startup_hook()
|
||||
|
||||
if not modded_command.strip():
|
||||
print(colored("Empty command. Command execution cancelled.", "yellow"))
|
||||
return
|
||||
|
||||
# Print the modified command with styling
|
||||
print_command(modded_command, config)
|
||||
|
||||
# Ask for confirmation
|
||||
print("Execute modified command? [Y/n] ==> ", end='')
|
||||
confirm = input().lower()
|
||||
|
||||
if confirm in ["n", "no"]:
|
||||
print(colored("Command execution cancelled.", "yellow"))
|
||||
return
|
||||
|
||||
# Execute the modified command
|
||||
try:
|
||||
command_executor.execute_command(config.get("shell", os_utils.get_default_shell()), modded_command)
|
||||
except Exception as e:
|
||||
logger.error(f"Error executing modified command: {e}")
|
||||
print(colored(f"Error executing modified command: {e}", 'red'))
|
||||
|
||||
elif user_input.upper() == "C":
|
||||
if os_utils.missing_posix_display():
|
||||
return
|
||||
try:
|
||||
pyperclip.copy(command)
|
||||
print("Copied command to clipboard.")
|
||||
except Exception as e:
|
||||
logger.error(f"Error copying to clipboard: {e}")
|
||||
print(colored(f"Error copying to clipboard: {e}", 'red'))
|
||||
@@ -0,0 +1,340 @@
|
||||
"""
|
||||
Interactive shell for the Edison application.
|
||||
"""
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
from termcolor import colored
|
||||
from prompt_toolkit import PromptSession
|
||||
from prompt_toolkit.history import FileHistory
|
||||
from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
|
||||
from prompt_toolkit.lexers import PygmentsLexer
|
||||
from pygments.lexers.shell import BashLexer
|
||||
from edison.core import api_client, command_executor
|
||||
from edison.utils import os_utils, markdown_utils
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def handle_streaming_output_interactive(token):
|
||||
"""
|
||||
Handle a token from the streaming API response in interactive mode.
|
||||
|
||||
Args:
|
||||
token (str): A token from the streaming response.
|
||||
"""
|
||||
# Print the token without a newline and flush immediately
|
||||
print(token, end='', flush=True)
|
||||
|
||||
def get_command_explanation(client, command, structured=False):
|
||||
"""
|
||||
Get an explanation for a command.
|
||||
|
||||
Args:
|
||||
client: The OpenAI client.
|
||||
command (str): The command to explain.
|
||||
structured (bool): Whether to use structured explanation format.
|
||||
|
||||
Returns:
|
||||
str: The explanation of the command.
|
||||
"""
|
||||
try:
|
||||
system_prompt = """Explain this shell command in simple terms, breaking down each part:"""
|
||||
|
||||
if structured:
|
||||
system_prompt = """
|
||||
Explain this shell command in a structured format with these sections:
|
||||
|
||||
## Overview
|
||||
A brief overview of what the command does
|
||||
|
||||
## Command Breakdown
|
||||
Break down each part of the command with explanations
|
||||
|
||||
## Options/Flags
|
||||
Explain any options or flags used
|
||||
|
||||
## Examples
|
||||
Provide 1-2 simple examples of variations
|
||||
|
||||
## Cautions
|
||||
Note any potential issues or cautions
|
||||
"""
|
||||
|
||||
response = client.chat.completions.create(
|
||||
model="gpt-4o-mini",
|
||||
messages=[
|
||||
{"role": "system", "content": system_prompt},
|
||||
{"role": "user", "content": command}
|
||||
]
|
||||
)
|
||||
return response.choices[0].message.content
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting command explanation: {e}")
|
||||
return "Could not generate explanation."
|
||||
|
||||
def interactive_mode(client, config):
|
||||
"""
|
||||
Start an interactive shell.
|
||||
|
||||
Args:
|
||||
client: The OpenAI client.
|
||||
config (dict): The configuration dictionary.
|
||||
"""
|
||||
# Create history file path
|
||||
history_path = os.path.expanduser("~/.edison_history")
|
||||
history_dir = os.path.dirname(history_path)
|
||||
os.makedirs(history_dir, exist_ok=True)
|
||||
|
||||
# Create prompt session
|
||||
history = FileHistory(history_path)
|
||||
session = PromptSession(
|
||||
history=history,
|
||||
auto_suggest=AutoSuggestFromHistory(),
|
||||
lexer=PygmentsLexer(BashLexer)
|
||||
)
|
||||
|
||||
# Print welcome message
|
||||
print(colored("Edison Interactive Shell", "blue", attrs=["bold"]))
|
||||
print("Type your queries in natural language.")
|
||||
print("Type '!help' for help, '!exit' or '!quit' to exit, or press Ctrl+D to exit.")
|
||||
|
||||
while True:
|
||||
try:
|
||||
# Get user input
|
||||
query = session.prompt("edison> ")
|
||||
|
||||
# Handle special commands
|
||||
if query.strip().lower() in ["!exit", "!quit", "exit", "quit"]:
|
||||
break
|
||||
elif query.strip() == "!help":
|
||||
show_help()
|
||||
continue
|
||||
elif not query.strip():
|
||||
continue
|
||||
|
||||
# Generate command
|
||||
try:
|
||||
# Use streaming if enabled in config
|
||||
if config.get("streaming", True):
|
||||
# Check if we should show the "Generating command: " prefix
|
||||
if config.get("show_generating_prefix", True):
|
||||
print(colored("Generating command: ", "yellow"), end='', flush=True)
|
||||
try:
|
||||
command = api_client.generate_command_streaming(
|
||||
client, config, query, handle_streaming_output_interactive
|
||||
)
|
||||
print() # Add a newline after streaming is complete
|
||||
except KeyboardInterrupt:
|
||||
print("\nCommand generation cancelled.")
|
||||
continue
|
||||
else:
|
||||
print(colored("Generating command...", "yellow"))
|
||||
command = api_client.generate_command(client, config, query)
|
||||
|
||||
# Print command with appropriate styling based on config
|
||||
ui_config = config.get("ui", {})
|
||||
if ui_config.get("rich_formatting", True):
|
||||
command_style = ui_config.get("command_style", "panel")
|
||||
theme = ui_config.get("theme", "monokai")
|
||||
shell = config.get("shell", "bash")
|
||||
|
||||
if command_style == "panel":
|
||||
# Use rich panel style
|
||||
from edison.utils.markdown_utils import print_command_rich
|
||||
print_command_rich(command, shell=shell, theme=theme)
|
||||
else:
|
||||
# Use simple highlighting
|
||||
print("Command: " + colored(command, 'blue'))
|
||||
else:
|
||||
# Use traditional styling
|
||||
print("Command: " + colored(command, 'blue'))
|
||||
|
||||
# Ask user what to do with the same prompt as non-interactive mode
|
||||
prompt_text = "Execute command? [Y]es [n]o [m]odify (prompt/command) [c]opy to clipboard [x]plain ==> "
|
||||
if os_utils.missing_posix_display():
|
||||
prompt_text = "Execute command? [Y]es [n]o [m]odify (prompt/command) [x]plain ==> "
|
||||
|
||||
action = session.prompt(prompt_text)
|
||||
|
||||
if action.lower() in ["y", "yes", "e", "execute", ""]:
|
||||
# Execute command
|
||||
shell = config.get("shell", os.environ.get("SHELL", "bash"))
|
||||
try:
|
||||
command_executor.execute_command(shell, command)
|
||||
except Exception as e:
|
||||
logger.error(f"Error executing command: {e}")
|
||||
print(colored(f"Error executing command: {e}", 'red'))
|
||||
elif action.lower() in ["m", "modify"]:
|
||||
# Ask whether to modify prompt or command
|
||||
mod_choice = session.prompt("Modify [p]rompt or [c]ommand? [c] ==> ")
|
||||
|
||||
if mod_choice.lower() in ["p", "prompt"]:
|
||||
# Modify the prompt (natural language description)
|
||||
new_prompt = session.prompt("Modified prompt: ")
|
||||
|
||||
if not new_prompt.strip():
|
||||
print(colored("Empty prompt. Command execution cancelled.", "yellow"))
|
||||
continue
|
||||
|
||||
# Generate new command from modified prompt
|
||||
print(colored("Generating new command from modified prompt...", "yellow"))
|
||||
|
||||
# Use streaming if enabled
|
||||
if config.get("streaming", True):
|
||||
# Check if we should show the "Generating command: " prefix
|
||||
if config.get("show_generating_prefix", True):
|
||||
print(colored("Generating command: ", "yellow"), end='', flush=True)
|
||||
try:
|
||||
new_command = api_client.generate_command_streaming(
|
||||
client, config, new_prompt, handle_streaming_output_interactive
|
||||
)
|
||||
print() # Add a newline after streaming is complete
|
||||
except KeyboardInterrupt:
|
||||
print("\nCommand generation cancelled.")
|
||||
continue
|
||||
else:
|
||||
new_command = api_client.generate_command(client, config, new_prompt)
|
||||
|
||||
# Print the new command with styling
|
||||
ui_config = config.get("ui", {})
|
||||
if ui_config.get("rich_formatting", True):
|
||||
command_style = ui_config.get("command_style", "panel")
|
||||
theme = ui_config.get("theme", "monokai")
|
||||
shell = config.get("shell", "bash")
|
||||
|
||||
if command_style == "panel":
|
||||
# Use rich panel style
|
||||
from edison.utils.markdown_utils import print_command_rich
|
||||
print_command_rich(new_command, shell=shell, theme=theme)
|
||||
else:
|
||||
# Use simple highlighting
|
||||
print("Command: " + colored(new_command, 'blue'))
|
||||
else:
|
||||
# Use traditional styling
|
||||
print("Command: " + colored(new_command, 'blue'))
|
||||
|
||||
# Ask for confirmation
|
||||
confirm = session.prompt("Execute this command? [Y/n] ")
|
||||
if confirm.lower() in ["n", "no"]:
|
||||
print(colored("Command execution cancelled.", "yellow"))
|
||||
continue
|
||||
|
||||
# Execute the new command
|
||||
shell = config.get("shell", os.environ.get("SHELL", "bash"))
|
||||
try:
|
||||
command_executor.execute_command(shell, new_command)
|
||||
except Exception as e:
|
||||
logger.error(f"Error executing command: {e}")
|
||||
print(colored(f"Error executing command: {e}", 'red'))
|
||||
else:
|
||||
# Modify the command directly
|
||||
modified_command = session.prompt("Modified command: ", default=command)
|
||||
|
||||
if not modified_command.strip():
|
||||
print(colored("Empty command. Command execution cancelled.", "yellow"))
|
||||
continue
|
||||
|
||||
# Print the modified command with styling
|
||||
ui_config = config.get("ui", {})
|
||||
if ui_config.get("rich_formatting", True):
|
||||
command_style = ui_config.get("command_style", "panel")
|
||||
theme = ui_config.get("theme", "monokai")
|
||||
shell = config.get("shell", "bash")
|
||||
|
||||
if command_style == "panel":
|
||||
# Use rich panel style
|
||||
from edison.utils.markdown_utils import print_command_rich
|
||||
print_command_rich(modified_command, shell=shell, theme=theme)
|
||||
else:
|
||||
# Use simple highlighting
|
||||
print("Command: " + colored(modified_command, 'blue'))
|
||||
else:
|
||||
# Use traditional styling
|
||||
print("Command: " + colored(modified_command, 'blue'))
|
||||
|
||||
# Ask for confirmation
|
||||
confirm = session.prompt("Execute modified command? [Y/n] ")
|
||||
if confirm.lower() in ["n", "no"]:
|
||||
print(colored("Command execution cancelled.", "yellow"))
|
||||
continue
|
||||
|
||||
# Execute the modified command
|
||||
shell = config.get("shell", os.environ.get("SHELL", "bash"))
|
||||
try:
|
||||
command_executor.execute_command(shell, modified_command)
|
||||
except Exception as e:
|
||||
logger.error(f"Error executing command: {e}")
|
||||
print(colored(f"Error executing command: {e}", 'red'))
|
||||
elif action.lower() in ["c", "copy"]:
|
||||
# Copy command to clipboard
|
||||
if not os_utils.missing_posix_display():
|
||||
try:
|
||||
import pyperclip
|
||||
pyperclip.copy(command)
|
||||
print(colored("Command copied to clipboard.", "green"))
|
||||
except Exception as e:
|
||||
print(colored(f"Error copying to clipboard: {e}", "red"))
|
||||
elif action.lower() in ["x", "explain"]:
|
||||
# Explain command
|
||||
print(colored("Generating explanation...", "yellow"))
|
||||
|
||||
# Get UI configuration
|
||||
ui_config = config.get("ui", {})
|
||||
structured = ui_config.get("structured_explanations", True)
|
||||
use_rich = ui_config.get("rich_formatting", True)
|
||||
|
||||
# Generate explanation
|
||||
explanation = get_command_explanation(client, command, structured=structured)
|
||||
|
||||
if use_rich:
|
||||
print() # Add a blank line before the explanation
|
||||
# Format the explanation with rich formatting
|
||||
formatted_explanation = markdown_utils.format_command_explanation(explanation, use_rich=True)
|
||||
print(formatted_explanation)
|
||||
else:
|
||||
print(colored("\nExplanation:", "green", attrs=["bold"]))
|
||||
# Format the explanation with terminal-friendly markdown
|
||||
formatted_explanation = markdown_utils.format_command_explanation(explanation)
|
||||
print(formatted_explanation)
|
||||
elif action.lower() in ["n", "no", "s", "skip"]:
|
||||
# Skip
|
||||
print(colored("Command skipped.", "yellow"))
|
||||
else:
|
||||
# Skip if input not recognized
|
||||
print(colored("Command skipped - input not recognized.", "yellow"))
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error: {e}")
|
||||
print(colored(f"Error: {e}", "red"))
|
||||
|
||||
except KeyboardInterrupt:
|
||||
continue
|
||||
except EOFError:
|
||||
break
|
||||
|
||||
print("Goodbye!")
|
||||
|
||||
def show_help():
|
||||
"""
|
||||
Show help information.
|
||||
"""
|
||||
help_text = """
|
||||
Edison Interactive Shell Help
|
||||
|
||||
Commands:
|
||||
!help - Show this help message
|
||||
!exit, !quit - Exit the interactive shell
|
||||
exit, quit - Exit the interactive shell
|
||||
Ctrl+D - Exit the interactive shell
|
||||
|
||||
Actions:
|
||||
y, yes, <enter> - Execute the generated command
|
||||
n, no - Skip the command
|
||||
m, modify - Modify the prompt or command before executing
|
||||
When selected, you'll be asked whether to modify:
|
||||
- [p]rompt: Change the natural language description
|
||||
- [c]ommand: Directly edit the generated command
|
||||
c, copy - Copy the command to clipboard
|
||||
x, explain - Get an explanation of the command
|
||||
"""
|
||||
print(help_text)
|
||||
@@ -0,0 +1,4 @@
|
||||
"""
|
||||
Utility functions for the Edison application.
|
||||
"""
|
||||
from . import markdown_utils
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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")
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user