Compare commits

...
4 Commits
Author SHA1 Message Date
Mervin Praison 53704f234c Release v2.3.49 2025-12-19 12:49:12 +00:00
Mervin Praison b575994d91 docs(readme): update AI commit CLI with security scanning and auto mode
- Add full auto mode (-a) with stage, security check, commit, push
- Document security scanning features
- Add --no-verify flag documentation
- List detected sensitive patterns
2025-12-19 12:35:49 +00:00
Mervin Praison 7ed8596261 Release v2.3.48 2025-12-19 12:35:02 +00:00
Mervin Praison deef65c4bb Release v2.3.47 2025-12-19 12:23:05 +00:00
9 changed files with 432 additions and 69 deletions

No files matched your search

+20 -2
View File
@@ -1446,13 +1446,31 @@ praisonai mcp help
### AI Commit CLI:
```bash
# Generate AI commit message for staged changes
# Full auto mode: stage all, security check, commit, and push
praisonai commit -a
# Interactive mode (requires git add first)
praisonai commit
# Generate, commit, and push
# Interactive with auto-push
praisonai commit --push
# Skip security check (not recommended)
praisonai commit -a --no-verify
```
**Features:**
- 🤖 AI-generated conventional commit messages
- 🔒 Built-in security scanning (API keys, passwords, secrets, sensitive files)
- 📦 Auto-staging with `-a` flag
- 🚀 Auto-push in full auto mode
- ✏️ Edit message before commit in interactive mode
**Security Detection:**
- API keys, secrets, tokens (AWS, GitHub, GitLab, Slack)
- Passwords and private keys
- Sensitive files (`.env`, `id_rsa`, `.pem`, `.key`, etc.)
### Serve CLI (API Server):
```bash
# Start API server for agents defined in YAML
+1 -1
View File
@@ -16,7 +16,7 @@ RUN mkdir -p /root/.praison
# Install Python packages (using latest versions)
RUN pip install --no-cache-dir \
praisonai_tools \
"praisonai>=2.3.46" \
"praisonai>=2.3.49" \
"praisonai[chat]" \
"embedchain[github,youtube]"
+1 -1
View File
@@ -20,7 +20,7 @@ RUN mkdir -p /root/.praison
# Install Python packages (using latest versions)
RUN pip install --no-cache-dir \
praisonai_tools \
"praisonai>=2.3.46" \
"praisonai>=2.3.49" \
"praisonai[ui]" \
"praisonai[chat]" \
"praisonai[realtime]" \
+1 -1
View File
@@ -16,7 +16,7 @@ RUN mkdir -p /root/.praison
# Install Python packages (using latest versions)
RUN pip install --no-cache-dir \
praisonai_tools \
"praisonai>=2.3.46" \
"praisonai>=2.3.49" \
"praisonai[ui]" \
"praisonai[crewai]"
+20 -2
View File
@@ -1446,13 +1446,31 @@ praisonai mcp help
### AI Commit CLI:
```bash
# Generate AI commit message for staged changes
# Full auto mode: stage all, security check, commit, and push
praisonai commit -a
# Interactive mode (requires git add first)
praisonai commit
# Generate, commit, and push
# Interactive with auto-push
praisonai commit --push
# Skip security check (not recommended)
praisonai commit -a --no-verify
```
**Features:**
- 🤖 AI-generated conventional commit messages
- 🔒 Built-in security scanning (API keys, passwords, secrets, sensitive files)
- 📦 Auto-staging with `-a` flag
- 🚀 Auto-push in full auto mode
- ✏️ Edit message before commit in interactive mode
**Security Detection:**
- API keys, secrets, tokens (AWS, GitHub, GitLab, Slack)
- Passwords and private keys
- Sensitive files (`.env`, `id_rsa`, `.pem`, `.key`, etc.)
### Serve CLI (API Server):
```bash
# Start API server for agents defined in YAML
+2 -2
View File
@@ -3,8 +3,8 @@ class Praisonai < Formula
desc "AI tools for various AI applications"
homepage "https://github.com/MervinPraison/PraisonAI"
url "https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.3.46.tar.gz"
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.3.46.tar.gz | shasum -a 256`.split.first
url "https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.3.49.tar.gz"
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.3.49.tar.gz | shasum -a 256`.split.first
license "MIT"
depends_on "[email protected]"
+385 -58
View File
@@ -126,6 +126,7 @@ class PraisonAI:
Initialize the PraisonAI object with default parameters.
"""
self.agent_yaml = agent_yaml
self._interactive_mode = False # Flag for interactive TUI mode
# Create config_list with AutoGen compatibility
# Support multiple environment variable patterns for better compatibility
# Priority order: MODEL_NAME > OPENAI_MODEL_NAME for model selection
@@ -213,6 +214,12 @@ class PraisonAI:
if getattr(args, 'interactive', False):
self._start_interactive_mode(args)
return None
# Handle --chat-mode flag - run single prompt in interactive style (non-interactive)
if getattr(args, 'chat_mode', False):
prompt = args.command or getattr(args, 'direct_prompt', None)
if prompt:
return self._run_chat_mode(prompt, args)
self.framework = args.framework or self.framework
@@ -697,6 +704,7 @@ class PraisonAI:
# Interactive TUI - terminal interface with slash commands
parser.add_argument("--interactive", "-i", action="store_true", help="Start interactive terminal mode with slash commands")
parser.add_argument("--chat-mode", action="store_true", dest="chat_mode", help="Run single prompt in interactive style (non-interactive, for testing)")
# Autonomy Mode - control AI action approval
parser.add_argument("--autonomy", type=str, choices=["suggest", "auto_edit", "full_auto"], help="Set autonomy mode for AI actions")
@@ -2435,6 +2443,90 @@ class PraisonAI:
except Exception as e:
print(f"[red]ERROR: MCP command failed: {e}[/red]")
# Compiled regex patterns for sensitive data detection (compiled once, zero runtime cost)
_SENSITIVE_PATTERNS = None
@classmethod
def _get_sensitive_patterns(cls):
"""Lazy-load and compile sensitive patterns only when needed."""
if cls._SENSITIVE_PATTERNS is None:
import re
cls._SENSITIVE_PATTERNS = [
# API Keys and Tokens
(re.compile(r'(?i)(api[_-]?key|apikey)\s*[=:]\s*["\']?[a-zA-Z0-9_\-]{20,}'), "API Key"),
(re.compile(r'(?i)(secret[_-]?key|secretkey)\s*[=:]\s*["\']?[a-zA-Z0-9_\-]{20,}'), "Secret Key"),
(re.compile(r'(?i)(access[_-]?token|accesstoken)\s*[=:]\s*["\']?[a-zA-Z0-9_\-]{20,}'), "Access Token"),
(re.compile(r'(?i)(auth[_-]?token|authtoken)\s*[=:]\s*["\']?[a-zA-Z0-9_\-]{20,}'), "Auth Token"),
# AWS
(re.compile(r'AKIA[0-9A-Z]{16}'), "AWS Access Key ID"),
(re.compile(r'(?i)aws[_-]?secret[_-]?access[_-]?key\s*[=:]\s*["\']?[a-zA-Z0-9/+=]{40}'), "AWS Secret Key"),
# Passwords
(re.compile(r'(?i)(password|passwd|pwd)\s*[=:]\s*["\']?[^\s"\']{8,}'), "Password"),
(re.compile(r'(?i)db[_-]?password\s*[=:]\s*["\']?[^\s"\']+'), "Database Password"),
# Private Keys
(re.compile(r'-----BEGIN (RSA |DSA |EC |OPENSSH )?PRIVATE KEY-----'), "Private Key"),
(re.compile(r'-----BEGIN PGP PRIVATE KEY BLOCK-----'), "PGP Private Key"),
# GitHub/GitLab tokens
(re.compile(r'ghp_[a-zA-Z0-9]{36}'), "GitHub Personal Access Token"),
(re.compile(r'gho_[a-zA-Z0-9]{36}'), "GitHub OAuth Token"),
(re.compile(r'glpat-[a-zA-Z0-9\-]{20,}'), "GitLab Personal Access Token"),
# Slack
(re.compile(r'xox[baprs]-[0-9]{10,13}-[0-9]{10,13}-[a-zA-Z0-9]{24}'), "Slack Token"),
# Generic secrets
(re.compile(r'(?i)(client[_-]?secret)\s*[=:]\s*["\']?[a-zA-Z0-9_\-]{20,}'), "Client Secret"),
(re.compile(r'(?i)(private[_-]?key)\s*[=:]\s*["\']?[a-zA-Z0-9_\-/+=]{20,}'), "Private Key Value"),
]
return cls._SENSITIVE_PATTERNS
# Sensitive file patterns (simple string matching - very fast)
_SENSITIVE_FILES = {'.env', '.env.local', '.env.production', '.env.development',
'id_rsa', 'id_dsa', 'id_ecdsa', 'id_ed25519',
'.pem', '.key', '.p12', '.pfx', 'credentials', 'secrets.json',
'secrets.yaml', 'secrets.yml', '.htpasswd', '.netrc'}
_SENSITIVE_EXTENSIONS = {'.pem', '.key', '.p12', '.pfx', '.jks', '.keystore'}
def _check_sensitive_content(self, diff_content: str, staged_files: list) -> list:
"""
Check for sensitive content in staged changes.
Args:
diff_content: The git diff content
staged_files: List of staged file names
Returns:
List of (file, issue_type, match) tuples for detected issues
"""
issues = []
# Quick check for sensitive files by name/extension
for file_path in staged_files:
file_name = file_path.split('/')[-1].lower()
# Check exact file names
if file_name in self._SENSITIVE_FILES:
issues.append((file_path, "Sensitive File", file_name))
continue
# Check extensions
for ext in self._SENSITIVE_EXTENSIONS:
if file_name.endswith(ext):
issues.append((file_path, "Sensitive Extension", ext))
break
# Only scan diff content if it's not too large (performance guard)
if len(diff_content) < 50000:
patterns = self._get_sensitive_patterns()
# Scan only added lines (lines starting with +)
for line in diff_content.split('\n'):
if line.startswith('+') and not line.startswith('+++'):
for pattern, issue_type in patterns:
match = pattern.search(line)
if match:
# Truncate match for display
matched_text = match.group(0)[:50] + '...' if len(match.group(0)) > 50 else match.group(0)
issues.append(("diff", issue_type, matched_text))
break # One issue per line is enough
return issues
def handle_commit_command(self, args: list):
"""
Handle AI commit message generation.
@@ -2442,7 +2534,10 @@ class PraisonAI:
Generates a commit message based on staged changes using AI.
Args:
args: Additional arguments (e.g., --push to auto-push)
args: Additional arguments:
--push: Auto-push after commit
-a, --auto: Full auto mode (stage, commit, push) - aborts on security issues
--no-verify: Skip sensitive content check
"""
try:
import subprocess
@@ -2456,6 +2551,12 @@ class PraisonAI:
print("[red]ERROR: Not in a git repository[/red]")
return
# Handle auto mode
auto_mode = '-a' in args or '--auto' in args
if auto_mode:
print("[cyan]Auto-staging all changes...[/cyan]")
subprocess.run(["git", "add", "-A"], capture_output=True)
# Get staged diff
result = subprocess.run(
["git", "diff", "--cached", "--stat"],
@@ -2464,7 +2565,7 @@ class PraisonAI:
)
if not result.stdout.strip():
print("[yellow]No staged changes. Use 'git add' to stage files first.[/yellow]")
print("[yellow]No staged changes. Use 'git add' to stage files first, or use -a/--auto.[/yellow]")
return
# Get detailed diff for context
@@ -2477,6 +2578,46 @@ class PraisonAI:
# Limit diff size for context
diff_content = diff_result.stdout[:8000] if len(diff_result.stdout) > 8000 else diff_result.stdout
# Security check for sensitive content (unless --no-verify)
if '--no-verify' not in args:
# Extract staged file names from stat output
staged_files = [line.split('|')[0].strip() for line in result.stdout.strip().split('\n') if '|' in line]
issues = self._check_sensitive_content(diff_result.stdout, staged_files)
if issues:
print("\n[bold red]⚠️ SECURITY WARNING: Sensitive content detected![/bold red]")
for file_path, issue_type, match in issues:
print(f" [red]• {issue_type}[/red] in [yellow]{file_path}[/yellow]: {match}")
print("\n[yellow]Options:[/yellow]")
print(" [c] Continue anyway (not recommended)")
print(" [a] Abort commit")
print(" [i] Ignore and add to .gitignore")
# In auto mode, abort on security issues
if auto_mode:
print("[red]Auto mode aborted due to security concerns. Use --no-verify to skip.[/red]")
return
sec_choice = input("\nYour choice [c/a/i]: ").strip().lower()
if sec_choice == 'a':
print("[yellow]Commit aborted due to security concerns.[/yellow]")
return
elif sec_choice == 'i':
# Add sensitive files to .gitignore
sensitive_file_paths = [f for f, t, _ in issues if t in ("Sensitive File", "Sensitive Extension")]
if sensitive_file_paths:
with open('.gitignore', 'a') as gi:
gi.write('\n# Auto-added by praisonai commit\n')
for fp in sensitive_file_paths:
gi.write(f'{fp}\n')
print(f"[green]Added {len(sensitive_file_paths)} file(s) to .gitignore[/green]")
# Unstage the sensitive files
subprocess.run(["git", "reset", "HEAD", "--"] + sensitive_file_paths, capture_output=True)
print("[cyan]Unstaged sensitive files. Please re-run commit.[/cyan]")
return
# else continue
print("[yellow]Proceeding despite security warnings...[/yellow]")
print("[bold]Staged changes:[/bold]")
print(result.stdout)
print("\n[bold]Generating commit message...[/bold]")
@@ -2522,6 +2663,14 @@ Provide ONLY the commit message, no explanations."""
print("\n[bold green]Suggested commit message:[/bold green]")
print(f"[cyan]{commit_message}[/cyan]")
# In auto mode, skip confirmation and commit + push
if auto_mode:
subprocess.run(["git", "commit", "-m", commit_message], check=True)
print("[green]✅ Committed successfully![/green]")
subprocess.run(["git", "push"], check=True)
print("[green]✅ Pushed to remote![/green]")
return
# Ask for confirmation
print("\n[bold]Options:[/bold]")
print(" [y] Use this message and commit")
@@ -2766,6 +2915,10 @@ Provide ONLY the commit message, no explanations."""
"backstory": "You are a helpful AI assistant"
}
# Suppress verbose output in interactive mode (no Agent Info, Task, Response panels)
if getattr(self, '_interactive_mode', False):
agent_config["verbose"] = False
# Add llm if specified
if hasattr(self, 'args') and self.args.llm:
# Check if max_tokens is specified - pass as dict config
@@ -3620,73 +3773,247 @@ Now, {final_instruction.lower()}:"""
sys.exit(1)
def _start_interactive_mode(self, args):
"""Start interactive TUI mode with slash commands."""
"""
Start interactive TUI mode with streaming responses and tool support.
UX inspired by Gemini CLI, Codex CLI, and Claude Code:
- Streaming text output (no boxes)
- Tool status indicators
- Built-in tools (file ops, shell, web search)
- @ mentions for file inclusion
"""
try:
from praisonai.cli.features import InteractiveTUIHandler, SlashCommandHandler, CostTrackerHandler
from praisonai.cli.features.interactive_tui import InteractiveConfig
from rich.console import Console
# Initialize handlers
slash_handler = SlashCommandHandler()
cost_handler = CostTrackerHandler()
cost_handler.initialize()
console = Console()
# Configure interactive session
config = InteractiveConfig(
prompt="praisonai> ",
multiline=False,
enable_completions=True,
show_status_bar=True
)
# Set interactive mode flag
self._interactive_mode = True
# Define input handler
def on_input(text):
"""Handle regular input - send to agent."""
if not text.strip():
return ""
# Track cost if enabled
result = self.handle_direct_prompt(text)
return result
# Load interactive tools
tools_list = self._load_interactive_tools()
# Define command handler
def on_command(cmd):
"""Handle slash commands."""
result = slash_handler.execute(cmd)
if result.get("type") == "exit":
return result
# Handle special commands
if result.get("type") == "cost":
return {"type": "info", "message": cost_handler.get_summary()}
return result
# Initialize TUI handler
tui_handler = InteractiveTUIHandler()
session = tui_handler.initialize(
config=config,
on_input=on_input,
on_command=on_command
)
# Add slash commands for completion
session.add_commands(["help", "exit", "quit", "cost", "tokens", "model", "diff", "commit", "undo", "plan", "map", "clear", "settings"])
print("[bold green]PraisonAI Interactive Mode[/bold green]")
print("Type your prompt or use /help for commands. /exit to quit.\n")
# Run the interactive session
tui_handler.run()
# Print welcome message
console.print("\n[bold cyan]PraisonAI Interactive Mode[/bold cyan]")
console.print("[dim]Type your prompt, use /help for commands, /exit to quit[/dim]")
console.print("[dim]Tools: file read/write, shell, web search[/dim]\n")
running = True
while running:
try:
# Get user input
user_input = input("❯ ").strip()
if not user_input:
continue
# Handle slash commands
if user_input.startswith("/"):
cmd = user_input[1:].lower().split()[0] if user_input[1:] else ""
if cmd in ["exit", "quit", "q"]:
console.print("[dim]Goodbye![/dim]")
running = False
continue
elif cmd == "help":
self._print_interactive_help(console)
continue
elif cmd == "clear":
console.clear()
continue
elif cmd == "tools":
console.print(f"[cyan]Available tools: {len(tools_list)}[/cyan]")
for tool in tools_list:
name = getattr(tool, '__name__', str(tool))
console.print(f" • {name}")
continue
else:
console.print(f"[yellow]Unknown command: /{cmd}. Type /help for available commands.[/yellow]")
continue
# Process the prompt with streaming
self._process_interactive_prompt(user_input, tools_list, console)
except KeyboardInterrupt:
console.print("\n[dim]Use /exit to quit[/dim]")
except EOFError:
running = False
except ImportError as e:
print(f"[red]ERROR: Interactive mode requires additional dependencies: {e}[/red]")
print("Install with: pip install prompt_toolkit")
print(f"[red]ERROR: Interactive mode requires rich: {e}[/red]")
print("Install with: pip install rich")
sys.exit(1)
except KeyboardInterrupt:
print("\n[yellow]Exiting interactive mode.[/yellow]")
except Exception as e:
print(f"[red]ERROR: Interactive mode failed: {e}[/red]")
import traceback
traceback.print_exc()
sys.exit(1)
def _load_interactive_tools(self):
"""Load tools for interactive mode."""
tools_list = []
try:
from praisonaiagents.tools import (
read_file as tool_read_file,
write_file as tool_write_file,
list_files as tool_list_files,
execute_command,
internet_search
)
tools_list = [tool_read_file, tool_write_file, tool_list_files, execute_command, internet_search]
except ImportError:
# Try individual imports
try:
from praisonaiagents.tools import read_file as tool_read_file
tools_list.append(tool_read_file)
except ImportError:
pass
try:
from praisonaiagents.tools import write_file as tool_write_file
tools_list.append(tool_write_file)
except ImportError:
pass
try:
from praisonaiagents.tools import list_files as tool_list_files
tools_list.append(tool_list_files)
except ImportError:
pass
try:
from praisonaiagents.tools import execute_command
tools_list.append(execute_command)
except ImportError:
pass
try:
from praisonaiagents.tools import internet_search
tools_list.append(internet_search)
except ImportError:
pass
return tools_list
def _print_interactive_help(self, console):
"""Print help for interactive mode."""
console.print("\n[bold]Commands:[/bold]")
console.print(" /help - Show this help")
console.print(" /exit - Exit interactive mode")
console.print(" /clear - Clear screen")
console.print(" /tools - List available tools")
console.print("\n[bold]Features:[/bold]")
console.print(" • Streaming responses")
console.print(" • File operations (read, write, list)")
console.print(" • Shell command execution")
console.print(" • Web search")
console.print(" • @file:path to include file content")
console.print("")
def _run_chat_mode(self, prompt, args):
"""
Run a single prompt in interactive style (non-interactive mode for testing).
Usage: praisonai "your prompt" --chat
This runs the prompt using the same agent/tools as interactive mode
but exits after one response (useful for testing and scripting).
"""
from rich.console import Console
console = Console()
self._interactive_mode = True # Use interactive mode settings
# Load tools
tools_list = self._load_interactive_tools()
console.print(f"[dim]Chat mode: {len(tools_list)} tools available[/dim]")
console.print(f"[dim]Prompt: {prompt[:50]}{'...' if len(prompt) > 50 else ''}[/dim]\n")
# Process the prompt
self._process_interactive_prompt(prompt, tools_list, console)
return None
def _process_interactive_prompt(self, prompt, tools_list, console):
"""Process a prompt in interactive mode with streaming."""
from rich.live import Live
from rich.spinner import Spinner
import sys
import warnings
import logging
# Store original log levels to restore later (no global impact)
original_levels = {}
loggers_to_suppress = ["httpx", "httpcore", "duckduckgo_search", "crawl4ai", "lib"]
for logger_name in loggers_to_suppress:
logger = logging.getLogger(logger_name)
original_levels[logger_name] = logger.level
logger.setLevel(logging.WARNING)
# Temporarily filter warnings (scoped to this function)
with warnings.catch_warnings():
warnings.filterwarnings("ignore", message=".*duckduckgo_search.*")
warnings.filterwarnings("ignore", message=".*has been renamed.*")
try:
# Import agent
from praisonaiagents import Agent
# Custom callback to show tool usage
tool_calls_made = []
def on_tool_start(tool_name, tool_input):
"""Called when a tool starts executing."""
tool_calls_made.append(tool_name)
console.print(f"[dim]⚙ Using {tool_name}...[/dim]", end="\r")
def on_tool_end(tool_name, tool_output):
"""Called when a tool finishes."""
console.print(f"[dim]✓ {tool_name} complete[/dim] ")
# Show thinking indicator
with Live(Spinner("dots", text="Thinking...", style="cyan"), console=console, refresh_per_second=10, transient=True):
# Create agent with tools
agent = Agent(
name="Assistant",
role="Helpful AI Assistant",
goal="Help the user with their tasks",
backstory="You are a helpful AI assistant with access to tools for file operations, shell commands, and web search. Use tools when needed to complete tasks.",
tools=tools_list if tools_list else None,
verbose=False, # Suppress verbose panels
llm=getattr(self.args, 'llm', None) if hasattr(self, 'args') else None
)
# Get response with streaming
console.print() # New line before response
# Try to use streaming if available
try:
# Use stream method for real streaming
response_text = ""
for chunk in agent.stream(prompt):
if chunk:
chunk_str = str(chunk)
console.print(chunk_str, end="")
sys.stdout.flush()
response_text += chunk_str
console.print("\n") # Final newline
except (AttributeError, TypeError):
# Fallback to chat method with simulated streaming
response = agent.chat(prompt)
# Print response with word-by-word streaming effect
if response:
words = str(response).split()
for i, word in enumerate(words):
console.print(word + " ", end="")
sys.stdout.flush()
if i % 10 == 9: # Small pause every 10 words
import time
time.sleep(0.01)
console.print("\n") # Final newline
except Exception as e:
console.print(f"\n[red]Error: {e}[/red]")
finally:
# Restore original log levels (no global impact on main package)
for logger_name, level in original_levels.items():
logging.getLogger(logger_name).setLevel(level)
if __name__ == "__main__":
praison_ai = PraisonAI()
+1 -1
View File
@@ -57,7 +57,7 @@ class CloudDeployer:
file.write("FROM python:3.11-slim\n")
file.write("WORKDIR /app\n")
file.write("COPY . .\n")
file.write("RUN pip install flask praisonai==2.3.46 gunicorn markdown\n")
file.write("RUN pip install flask praisonai==2.3.49 gunicorn markdown\n")
file.write("EXPOSE 8080\n")
file.write('CMD ["gunicorn", "-b", "0.0.0.0:8080", "api:app"]\n')
+1 -1
View File
@@ -1 +1 @@
__version__ = "2.3.46"
__version__ = "2.3.49"