Last active
February 1, 2026 01:30
-
-
Save simonseo/1193b43f25c7305d9b795d71305afa7b to your computer and use it in GitHub Desktop.
whatcmd
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/bin/bash | |
| # Installation script for whatcmd | |
| set -e | |
| INSTALL_DIR="${INSTALL_DIR:-$HOME/bin}" | |
| SCRIPT_NAME="whatcmd" | |
| SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" | |
| echo "Installing whatcmd to $INSTALL_DIR..." | |
| # Create install directory if it doesn't exist | |
| mkdir -p "$INSTALL_DIR" | |
| # Copy the script | |
| cp "$SCRIPT_DIR/$SCRIPT_NAME" "$INSTALL_DIR/$SCRIPT_NAME" | |
| chmod +x "$INSTALL_DIR/$SCRIPT_NAME" | |
| echo "✓ Installed $SCRIPT_NAME to $INSTALL_DIR/$SCRIPT_NAME" | |
| # Check if install directory is in PATH | |
| if [[ ":$PATH:" != *":$INSTALL_DIR:"* ]]; then | |
| echo "" | |
| echo "⚠️ Warning: $INSTALL_DIR is not in your PATH" | |
| echo "" | |
| echo "Add the following line to your ~/.zshrc or ~/.bashrc:" | |
| echo "" | |
| echo " export PATH=\"$INSTALL_DIR:\$PATH\"" | |
| echo "" | |
| fi | |
| # Check if opencode is installed | |
| if ! command -v opencode &> /dev/null; then | |
| echo "" | |
| echo "⚠️ Warning: 'opencode' command not found" | |
| echo "" | |
| echo "whatcmd requires opencode to be installed." | |
| echo "Please install opencode first: https://github.com/anomalyco/opencode" | |
| echo "" | |
| exit 1 | |
| fi | |
| echo "" | |
| echo "Installation complete! 🎉" | |
| echo "" | |
| echo "Usage: whatcmd <description of what you want to do>" | |
| echo "" | |
| echo "Examples:" | |
| echo " whatcmd find all PDF files larger than 10MB" | |
| echo " whatcmd turn this directory of images into a 5fps gif" | |
| echo " whatcmd list all processes using more than 1GB RAM" | |
| echo "" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| """ | |
| what-cmd - Natural language CLI command generator | |
| A Python wrapper around opencode that generates shell commands from natural language prompts. | |
| Actively utilizes user-defined aliases and functions from ~/.zshrc. | |
| """ | |
| import sys | |
| import os | |
| import subprocess | |
| import re | |
| import tempfile | |
| import json | |
| from typing import Optional, Tuple, Dict, List | |
| def extract_shell_context() -> str: | |
| """ | |
| Extract aliases and functions from ~/.zshrc for context. | |
| Returns: | |
| Formatted string with aliases and functions, or error message | |
| """ | |
| zshrc_path = os.path.expanduser('~/.zshrc') | |
| try: | |
| with open(zshrc_path, 'r') as f: | |
| content = f.read() | |
| except FileNotFoundError: | |
| return "# No ~/.zshrc found" | |
| except PermissionError: | |
| return "# Unable to read ~/.zshrc (permission denied)" | |
| except Exception as e: | |
| return f"# Error reading shell config: {e}" | |
| # Extract aliases | |
| aliases = [] | |
| alias_pattern = r'^\s*alias\s+([^=]+)=[\'"](.*?)[\'"]' | |
| for match in re.finditer(alias_pattern, content, re.MULTILINE): | |
| name = match.group(1).strip() | |
| command = match.group(2).strip() | |
| aliases.append(f"- {name} → {command}") | |
| # Also catch aliases without quotes | |
| alias_pattern_noquote = r'^\s*alias\s+([^=]+)=([^\s]+)' | |
| for match in re.finditer(alias_pattern_noquote, content, re.MULTILINE): | |
| name = match.group(1).strip() | |
| command = match.group(2).strip() | |
| # Avoid duplicates from quoted pattern | |
| alias_str = f"- {name} → {command}" | |
| if alias_str not in aliases: | |
| aliases.append(alias_str) | |
| # Extract functions (simple parsing) | |
| functions = [] | |
| # Pattern: function_name() { or function function_name { | |
| func_pattern = r'^\s*(?:function\s+)?(\w+)\s*\(\)\s*\{' | |
| for match in re.finditer(func_pattern, content, re.MULTILINE): | |
| name = match.group(1).strip() | |
| functions.append(f"- {name}()") | |
| # Build context string | |
| context_parts = ["Available shell aliases and functions from ~/.zshrc:\n"] | |
| if aliases: | |
| context_parts.append("ALIASES:") | |
| context_parts.extend(aliases) | |
| context_parts.append("") | |
| if functions: | |
| context_parts.append("FUNCTIONS:") | |
| context_parts.extend(functions) | |
| context_parts.append("") | |
| if not aliases and not functions: | |
| context_parts.append("# No aliases or functions found") | |
| context_parts.append("Please use these aliases/functions when appropriate in your command suggestions.") | |
| return "\n".join(context_parts) | |
| def call_opencode(prompt: str, timeout: int = 60) -> Dict: | |
| """ | |
| Call opencode run with the given prompt. | |
| Args: | |
| prompt: The full prompt to send to opencode | |
| timeout: Timeout in seconds | |
| Returns: | |
| Parsed response from opencode | |
| Raises: | |
| RuntimeError: If opencode fails or times out | |
| """ | |
| try: | |
| result = subprocess.run( | |
| ['opencode', 'run', '--format', 'json', prompt], | |
| capture_output=True, | |
| text=True, | |
| timeout=timeout | |
| ) | |
| if result.returncode != 0: | |
| raise RuntimeError(f"OpenCode failed with exit code {result.returncode}: {result.stderr}") | |
| lines = result.stdout.strip().split('\n') | |
| response_text = "" | |
| for line in lines: | |
| if not line.strip(): | |
| continue | |
| try: | |
| event = json.loads(line) | |
| if event.get('type') == 'text': | |
| part = event.get('part', {}) | |
| text = part.get('text', '') if isinstance(part, dict) else event.get('text', '') | |
| response_text += text | |
| except json.JSONDecodeError: | |
| continue | |
| if not response_text: | |
| raise RuntimeError("No response text received from opencode") | |
| return {'text': response_text.strip()} | |
| except subprocess.TimeoutExpired: | |
| raise RuntimeError("OpenCode request timed out") | |
| except FileNotFoundError: | |
| raise RuntimeError("'opencode' command not found. Please install opencode first.") | |
| except Exception as e: | |
| raise RuntimeError(f"Error calling opencode: {e}") | |
| def parse_response(response_text: str) -> Tuple[Optional[str], Optional[str], Optional[str]]: | |
| """ | |
| Parse opencode response to extract question, command, or explanation. | |
| Returns: | |
| Tuple of (question, command, explanation) | |
| - question: If opencode is asking a clarifying question | |
| - command: If opencode provided a command | |
| - explanation: Explanation of the command | |
| """ | |
| question = None | |
| command = None | |
| explanation = None | |
| # Look for QUESTION: pattern | |
| question_match = re.search(r'QUESTION:\s*(.+?)(?:\n|$)', response_text, re.IGNORECASE) | |
| if question_match: | |
| question = question_match.group(1).strip() | |
| # Look for COMMAND: pattern | |
| command_match = re.search(r'COMMAND:\s*(.+?)(?:\n|$)', response_text, re.IGNORECASE) | |
| if command_match: | |
| command = command_match.group(1).strip() | |
| # Look for EXPLANATION: pattern | |
| explanation_match = re.search(r'EXPLANATION:\s*(.+?)(?:\n|$)', response_text, re.IGNORECASE | re.DOTALL) | |
| if explanation_match: | |
| explanation = explanation_match.group(1).strip() | |
| # If no structured format found, try to extract from code blocks | |
| if not command: | |
| code_block_match = re.search(r'```(?:bash|sh)?\n(.+?)\n```', response_text, re.DOTALL) | |
| if code_block_match: | |
| command = code_block_match.group(1).strip() | |
| # Use rest of text as explanation | |
| explanation = response_text.replace(code_block_match.group(0), '').strip() | |
| return question, command, explanation | |
| def build_system_prompt(shell_context: str, user_request: str) -> str: | |
| """ | |
| Build the system prompt for opencode. | |
| Args: | |
| shell_context: Extracted shell aliases and functions | |
| user_request: User's natural language request | |
| Returns: | |
| Complete prompt string | |
| """ | |
| return f"""{shell_context} | |
| User request: {user_request} | |
| You are a shell command generator. Your task is to generate a single shell command that accomplishes the user's request. | |
| Rules: | |
| 1. If the request is unclear, ask ONE specific clarifying question using format: QUESTION: [your question] | |
| 2. If clear, provide the exact command using format: | |
| COMMAND: [the command] | |
| EXPLANATION: [brief explanation of what it does] | |
| 3. If it requires a complex script (more than 3 piped commands or multiple steps), respond with: | |
| COMPLEX: This requires a multi-step script | |
| SUGGESTION: [suggest the approach or steps] | |
| 4. Prefer using user's aliases and functions when appropriate | |
| 5. Be concise and practical | |
| Respond now:""" | |
| def display_command(command: str, explanation: Optional[str]): | |
| """ | |
| Display the suggested command with formatting. | |
| Args: | |
| command: The command to display | |
| explanation: Optional explanation | |
| """ | |
| print("\n╭─────────────────────────────────────────╮") | |
| print("│ Suggested Command │") | |
| print("╰─────────────────────────────────────────╯\n") | |
| print(f" {command}\n") | |
| if explanation: | |
| print(f"💡 Explanation: {explanation}\n") | |
| def get_approval() -> str: | |
| """ | |
| Get user approval to run the command. | |
| Returns: | |
| User's choice: 'y', 'n', or 'e' | |
| """ | |
| while True: | |
| response = input("Run this command? [y/N/e(dit)]: ").strip().lower() | |
| if response in ['y', 'yes']: | |
| return 'y' | |
| elif response in ['n', 'no', '']: | |
| return 'n' | |
| elif response in ['e', 'edit']: | |
| return 'e' | |
| else: | |
| print("Please enter 'y' (yes), 'n' (no), or 'e' (edit)") | |
| def edit_command(command: str) -> Optional[str]: | |
| """ | |
| Open command in editor for modification. | |
| Args: | |
| command: Initial command | |
| Returns: | |
| Edited command, or None if user cancelled | |
| """ | |
| editor = os.environ.get('EDITOR', os.environ.get('VISUAL', 'nano')) | |
| with tempfile.NamedTemporaryFile(mode='w+', suffix='.sh', delete=False) as tf: | |
| tf.write(command) | |
| tf.flush() | |
| temp_path = tf.name | |
| try: | |
| subprocess.run([editor, temp_path]) | |
| with open(temp_path, 'r') as f: | |
| edited_command = f.read().strip() | |
| return edited_command if edited_command else None | |
| finally: | |
| os.unlink(temp_path) | |
| def execute_command(cmd: str) -> int: | |
| """ | |
| Execute command in user's shell with proper environment. | |
| Args: | |
| cmd: Command to execute | |
| Returns: | |
| Exit code | |
| """ | |
| shell = os.environ.get('SHELL', '/bin/zsh') | |
| result = subprocess.run( | |
| cmd, | |
| shell=True, | |
| executable=shell | |
| ) | |
| return result.returncode | |
| def interactive_loop(shell_context: str, user_request: str, max_questions: int = 3) -> Tuple[Optional[str], Optional[str]]: | |
| """ | |
| Interactive question/answer loop with opencode. | |
| Args: | |
| shell_context: Extracted shell context | |
| user_request: User's initial request | |
| max_questions: Maximum number of clarifying questions | |
| Returns: | |
| Tuple of (command, explanation) or (None, None) if failed | |
| """ | |
| conversation_history = [] | |
| question_count = 0 | |
| prompt = build_system_prompt(shell_context, user_request) | |
| while question_count < max_questions: | |
| try: | |
| response = call_opencode(prompt) | |
| response_text = response.get('text', '') | |
| if not response_text: | |
| print("Error: Empty response from opencode") | |
| return None, None | |
| question, command, explanation = parse_response(response_text) | |
| # Check for complex script indication | |
| if 'COMPLEX:' in response_text or 'requires a script' in response_text.lower(): | |
| print("\nThis task requires a script with multiple steps:\n") | |
| # Extract suggestions | |
| suggestion_match = re.search(r'SUGGESTION:\s*(.+)', response_text, re.IGNORECASE | re.DOTALL) | |
| if suggestion_match: | |
| print(suggestion_match.group(1).strip()) | |
| else: | |
| print(response_text) | |
| print("\nConsider creating a shell script or running these commands manually.") | |
| return None, None | |
| # If we got a command, return it | |
| if command: | |
| return command, explanation | |
| # If we got a question, ask user | |
| if question: | |
| question_count += 1 | |
| print(f"\nQ{question_count}: {question}") | |
| user_answer = input("> ").strip() | |
| if not user_answer: | |
| print("No answer provided. Exiting.") | |
| return None, None | |
| # Update prompt with the answer | |
| conversation_history.append(f"Q: {question}") | |
| conversation_history.append(f"A: {user_answer}") | |
| prompt = build_system_prompt(shell_context, user_request) | |
| prompt += "\n\nPrevious conversation:\n" + "\n".join(conversation_history) | |
| prompt += "\n\nNow provide the command or ask another question if still unclear:" | |
| else: | |
| # No question and no command - unclear response | |
| print("Unable to generate clear command. Response:") | |
| print(response_text) | |
| return None, None | |
| except RuntimeError as e: | |
| print(f"Error: {e}") | |
| return None, None | |
| # Reached max questions | |
| print(f"\nUnable to generate a clear command after {max_questions} questions.") | |
| print("\nSuggestions:") | |
| print("- Your request might need a custom script") | |
| print("- Try breaking it into smaller commands") | |
| print("- Be more specific about what you want to accomplish") | |
| return None, None | |
| def main(): | |
| """Main entry point.""" | |
| # Parse arguments | |
| if len(sys.argv) < 2: | |
| print("Usage: whatcmd <description of what you want to do>") | |
| print("\nExamples:") | |
| print(" whatcmd find all PDF files larger than 10MB") | |
| print(" whatcmd turn this directory of images into a 5fps gif") | |
| print(" whatcmd list all processes using more than 1GB RAM") | |
| sys.exit(1) | |
| # Check for help flag | |
| if sys.argv[1] in ['-h', '--help', 'help']: | |
| print("Usage: whatcmd <description of what you want to do>") | |
| print("\nExamples:") | |
| print(" whatcmd find all PDF files larger than 10MB") | |
| print(" whatcmd turn this directory of images into a 5fps gif") | |
| print(" whatcmd list all processes using more than 1GB RAM") | |
| sys.exit(0) | |
| user_request = ' '.join(sys.argv[1:]) | |
| if not user_request.strip(): | |
| print("Error: Empty request") | |
| sys.exit(1) | |
| # Extract shell context | |
| print("Analyzing your shell environment...") | |
| shell_context = extract_shell_context() | |
| # Interactive loop to get command | |
| print("Generating command...\n") | |
| command, explanation = interactive_loop(shell_context, user_request) | |
| if not command: | |
| sys.exit(1) | |
| # Display and get approval | |
| display_command(command, explanation) | |
| approval = get_approval() | |
| if approval == 'n': | |
| print("Command not executed.") | |
| sys.exit(0) | |
| elif approval == 'e': | |
| edited = edit_command(command) | |
| if not edited: | |
| print("Edit cancelled.") | |
| sys.exit(0) | |
| # Display edited command and get approval again | |
| display_command(edited, "Edited command") | |
| approval = get_approval() | |
| if approval != 'y': | |
| print("Command not executed.") | |
| sys.exit(0) | |
| command = edited | |
| # Execute command | |
| print("\nExecuting...\n") | |
| exit_code = execute_command(command) | |
| if exit_code == 0: | |
| print("\n✓ Command completed successfully") | |
| else: | |
| print(f"\n⚠️ Command exited with code {exit_code}") | |
| sys.exit(exit_code) | |
| if __name__ == '__main__': | |
| main() |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
whatcmd
Generate shell commands from natural language using AI. No more memorizing syntax or searching StackOverflow.
Powered by OpenCode.
Quick Install
Requirements:
Usage
No quotes needed. Just type naturally.
Examples
How It Works
~/.zshrcfor aliases and functionsYour options:
y/yes→ Execute immediatelyn/no/Enter→ Cancel (default)e/edit→ Open in$EDITORto modify firstFeatures
✅ Shell-aware - Uses your aliases automatically (e.g.,
kinstead ofkubectl)✅ Interactive clarification - Asks up to 3 questions if request is unclear
✅ Safe by default - Requires explicit approval before running
✅ Editable - Modify commands before execution
✅ No API keys - Uses your OpenCode credentials
✅ No quotes needed -
whatcmd do the thingjust worksInstallation Details
The installer:
whatcmdto~/bin/(or$INSTALL_DIR)~/binis in yourPATHCustom install location:
Add to PATH (if needed):
Troubleshooting
Command not found
Check if
~/binis in PATH:Add to
~/.zshrc:OpenCode not found
Install OpenCode first:
# See: https://github.com/anomalyco/opencodeGets stuck or times out
opencode run "test"Command doesn't use my aliases
Make sure aliases are defined in
~/.zshrc(not.bash_profileor other files).Test if alias parsing works:
Advanced
Interactive Questions
If your request is unclear,
whatcmdasks clarifying questions:Max 3 questions. If still unclear, exits with suggestions.
Complex Tasks
For multi-step operations,
whatcmdsuggests an approach instead:$ whatcmd set up a web server with nginx and ssl This task requires a script with multiple steps: 1. Install nginx: sudo apt install nginx 2. Configure nginx: edit /etc/nginx/sites-available/default 3. Install certbot: sudo apt install certbot python3-certbot-nginx 4. Run certbot: sudo certbot --nginx Consider creating a shell script or running these commands manually.Limitations
~/.zshrc(not.bashrcor other shells)Uninstall
rm ~/bin/whatcmdSource Code
This is a Python wrapper around OpenCode that:
See the full source.
License
MIT
Like this? Star the gist ⭐