hashcli 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
hashcli/__init__.py ADDED
@@ -0,0 +1,29 @@
1
+ """Hash (HAcker SHell) - Intelligent CLI system with dual-mode functionality.
2
+
3
+ This package provides a modern CLI that combines LLM conversational assistance
4
+ with command proxy functionality, operating in two distinct modes:
5
+
6
+ - LLM Chat Mode: Natural language queries for intelligent assistance
7
+ - Command Proxy Mode: Slash-prefixed commands for direct functionality
8
+
9
+ The system is designed for cross-platform compatibility and extensibility,
10
+ supporting multiple LLM providers and built-in command extensions.
11
+ """
12
+
13
+ from .command_proxy import CommandProxy
14
+ from .config import HashConfig
15
+ from .history import ConversationHistory
16
+ from .llm_handler import LLMHandler
17
+ from .main import app
18
+
19
+ __version__ = "0.1.0"
20
+ __author__ = "Hash CLI Team"
21
+ __email__ = "team@hashcli.dev"
22
+
23
+ __all__ = [
24
+ "app",
25
+ "HashConfig",
26
+ "LLMHandler",
27
+ "CommandProxy",
28
+ "ConversationHistory",
29
+ ]
@@ -0,0 +1,240 @@
1
+ """Command proxy system for handling slash-prefixed commands."""
2
+
3
+ import os
4
+ import platform
5
+ import shlex
6
+ import subprocess
7
+ from abc import ABC, abstractmethod
8
+ from typing import Any, Dict, List, Optional
9
+
10
+ from rich.console import Console
11
+
12
+ from .config import HashConfig
13
+
14
+ console = Console()
15
+
16
+
17
+ class Command(ABC):
18
+ """Abstract base class for all commands."""
19
+
20
+ @abstractmethod
21
+ def execute(self, args: List[str], config: HashConfig) -> str:
22
+ """Execute the command with given arguments."""
23
+ pass
24
+
25
+ @abstractmethod
26
+ def get_help(self) -> str:
27
+ """Get help text for this command."""
28
+ pass
29
+
30
+ def validate_args(self, args: List[str]) -> bool:
31
+ """Validate command arguments. Override if needed."""
32
+ return True
33
+
34
+
35
+ class CommandProxy:
36
+ """Main command proxy that routes slash commands to their handlers."""
37
+
38
+ def __init__(self, config: HashConfig):
39
+ self.config = config
40
+ self.commands = self._register_commands()
41
+
42
+ def execute(self, command_line: str) -> str:
43
+ """Execute a slash command."""
44
+ # Remove leading slash and parse command
45
+ command_line = command_line.lstrip().lstrip("/")
46
+
47
+ if not command_line:
48
+ return "No command specified. Use /help for available commands."
49
+
50
+ # Parse command and arguments safely
51
+ try:
52
+ parts = shlex.split(command_line)
53
+ except ValueError as e:
54
+ return f"Error parsing command: {e}"
55
+
56
+ if not parts:
57
+ return "No command specified. Use /help for available commands."
58
+
59
+ cmd = parts[0]
60
+ args = parts[1:] if len(parts) > 1 else []
61
+
62
+ # Check if command exists
63
+ if cmd not in self.commands:
64
+ return f"Unknown command: /{cmd}\nUse /help for available commands."
65
+
66
+ # Get command handler
67
+ handler = self.commands[cmd]
68
+
69
+ # Validate arguments
70
+ if not handler.validate_args(args):
71
+ return f"Invalid arguments for /{cmd}\n{handler.get_help()}"
72
+
73
+ # Execute command
74
+ try:
75
+ return handler.execute(args, self.config)
76
+ except Exception as e:
77
+ if self.config.show_debug:
78
+ import traceback
79
+
80
+ return f"Command execution error: {e}\n{traceback.format_exc()}"
81
+ else:
82
+ return f"Command execution error: {e}"
83
+
84
+ def _register_commands(self) -> Dict[str, Command]:
85
+ """Register all available commands."""
86
+ from .commands import (
87
+ ClearCommand,
88
+ ConfigCommand,
89
+ FixCommand,
90
+ HelpCommand,
91
+ LSCommand,
92
+ ModelCommand,
93
+ )
94
+
95
+ return {
96
+ "ls": LSCommand(),
97
+ "dir": LSCommand(), # Windows alias
98
+ "clear": ClearCommand(),
99
+ "model": ModelCommand(),
100
+ "fix": FixCommand(),
101
+ "help": HelpCommand(),
102
+ "config": ConfigCommand(),
103
+ "history": HistoryCommand(),
104
+ "exit": ExitCommand(),
105
+ "quit": ExitCommand(),
106
+ }
107
+
108
+ def get_available_commands(self) -> List[str]:
109
+ """Get list of available command names."""
110
+ return sorted(self.commands.keys())
111
+
112
+ def get_command_help(self, command: str) -> Optional[str]:
113
+ """Get help for a specific command."""
114
+ if command in self.commands:
115
+ return self.commands[command].get_help()
116
+ return None
117
+
118
+
119
+ class SystemCommand(Command):
120
+ """Base class for system commands that execute shell operations."""
121
+
122
+ def execute_system_command(self, cmd_args: List[str], config: HashConfig) -> str:
123
+ """Execute a system command with security checks."""
124
+
125
+ # Security check: validate command against blocked list
126
+ cmd_str = " ".join(cmd_args)
127
+ for blocked in config.blocked_commands:
128
+ if blocked.lower() in cmd_str.lower():
129
+ return f"Blocked command detected: {blocked}"
130
+
131
+ # Security check: validate against allowed list if configured
132
+ if config.allowed_commands:
133
+ base_cmd = cmd_args[0] if cmd_args else ""
134
+ if base_cmd not in config.allowed_commands:
135
+ return f"Command not in allowed list: {base_cmd}"
136
+
137
+ try:
138
+ # Execute command with timeout
139
+ result = subprocess.run(
140
+ cmd_args,
141
+ capture_output=True,
142
+ text=True,
143
+ timeout=config.command_timeout,
144
+ shell=False, # Never use shell=True for security
145
+ )
146
+
147
+ # Format output
148
+ output = ""
149
+ if result.stdout:
150
+ output += result.stdout
151
+ if result.stderr:
152
+ if output:
153
+ output += "\n"
154
+ output += f"stderr: {result.stderr}"
155
+
156
+ if result.returncode != 0 and not output:
157
+ output = f"Command failed with exit code {result.returncode}"
158
+
159
+ return output.strip()
160
+
161
+ except subprocess.TimeoutExpired:
162
+ return f"Command timed out after {config.command_timeout} seconds"
163
+ except subprocess.CalledProcessError as e:
164
+ return f"Command failed: {e}"
165
+ except FileNotFoundError:
166
+ return f"Command not found: {cmd_args[0] if cmd_args else 'unknown'}"
167
+ except Exception as e:
168
+ return f"Execution error: {e}"
169
+
170
+
171
+ # History command for conversation history management
172
+ class HistoryCommand(Command):
173
+ """Command to manage conversation history."""
174
+
175
+ def execute(self, args: List[str], config: HashConfig) -> str:
176
+ from .history import ConversationHistory
177
+
178
+ if not config.history_enabled:
179
+ return "History is disabled in configuration."
180
+
181
+ history = ConversationHistory(config.history_dir)
182
+
183
+ if not args or args[0] == "list":
184
+ # List recent conversations
185
+ sessions = history.list_sessions()
186
+ if not sessions:
187
+ return "No conversation history found."
188
+
189
+ output = "Recent conversations:\n"
190
+ for session in sessions[-10:]: # Show last 10
191
+ output += f" {session['id']}: {session['created']} ({session['message_count']} messages)\n"
192
+ return output.strip()
193
+
194
+ elif args[0] == "show" and len(args) > 1:
195
+ # Show specific conversation
196
+ session_id = args[1]
197
+ messages = history.get_session_messages(session_id)
198
+ if not messages:
199
+ return f"No messages found for session {session_id}"
200
+
201
+ output = f"Conversation {session_id}:\n\n"
202
+ for msg in messages:
203
+ role = msg["role"].upper()
204
+ content = (
205
+ msg["content"][:200] + "..."
206
+ if len(msg["content"]) > 200
207
+ else msg["content"]
208
+ )
209
+ output += f"[{role}] {content}\n\n"
210
+ return output.strip()
211
+
212
+ elif args[0] == "clear":
213
+ # Clear all history
214
+ if history.clear_all_history():
215
+ return "All conversation history cleared."
216
+ else:
217
+ return "Failed to clear history."
218
+
219
+ else:
220
+ return self.get_help()
221
+
222
+ def get_help(self) -> str:
223
+ return """Manage conversation history:
224
+ /history list - List recent conversations
225
+ /history show <id> - Show specific conversation
226
+ /history clear - Clear all history"""
227
+
228
+
229
+ # Exit command
230
+ class ExitCommand(Command):
231
+ """Command to exit the application."""
232
+
233
+ def execute(self, args: List[str], config: HashConfig) -> str:
234
+ import sys
235
+
236
+ console.print("[yellow]Goodbye![/yellow]")
237
+ sys.exit(0)
238
+
239
+ def get_help(self) -> str:
240
+ return "Exit the Hash CLI application."
@@ -0,0 +1,17 @@
1
+ """Built-in command implementations for command proxy mode."""
2
+
3
+ from .clear import ClearCommand
4
+ from .config import ConfigCommand
5
+ from .fix import FixCommand
6
+ from .help import HelpCommand
7
+ from .ls import LSCommand
8
+ from .model import ModelCommand
9
+
10
+ __all__ = [
11
+ "LSCommand",
12
+ "ClearCommand",
13
+ "ModelCommand",
14
+ "FixCommand",
15
+ "HelpCommand",
16
+ "ConfigCommand",
17
+ ]
@@ -0,0 +1,70 @@
1
+ """Clear command implementation for clearing conversation history."""
2
+
3
+ from typing import List
4
+
5
+ from ..command_proxy import Command
6
+ from ..config import HashConfig
7
+
8
+
9
+ class ClearCommand(Command):
10
+ """Command to clear conversation history."""
11
+
12
+ def execute(self, args: List[str], config: HashConfig) -> str:
13
+ """Clear conversation history."""
14
+ from ..history import ConversationHistory
15
+
16
+ if not config.history_enabled:
17
+ return "History is disabled in configuration."
18
+
19
+ # Parse arguments
20
+ clear_all = "--all" in args or "-a" in args
21
+
22
+ try:
23
+ history = ConversationHistory(config.history_dir)
24
+
25
+ if clear_all:
26
+ # Clear all history
27
+ success = history.clear_all_history()
28
+ if success:
29
+ return "All conversation history cleared successfully."
30
+ else:
31
+ return "Failed to clear conversation history."
32
+ else:
33
+ # Clear old history (default: 30 days)
34
+ days = 30
35
+
36
+ # Check for custom days argument
37
+ for i, arg in enumerate(args):
38
+ if arg == "--days" or arg == "-d":
39
+ if i + 1 < len(args):
40
+ try:
41
+ days = int(args[i + 1])
42
+ except ValueError:
43
+ return f"Invalid days value: {args[i + 1]}"
44
+ break
45
+
46
+ cleared_count = history.clear_old_history(days)
47
+ if cleared_count > 0:
48
+ return f"Cleared {cleared_count} old conversations (older than {days} days)."
49
+ else:
50
+ return f"No conversations older than {days} days found."
51
+
52
+ except Exception as e:
53
+ if config.show_debug:
54
+ import traceback
55
+
56
+ return f"Error clearing history: {e}\n{traceback.format_exc()}"
57
+ else:
58
+ return f"Error clearing history: {e}"
59
+
60
+ def get_help(self) -> str:
61
+ """Get help text for the clear command."""
62
+ return """Clear conversation history:
63
+ /clear - Clear conversations older than 30 days
64
+ /clear --days N - Clear conversations older than N days
65
+ /clear --all - Clear ALL conversation history
66
+
67
+ Examples:
68
+ /clear - Clear old conversations
69
+ /clear --days 7 - Clear conversations older than 7 days
70
+ /clear --all - Clear everything (cannot be undone)"""
@@ -0,0 +1,124 @@
1
+ """Config command implementation for configuration management."""
2
+
3
+ from typing import List
4
+
5
+ from ..command_proxy import Command
6
+ from ..config import HashConfig, save_config
7
+
8
+
9
+ class ConfigCommand(Command):
10
+ """Command to show and manage configuration."""
11
+
12
+ def execute(self, args: List[str], config: HashConfig) -> str:
13
+ """Show or manage configuration."""
14
+
15
+ if not args:
16
+ return self._show_config(config)
17
+
18
+ command = args[0].lower()
19
+
20
+ if command == "show":
21
+ return self._show_config(config)
22
+ elif command == "save":
23
+ return self._save_config(config)
24
+ elif command == "stats":
25
+ return self._show_stats(config)
26
+ else:
27
+ return f"Unknown config command: {command}\n{self.get_help()}"
28
+
29
+ def _show_config(self, config: HashConfig) -> str:
30
+ """Show current configuration."""
31
+ output = "Hash CLI Configuration:\n\n"
32
+
33
+ # LLM Configuration
34
+ output += "[bold blue]LLM Configuration:[/bold blue]\n"
35
+ output += f" Provider: {config.llm_provider.value}\n"
36
+ output += f" Model: {config.get_current_model()}\n"
37
+ output += f" API Key: {'✓ Set' if config.get_current_api_key() else '✗ Not set'}\n\n"
38
+
39
+ # Tool Configuration
40
+ output += "[bold blue]Tool Configuration:[/bold blue]\n"
41
+ output += f" Command execution: {'Enabled' if config.allow_command_execution else 'Disabled'}\n"
42
+ output += f" Confirmation required: {'Yes' if config.require_confirmation else 'No'}\n"
43
+ output += f" Command timeout: {config.command_timeout}s\n"
44
+ output += (
45
+ f" Sandbox commands: {'Yes' if config.sandbox_commands else 'No'}\n\n"
46
+ )
47
+
48
+ # History Configuration
49
+ output += "[bold blue]History Configuration:[/bold blue]\n"
50
+ output += f" History enabled: {'Yes' if config.history_enabled else 'No'}\n"
51
+ if config.history_enabled:
52
+ output += f" History directory: {config.history_dir}\n"
53
+ output += f" Max history size: {config.max_history_size}\n"
54
+ output += f" Retention days: {config.history_retention_days}\n"
55
+ output += "\n"
56
+
57
+ # Output Configuration
58
+ output += "[bold blue]Output Configuration:[/bold blue]\n"
59
+ output += f" Rich output: {'Yes' if config.rich_output else 'No'}\n"
60
+ output += f" Debug mode: {'Yes' if config.show_debug else 'No'}\n"
61
+ output += f" Log level: {config.log_level.value}\n\n"
62
+
63
+ # Security Configuration
64
+ output += "[bold blue]Security Configuration:[/bold blue]\n"
65
+ if config.allowed_commands:
66
+ output += f" Allowed commands: {', '.join(config.allowed_commands)}\n"
67
+ else:
68
+ output += f" Allowed commands: All (no whitelist)\n"
69
+ output += f" Blocked commands: {', '.join(config.blocked_commands)}\n"
70
+
71
+ return output.strip()
72
+
73
+ def _save_config(self, config: HashConfig) -> str:
74
+ """Save current configuration to file."""
75
+ try:
76
+ success = save_config(config)
77
+ if success:
78
+ config_path = config.history_dir.parent / "config.toml"
79
+ return f"Configuration saved to {config_path}"
80
+ else:
81
+ return "Failed to save configuration"
82
+ except Exception as e:
83
+ return f"Error saving configuration: {e}"
84
+
85
+ def _show_stats(self, config: HashConfig) -> str:
86
+ """Show usage statistics."""
87
+ if not config.history_enabled:
88
+ return "History is disabled - no statistics available."
89
+
90
+ try:
91
+ from ..history import ConversationHistory
92
+
93
+ history = ConversationHistory(config.history_dir)
94
+ stats = history.get_statistics()
95
+
96
+ output = "Hash CLI Usage Statistics:\n\n"
97
+ output += f"Total conversations: {stats['total_sessions']}\n"
98
+ output += f"Total messages: {stats['total_messages']}\n"
99
+ output += f"Recent conversations (7d): {stats['recent_sessions_7d']}\n"
100
+ output += f"Recent messages (7d): {stats['recent_messages_7d']}\n"
101
+ output += f"Database size: {stats['database_size_bytes'] / 1024:.1f} KB\\n"
102
+ output += f"Database location: {stats['database_path']}\\n"
103
+
104
+ if stats["total_sessions"] > 0:
105
+ avg_messages = stats["total_messages"] / stats["total_sessions"]
106
+ output += f"Average messages per conversation: {avg_messages:.1f}\\n"
107
+
108
+ return output
109
+
110
+ except Exception as e:
111
+ return f"Error getting statistics: {e}"
112
+
113
+ def get_help(self) -> str:
114
+ """Get help text for the config command."""
115
+ return """Show and manage configuration:
116
+ /config - Show current configuration
117
+ /config show - Show current configuration (same as above)
118
+ /config save - Save current config to file
119
+ /config stats - Show usage statistics
120
+
121
+ Examples:
122
+ /config - View all settings
123
+ /config save - Save to ~/.hashcli/config.toml
124
+ /config stats - See usage statistics"""
@@ -0,0 +1,54 @@
1
+ """Fix command implementation for coding assistance."""
2
+
3
+ from typing import List
4
+
5
+ from ..command_proxy import Command
6
+ from ..config import HashConfig
7
+
8
+
9
+ class FixCommand(Command):
10
+ """Command for coding-specialized assistance."""
11
+
12
+ def execute(self, args: List[str], config: HashConfig) -> str:
13
+ """Execute fix command for coding assistance."""
14
+
15
+ if not args:
16
+ return self.get_help()
17
+
18
+ # Join all arguments into a description
19
+ description = " ".join(args)
20
+
21
+ # Create a specialized prompt for coding assistance
22
+ coding_prompt = f"""I need help with a coding issue. Please provide a practical solution:
23
+
24
+ Issue: {description}
25
+
26
+ Please provide:
27
+ 1. A clear explanation of the problem
28
+ 2. A concrete solution with code examples if applicable
29
+ 3. Any relevant best practices or alternatives
30
+ 4. Commands to run if needed (I can execute them with your guidance)
31
+
32
+ Focus on being practical and actionable."""
33
+
34
+ # This would normally trigger LLM mode with the specialized prompt
35
+ # For now, return a message indicating the prompt would be processed
36
+ return f"Coding assistance request: '{description}'\n\nThis would normally trigger an LLM conversation with specialized coding context. In a full implementation, this would seamlessly switch to LLM mode with the enhanced prompt above."
37
+
38
+ def get_help(self) -> str:
39
+ """Get help text for the fix command."""
40
+ return """Get coding assistance for development issues:
41
+ /fix <description> - Get help with a coding problem
42
+
43
+ Examples:
44
+ /fix my python script has a syntax error
45
+ /fix how do I implement authentication in Express.js
46
+ /fix git merge conflict resolution
47
+ /fix optimize this slow database query
48
+ /fix unit test is failing with TypeError
49
+
50
+ This command provides specialized coding assistance with:
51
+ - Problem analysis and solutions
52
+ - Code examples and best practices
53
+ - Command suggestions for fixes
54
+ - Step-by-step guidance"""
@@ -0,0 +1,89 @@
1
+ """Help command implementation for showing available commands."""
2
+
3
+ from typing import List
4
+
5
+ from ..command_proxy import Command
6
+ from ..config import HashConfig
7
+
8
+
9
+ class HelpCommand(Command):
10
+ """Command to show help information."""
11
+
12
+ def execute(self, args: List[str], config: HashConfig) -> str:
13
+ """Show help information."""
14
+
15
+ if args and args[0] != "":
16
+ # Show help for specific command
17
+ return self._show_command_help(args[0])
18
+ else:
19
+ # Show general help
20
+ return self._show_general_help()
21
+
22
+ def _show_general_help(self) -> str:
23
+ """Show general help with all available commands."""
24
+ help_text = """Hash CLI - Intelligent Terminal Assistant
25
+
26
+ DUAL MODE OPERATION:
27
+ hashcli <natural language> - LLM chat mode for questions & assistance
28
+ hashcli /<command> - Command proxy mode for direct actions
29
+
30
+ AVAILABLE COMMANDS:
31
+ /ls [args] - List directory contents (cross-platform)
32
+ /clear [options] - Clear conversation history
33
+ /model [options] - Switch LLM models and providers
34
+ /fix <description> - Get coding assistance
35
+ /help [command] - Show help (this message)
36
+ /config - Show current configuration
37
+ /history [options] - Manage conversation history
38
+ /exit, /quit - Exit the application
39
+
40
+ EXAMPLES:
41
+ # LLM Mode:
42
+ hashcli how do I find large files?
43
+ hashcli explain this error: permission denied
44
+ hashcli help me optimize this Python script
45
+
46
+ # Command Mode:
47
+ hashcli /ls -la
48
+ hashcli /model set gpt-5-mini
49
+ hashcli /clear --days 7
50
+ hashcli /fix my tests are failing
51
+
52
+ GETTING STARTED:
53
+ 1. Set API key: export OPENAI_API_KEY="your-key"
54
+ 2. Try: hashcli hello world
55
+ 3. Or: hashcli /help model
56
+
57
+ For command-specific help: /help <command>"""
58
+
59
+ return help_text
60
+
61
+ def _show_command_help(self, command_name: str) -> str:
62
+ """Show help for a specific command."""
63
+ # Import here to avoid circular imports
64
+ from ..command_proxy import CommandProxy
65
+ from ..config import HashConfig
66
+
67
+ # Create a temporary config to access command registry
68
+ temp_config = HashConfig()
69
+ proxy = CommandProxy(temp_config)
70
+
71
+ # Get help for the specific command
72
+ command_help = proxy.get_command_help(command_name)
73
+
74
+ if command_help:
75
+ return f"Help for /{command_name}:\\n\\n{command_help}"
76
+ else:
77
+ available_commands = ", ".join(proxy.get_available_commands())
78
+ return f"Unknown command: /{command_name}\\n\\nAvailable commands: {available_commands}\\n\\nUse '/help' for full help."
79
+
80
+ def get_help(self) -> str:
81
+ """Get help text for the help command."""
82
+ return """Show help information:
83
+ /help - Show general help and all commands
84
+ /help <command> - Show help for specific command
85
+
86
+ Examples:
87
+ /help - Show this help
88
+ /help model - Show help for model command
89
+ /help ls - Show help for ls command"""