#!/bin/sh # ============================================================================ # aiagent.sh - A pure POSIX-sh "AI Agent" that understands natural English # sentences and performs common DevOps operations. # # Target: Linux, interpreter: /bin/sh (POSIX sh, no bashisms) # Usage: ./aiagent.sh # interactive REPL # ./aiagent.sh "please check disk usage" # ============================================================================ set -u # ---------- Color helpers (no-op if not a terminal) ---------- if [ -t 1 ]; then C_RESET='\033[0m' C_RED='\033[31m' C_GREEN='\033[32m' C_YELLOW='\033[33m' C_BLUE='\033[34m' C_CYAN='\033[36m' else C_RESET='' C_RED='' C_GREEN='' C_YELLOW='' C_BLUE='' C_CYAN='' fi info() { printf "${C_CYAN}[INFO]${C_RESET} %s\n" "$*"; } warn() { printf "${C_YELLOW}[WARN]${C_RESET} %s\n" "$*"; } error() { printf "${C_RED}[ERROR]${C_RESET} %s\n" "$*" >&2; } success() { printf "${C_GREEN}[OK]${C_RESET} %s\n" "$*"; } # ============================================================================ # Safety confirmation — blocks until user answers y/N # ============================================================================ confirm() { _msg="$1" printf "${C_YELLOW}[CONFIRM]${C_RESET} %s ${C_YELLOW}(y/N):${C_RESET} " "$_msg" read _ans case "$_ans" in y|Y|yes|YES) return 0 ;; *) warn "Operation cancelled."; return 1 ;; esac } # ============================================================================ # Verb → canonical verb mapping (synonym normalization) # ============================================================================ # Input: $1 = raw verb token # Output: prints canonical verb on stdout (or original if unknown) canonical_verb() { case "$1" in # --- system / show --- show|display|list|print|tell|what|reveal|dump) echo "show" ;; check|monitor|verify|inspect|diagnose) echo "check" ;; find|locate|search|lookup|track) echo "find" ;; # --- mutate / destructive --- remove|delete|erase|rm|wipe|cleanup|clean) echo "remove" ;; kill|stop|terminate|shutdown|halt) echo "kill" ;; start|launch|run|begin|enable) echo "start" ;; restart|reload|reboot) echo "restart" ;; install|add|setup|deploy) echo "install" ;; create|make|mkdir|touch|new) echo "create" ;; copy|cp|duplicate|backup) echo "copy" ;; move|mv|rename) echo "move" ;; # --- network --- ping|test.*connection|check.*connection) echo "ping" ;; # --- misc --- tail|follow) echo "tail" ;; help|usage|about|manual) echo "help" ;; quit|exit|bye|goodbye) echo "quit" ;; # --- common nouns that strongly imply a "show" intent --- uptime|version|memory|ram|cpu|load|disk|usage|whoami|\ process|proc|port|listen|files|logs|log|user|users|\ kernel|os|system|date|time) echo "show" ;; # --- nouns that imply a "check" intent --- health|connection|reach|status|reachability) echo "check" ;; *) echo "$1" ;; esac } # Strip leading articles/prepositions/auxiliaries from the token stream # and return a sanitized argument list via stdout (space-joined). # Returns TRUE (0) if the token is a known action verb or verb synonym. is_verb() { case "$1" in show|display|list|print|tell|what|reveal|dump|\ check|monitor|verify|inspect|diagnose|\ find|locate|search|lookup|track|\ remove|delete|erase|rm|wipe|cleanup|clean|\ kill|stop|terminate|shutdown|halt|\ start|launch|run|begin|enable|\ restart|reload|reboot|\ install|add|setup|deploy|\ create|make|mkdir|touch|new|\ copy|cp|duplicate|backup|\ move|mv|rename|\ ping|tail|follow|\ help|usage|about|manual|\ quit|exit|bye|goodbye) return 0 ;; *) return 1 ;; esac } # Strip purely grammatical filler tokens. IMPORTANT: we do NOT strip any # token that could be a valid action verb — that way the very first surviving # token is always the intent verb, even if the user prepends "please help me". strip_fillers() { _out="" for _tok in "$@"; do case "$_tok" in # articles / prepositions / pronouns / auxiliaries — drop them please|pls|me|my|mine|your|yours|us|our|we|they|them|their|\ he|him|his|she|her|hers|it|its|i|you|to|a|an|the|of|for|in|on|at|by|with|from|about|into|over|under|above|below|between|\ can|could|would|should|may|might|must|shall|will|won't|don't|doesn't|didn't|\ do|does|did|is|are|was|were|be|been|being|am|not|no|never|\ want|wanna|need|gonna|let|just|only|also|again|still|already|yet|ever|never|\ very|really|please\?*|\?*|!) : ;; *) _out="$_out $_tok" ;; esac done # trim leading space printf '%s' "${_out# }" } # ============================================================================ # dispatch — the big intent router # # Receives the already-split positional params (after set -- ). # First token is treated as the verb; remaining are the object + args. # ============================================================================ # Known service names — used by show/check fallbacks to detect a bare service name. _SERVICES_RE='sshd?|nginx|apache2?|httpd|mysql|mysqld|mariadb|docker|redis|postgres|postgresql|systemd|rsyslog|cron|crond|kubelet|containerd|etcd|haproxy|tomcat|mongod|mongo|elasticsearch|kibana|grafana|prometheus|consul|vault|zookeeper|kafka|rabbitmq|supervisord|chrony|chronyd|ntp|ntpd|firewalld|iptables|ufw|fail2ban|fluentd|filebeat|logstash|metricbeat|node_exporter|cadvisor|glances|netdata|snmpd|telnetd|ftpd|vsftpd|proftpd' dispatch() { if [ $# -eq 0 ]; then error "Empty command. Try: 'please help me check disk usage'" return 1 fi # Normalize verb _raw_verb="$1"; shift verb="$(canonical_verb "$_raw_verb")" # Remaining tokens form the "object phrase" phrase="$*" # --- fallback: if phrase is empty BUT the raw verb was a noun that we # mapped to "show", treat the raw verb as if it were the phrase, # so that "uptime" → show uptime → do_show_uptime. if [ -z "$phrase" ] && [ "$verb" = "show" ]; then phrase="$_raw_verb" fi case "$verb" in quit) printf "Bye.\n"; exit 0 ;; help) do_help "$phrase" ;; # ======================= SHOW ======================= show) # first try keyword match on phrase _matched=0 case "$phrase" in *disk*) do_show_disk; _matched=1 ;; *memor*|*ram*) do_show_memory; _matched=1 ;; *cpu*|*load*|*aver*) do_show_cpu; _matched=1 ;; *uptime*|*boot*) do_show_uptime; _matched=1 ;; *ver*|*kernel*|*os*) do_show_version; _matched=1 ;; *listen*) do_show_listen; _matched=1 ;; *user*|*who*) do_show_users; _matched=1 ;; *file*|*dir*|*ls*|*content*) do_list_files "$phrase"; _matched=1 ;; *log*) do_tail_log "$phrase"; _matched=1 ;; *process*|*proc*) do_find_process "$phrase"; _matched=1 ;; *status*) do_service_status "$phrase"; _matched=1 ;; *service*) do_service_status "$phrase"; _matched=1 ;; esac # fallback: numeric phrase → likely a port if [ "$_matched" -eq 0 ] && echo "$phrase" | grep -qE '^[0-9]+$'; then do_check_port "$phrase"; _matched=1 fi # fallback: does phrase contain a known service name? if [ "$_matched" -eq 0 ] && echo "$phrase" | grep -qE "(^|[[:space:]])($_SERVICES_RE)([[:space:]]|$)" 2>/dev/null; then do_service_status "$phrase" elif [ "$_matched" -eq 0 ]; then do_show_generic "$phrase" fi ;; # ======================= CHECK ======================= check) _matched=0 case "$phrase" in *disk*) do_show_disk; _matched=1 ;; *memor*|*ram*) do_show_memory; _matched=1 ;; *cpu*|*load*|*aver*) do_show_cpu; _matched=1 ;; *uptime*) do_show_uptime; _matched=1 ;; *port*) do_check_port "$phrase"; _matched=1 ;; *ping*|*host*|*connection*|*reach*) do_ping_host "$phrase"; _matched=1 ;; *process*|*proc*) do_find_process "$phrase"; _matched=1 ;; *service*|*status*) do_service_status "$phrase"; _matched=1 ;; *health*) do_health_check; _matched=1 ;; esac # fallback: numeric → likely a port if [ "$_matched" -eq 0 ] && echo "$phrase" | grep -qE '^[0-9]+$'; then do_check_port "$phrase"; _matched=1 fi # fallback: bare "check" or very short → do full health check if [ "$_matched" -eq 0 ]; then case "$phrase" in ''|check|health|status|it|that|this|system) do_health_check; _matched=1 ;; esac fi if [ "$_matched" -eq 0 ] && echo "$phrase" | grep -qE "(^|[[:space:]])($_SERVICES_RE)([[:space:]]|$)" 2>/dev/null; then do_service_status "$phrase" elif [ "$_matched" -eq 0 ]; then do_show_generic "$phrase" fi ;; # ======================= FIND ======================= find) case "$phrase" in *process*|*proc*|*pid*) do_find_process "$phrase" ;; *file*|*path*) do_find_file "$phrase" ;; *port*) do_check_port "$phrase" ;; *) do_find_generic "$phrase" ;; esac ;; # ======================= REMOVE (DESTRUCTIVE) ======================= remove) case "$phrase" in *dir*|*directory*|*folder*|*rm*-rf) do_remove_dir "$phrase" ;; *file*) do_remove_file "$phrase" ;; *log*) do_remove_log "$phrase" ;; *process*|*proc*) do_kill_process "$phrase" ;; *) do_remove_path "$phrase" ;; esac ;; # ======================= KILL (DESTRUCTIVE) ======================= kill) case "$phrase" in *port*) do_kill_by_port "$phrase" ;; *process*|*proc*|*pid*) do_kill_process "$phrase" ;; *service*) do_service_stop "$phrase" ;; *) do_kill_process "$phrase" ;; esac ;; # ======================= START ======================= start) do_service_start "$phrase" ;; # ======================= RESTART ======================= restart) do_service_restart "$phrase" ;; # ======================= CREATE ======================= create) case "$phrase" in *dir*|*directory*|*folder*) do_create_dir "$phrase" ;; *file*) do_create_file "$phrase" ;; *) do_create_dir "$phrase" ;; esac ;; # ======================= COPY ======================= copy) do_copy_path "$phrase" ;; # ======================= MOVE ======================= move) do_move_path "$phrase" ;; # ======================= INSTALL ======================= install) do_install_pkg "$phrase" ;; # ======================= PING ======================= ping) do_ping_host "$phrase" ;; # ======================= TAIL ======================= tail) do_tail_log "$phrase" ;; *) error "Unknown action: '$_raw_verb'. Type 'help' for available commands." return 1 ;; esac } # ============================================================================ # HELP # ============================================================================ do_help() { cat </dev/null 2>&1; then df -h 2>/dev/null || df else error "df not found" fi } do_show_memory() { info "Memory usage:" if [ -r /proc/meminfo ]; then awk '/^(MemTotal|MemFree|MemAvailable|Buffers|Cached|SwapTotal|SwapFree)/ {print}' /proc/meminfo elif command -v free >/dev/null 2>&1; then free -h 2>/dev/null || free else error "Cannot read memory info (no /proc/meminfo or free)" fi } do_show_cpu() { info "CPU load averages:" if [ -r /proc/loadavg ]; then cat /proc/loadavg printf "\nCPU model: " awk -F: '/model name/ {print $2; exit}' /proc/cpuinfo 2>/dev/null || echo "(unknown)" elif command -v uptime >/dev/null 2>&1; then uptime else error "Cannot read CPU/load info" fi } do_show_uptime() { info "System uptime:" if command -v uptime >/dev/null 2>&1; then uptime elif [ -r /proc/uptime ]; then awk '{d=$1/86400; h=($1%86400)/3600; m=($1%3600)/60; printf "up %.0f days, %.1f hours, %.0f minutes\n", d, h, m}' /proc/uptime else error "Cannot read uptime" fi } do_show_version() { info "System version:" if [ -r /etc/os-release ]; then while IFS='=' read -r k v; do case "$k" in PRETTY_NAME) echo "$v" | tr -d '"' ;; esac done < /etc/os-release fi if command -v uname >/dev/null 2>&1; then uname -sr fi info "Kernel:" [ -r /proc/version ] && head -c 200 /proc/version && echo } do_show_users() { info "Logged-in users:" if command -v who >/dev/null 2>&1; then who else cat /etc/passwd | awk -F: '$7 !~ /nologin/ && $7 !~ /false/ {print $1}' | head -20 fi } do_show_listen() { info "Listening ports:" if command -v ss >/dev/null 2>&1; then ss -tulnp 2>/dev/null || ss -tuln elif command -v netstat >/dev/null 2>&1; then netstat -tulnp 2>/dev/null || netstat -tuln else error "ss/netstat not found. Try: apt install net-tools or iproute2" fi } do_list_files() { # Extract path tokens from the phrase; default to $PWD _path="$PWD" for _tok in $1; do case "$_tok" in /*|~/*|.|..) _path="$_tok" ;; esac done info "Listing files in: $_path" if command -v ls >/dev/null 2>&1; then ls -lah "$_path" 2>/dev/null || ls "$_path" fi } do_tail_log() { _file="" for _tok in $1; do case "$_tok" in /*.log) _file="$_tok" ;; syslog) _file="/var/log/syslog" ;; messages) _file="/var/log/messages" ;; nginx*|*nginx*) if [ -f /var/log/nginx/access.log ]; then _file="/var/log/nginx/access.log" elif [ -f /var/log/nginx/error.log ]; then _file="/var/log/nginx/error.log" fi ;; docker*) _file="/var/log/docker.log" ;; esac done if [ -z "$_file" ]; then # last-resort: glob /var/log for something that matches for _tok in $1; do _hit=$(ls /var/log/*${_tok}* 2>/dev/null | head -1) [ -n "$_hit" ] && _file="$_hit" && break done fi if [ -z "$_file" ]; then error "Cannot figure out which log file you mean. Try a specific path, e.g. '/var/log/syslog'" return 1 fi if [ ! -f "$_file" ]; then error "File not found: $_file" return 1 fi info "Tailing $_file (last 30 lines):" if command -v tail >/dev/null 2>&1; then tail -30 "$_file" fi } do_show_generic() { # Unknown show target — try to just cat or grep something sensible warn "Sorry, I don't know how to show '$1'. Try a more specific phrase:" warn " show disk usage, check memory, show listening ports, ..." } # ============================================================================ # CHECK # ============================================================================ do_check_port() { _port="" for _tok in $1; do case "$_tok" in [0-9]*) _port="$_tok" ;; esac done if [ -z "$_port" ]; then error "No port number found in '$1'. Example: 'check port 8080'" return 1 fi info "Checking port $_port ..." if command -v ss >/dev/null 2>&1; then ss -tuln 2>/dev/null | awk -v p="$_port" '$5 ~ ":"p"$" || $5 ~ ":"p"," {found=1} END {exit !found}' \ && success "Port $_port is IN USE" \ || warn "Port $_port appears FREE (no listener found)" elif command -v netstat >/dev/null 2>&1; then netstat -tuln 2>/dev/null | awk -v p="$_port" '$4 ~ ":"p"$" {found=1} END {exit !found}' \ && success "Port $_port is IN USE" \ || warn "Port $_port appears FREE" elif command -v nc >/dev/null 2>&1; then (echo "" | nc -w1 localhost "$_port" >/dev/null 2>&1) \ && success "Port $_port is open on localhost" \ || warn "Port $_port is not reachable on localhost" else error "No ss/netstat/nc available" fi } do_ping_host() { _host="" for _tok in $1; do case "$_tok" in *.com|*.cn|*.org|*.io|*.net|*.co|*.me|\ [0-9]*.[0-9]*.[0-9]*.[0-9]*|*.*.*|*:*) _host="$_tok" ;; esac done # also catch bare tokens that look like hostnames (contain dot or are "localhost") if [ -z "$_host" ]; then for _tok in $1; do case "$_tok" in localhost|127.0.0.1|gateway|router) _host="$_tok"; break ;; esac done fi if [ -z "$_host" ]; then error "No host found in '$1'. Example: 'ping google.com'" return 1 fi info "Pinging $_host (3 packets)..." if command -v ping >/dev/null 2>&1; then ping -c 3 -W 2 "$_host" 2>&1 || true else error "ping not found" fi } do_service_status() { _svc="" for _tok in $1; do case "$_tok" in sshd|sshd*|nginx*|apache*|httpd*|mysql*|mysqld|mariadb*|\ docker*|redis*|postgres*|postgresql*|systemd*|rsyslog*|cron*|crond*|\ kubelet*|containerd*|etcd*|haproxy*|tomcat*|php*|snmpd*|telnetd*|\ ftpd*|vsftpd*|proftpd*|chronyd*|ntpd*|chrony*|ntp*|firewalld*|iptables*|\ ufw*|fail2ban*|supervisord*|consul*|vault*|zookeeper*|kafka*|rabbitmq*|\ activemq*|memcached*|mongod*|mongo*|fluentd*|fluent-bit*|filebeat*|\ metricbeat*|logstash*|elasticsearch*|kibana*|grafana*|prometheus*|\ alertmanager*|node_exporter*|cadvisor*|glances*|netdata*|\ [a-z][a-z0-9_.-]*) # Don't match very common english words as service names case "$_tok" in status|service|check|show|of|the|on|in|with|for|a|an|to|from|about) ;; *) _svc="$_tok" ;; esac ;; esac done if [ -z "$_svc" ]; then error "No service name found. Example: 'check status of nginx'" return 1 fi info "Status of '$_svc':" if command -v systemctl >/dev/null 2>&1; then systemctl is-active "$_svc" 2>/dev/null && success "active" || warn "inactive/failed" systemctl status "$_svc" --no-pager -l 2>/dev/null | head -15 || true elif command -v service >/dev/null 2>&1; then service "$_svc" status 2>&1 || warn "service $_svc not found or not running" else error "Neither systemctl nor service command found" fi } do_health_check() { info "Running quick health check ..." do_show_uptime; echo do_show_cpu; echo do_show_memory; echo do_show_disk } do_find_generic() { warn "Sorry, I don't know how to find '$1'. Try: find process python, find files *.log" } # ============================================================================ # FIND # ============================================================================ do_find_process() { _pat="" for _tok in $1; do case "$_tok" in process|proc|pid|me|please|help|to|find|the|a|an|of|for|in|on|at|by|with|from|about|running|list|show|display|\?*) : ;; *) _pat="$_tok" ;; esac done if [ -z "$_pat" ]; then info "All running processes (top 30):" ps aux 2>/dev/null | head -31 || ps -ef | head -31 return 0 fi info "Processes matching '$_pat':" ps aux 2>/dev/null | grep -i "$_pat" | grep -v grep || \ ps -ef | grep -i "$_pat" | grep -v grep || \ warn "No matching process found" } do_find_file() { _path="." _name="" for _tok in $1; do case "$_tok" in /*) _path="$_tok" ;; name|named|in|inside|under|files|file|path|paths) : ;; *) _name="${_name:+$_name }$_tok" ;; esac done # If the name doesn't look like a glob/extension, try adding wildcards case "$_name" in *\**|*.*) : ;; # already has glob or extension — keep as-is *) _name="*${_name}*" ;; # bare token → wrap as substring match esac if [ -z "$_name" ]; then error "Please give a filename pattern. Example: 'find files named *.log in /tmp'" return 1 fi info "Searching '$_path' for '$_name' ..." if command -v find >/dev/null 2>&1; then # shellcheck disable=SC2086 find "$_path" -type f -name "$_name" 2>/dev/null | head -20 else error "find not available" fi } # ============================================================================ # DESTRUCTIVE operations # ============================================================================ # Extract path-ish tokens from phrase → returns first /absolute or ./relative _extract_path() { for _tok in $1; do case "$_tok" in /*|~/*|.|..|./*) echo "$_tok"; return 0 ;; esac done # if no absolute/relative path, take the last noun as the path and prepend ./ _last="" for _tok in $1; do case "$_tok" in please|pls|help|me|to|a|an|the|of|for|in|on|at|by|with|from|about|remove|delete|directory|dir|folder|file|clean|old|log|files|tmp|temp|temporary|delete|erase|rm|wipe|cleanup|please\?*|\?*) : ;; *) _last="$_tok" ;; esac done case "$_last" in tmp|temp|temporary) echo "/tmp" ;; *) [ -n "$_last" ] && echo "./$_last" ;; esac } do_remove_dir() { _target="$(_extract_path "$1")" if [ -z "$_target" ]; then error "No directory path found. Example: 'please remove /tmp/cache'" return 1 fi if [ ! -d "$_target" ]; then warn "'$_target' is not a directory (or does not exist)" return 0 fi confirm "Recursively remove directory '$_target' ?" || return 1 info "Running: rm -rf $_target" rm -rf -- "$_target" && success "Removed '$_target'" || error "rm failed on '$_target'" } do_remove_file() { _target="$(_extract_path "$1")" if [ -z "$_target" ]; then error "No file path found."; return 1 fi if [ ! -e "$_target" ]; then warn "'$_target' does not exist"; return 0 fi confirm "Delete '$_target' ?" || return 1 rm -f -- "$_target" && success "Deleted '$_target'" || error "rm failed" } do_remove_log() { _dir="/var/log" _pat="*.log" for _tok in $1; do case "$_tok" in /*) _dir="$_tok" ;; esac done confirm "Remove all files matching '$_pat' in '$_dir' ?" || return 1 info "Running: find $_dir -type f -name '$_pat' -delete" # shellcheck disable=SC2086 find "$_dir" -type f -name "$_pat" -delete 2>/dev/null success "Done." } do_remove_path() { _target="$(_extract_path "$1")" if [ -z "$_target" ]; then error "No target path found."; return 1 fi if [ -d "$_target" ]; then do_remove_dir "$1" elif [ -f "$_target" ]; then do_remove_file "$1" else warn "'$_target' does not exist" fi } # ============================================================================ # KILL PROCESS # ============================================================================ do_find_pid_by_name() { _pat="$1" if command -v pgrep >/dev/null 2>&1; then pgrep -f "$_pat" 2>/dev/null || true else ps aux 2>/dev/null | awk -v p="$_pat" '$0 ~ p {print $2}' | tail -n +2 fi } do_find_pid_by_port() { _port="$1" if command -v ss >/dev/null 2>&1; then ss -tulnp 2>/dev/null | awk -v p="$_port" '$5 ~ ":"p"$" || $5 ~ ":"p"," {print $0}' elif command -v lsof >/dev/null 2>&1; then lsof -ti :"$_port" 2>/dev/null || true elif command -v fuser >/dev/null 2>&1; then fuser "$_port"/tcp 2>/dev/null | tr -d ' ' || true else error "Cannot find PID by port — no ss/lsof/fuser available" return 1 fi } do_kill_process() { _pat="" for _tok in $1; do case "$_tok" in kill|stop|terminate|process|proc|pid|please|help|me|to|the|a|an|of|for|on|in|\?*) : ;; *) _pat="$_tok" ;; esac done if [ -z "$_pat" ]; then error "Please give a process name or PID. Example: 'kill nginx'" return 1 fi # If it looks like a PID number case "$_pat" in [0-9]*) confirm "Send SIGTERM to PID $_pat ?" || return 1 kill "$_pat" && success "Sent SIGTERM to $_pat" || error "kill failed" ;; *) _pids="$(do_find_pid_by_name "$_pat")" if [ -z "$_pids" ]; then warn "No process matching '$_pat' found" return 0 fi info "Matching PIDs: $_pids" confirm "Send SIGTERM to all of them ?" || return 1 echo "$_pids" | while read -r _p; do [ -n "$_p" ] && kill "$_p" 2>/dev/null && success "Killed $_p" || warn "Could not kill $_p" done ;; esac } do_kill_by_port() { _port="" for _tok in $1; do case "$_tok" in [0-9]*) _port="$_tok" ;; esac done if [ -z "$_port" ]; then error "No port number. Example: 'kill the process on port 8080'"; return 1 fi _pids="$(do_find_pid_by_port "$_port")" if [ -z "$_pids" ]; then warn "No process appears to be listening on port $_port"; return 0 fi info "Processes on port $_port:" echo "$_pids" confirm "Send SIGTERM to all of them ?" || return 1 echo "$_pids" | while read -r _line; do _p="$(echo "$_line" | awk '{for(i=1;i<=NF;i++) if($i ~ /^[0-9]+$/ && $(i-1) ~ /pid=|users:/) print $i}')" # fallback: any number in the line if [ -z "$_p" ]; then _p="$(echo "$_line" | grep -oE '[0-9]+' | head -1)" fi [ -n "$_p" ] && kill "$_p" 2>/dev/null && success "Killed PID $_p" done } # ============================================================================ # SERVICE START / RESTART / STOP # ============================================================================ _extract_svcname() { for _tok in $1; do case "$_tok" in please|help|me|to|start|restart|restarted|stop|stopped|service|daemon|\ the|a|an|of|for|in|on|at|\?*) : ;; *) echo "$_tok"; return 0 ;; esac done } do_service_start() { _svc="$(_extract_svcname "$1")" [ -z "$_svc" ] && { error "No service name. Example: 'start nginx'"; return 1; } info "Starting '$_svc' ..." if command -v systemctl >/dev/null 2>&1; then systemctl start "$_svc" && success "$_svc started" || error "Failed to start $_svc" elif command -v service >/dev/null 2>&1; then service "$_svc" start 2>&1 | tee /dev/null \ && success "$_svc started" || error "Failed to start $_svc" else error "Neither systemctl nor service found" fi } do_service_restart() { _svc="$(_extract_svcname "$1")" [ -z "$_svc" ] && { error "No service name. Example: 'restart nginx'"; return 1; } info "Restarting '$_svc' ..." if command -v systemctl >/dev/null 2>&1; then systemctl restart "$_svc" && success "$_svc restarted" || error "Failed to restart $_svc" elif command -v service >/dev/null 2>&1; then service "$_svc" restart 2>&1 | tee /dev/null \ && success "$_svc restarted" || error "Failed to restart $_svc" else error "Neither systemctl nor service found" fi } do_service_stop() { _svc="$(_extract_svcname "$1")" [ -z "$_svc" ] && { error "No service name. Example: 'stop nginx'"; return 1; } confirm "Stop service '$_svc' ?" || return 1 info "Stopping '$_svc' ..." if command -v systemctl >/dev/null 2>&1; then systemctl stop "$_svc" && success "$_svc stopped" || error "Failed to stop $_svc" elif command -v service >/dev/null 2>&1; then service "$_svc" stop 2>&1 | tee /dev/null \ && success "$_svc stopped" || error "Failed to stop $_svc" else error "Neither systemctl nor service found" fi } # ============================================================================ # CREATE # ============================================================================ do_create_dir() { _target="$(_extract_path "$1")" if [ -z "$_target" ]; then error "No path. Example: 'create directory /data'"; return 1 fi if [ -d "$_target" ]; then warn "'$_target' already exists"; return 0 fi info "Creating directory: $_target" mkdir -p -- "$_target" && success "Created '$_target'" || error "mkdir failed" } do_create_file() { _target="$(_extract_path "$1")" if [ -z "$_target" ]; then error "No path. Example: 'create file /tmp/hello.txt'"; return 1 fi info "Creating empty file: $_target" mkdir -p -- "$(dirname "$_target")" 2>/dev/null || true : > "$_target" 2>/dev/null && success "Created '$_target'" || error "touch failed" } # ============================================================================ # COPY / MOVE # ============================================================================ do_copy_path() { _src=""; _dst="" _seen_copy=0 for _tok in $1; do case "$_tok" in copy|from) _seen_copy=1; continue ;; esac if [ -z "$_src" ]; then _src="$_tok"; continue; fi if [ -z "$_dst" ]; then _dst="$_tok"; continue; fi done if [ -z "$_src" ] || [ -z "$_dst" ]; then error "Need source AND destination. Example: 'copy /etc/hosts /tmp/hosts.bak'"; return 1 fi if [ ! -e "$_src" ]; then error "Source '$_src' does not exist"; return 1 fi info "Copying '$_src' → '$_dst'" if [ -d "$_src" ]; then cp -r -- "$_src" "$_dst" && success "Directory copied" || error "cp failed" else cp -f -- "$_src" "$_dst" && success "Copied to $_dst" || error "cp failed" fi } do_move_path() { _src=""; _dst="" for _tok in $1; do case "$_tok" in move|to) continue ;; esac if [ -z "$_src" ]; then _src="$_tok"; continue; fi if [ -z "$_dst" ]; then _dst="$_tok"; continue; fi done if [ -z "$_src" ] || [ -z "$_dst" ]; then error "Need source AND destination. Example: 'move /tmp/a.log /var/log/'"; return 1 fi if [ ! -e "$_src" ]; then error "Source '$_src' does not exist"; return 1 fi info "Moving '$_src' → '$_dst'" mv -f -- "$_src" "$_dst" && success "Moved" || error "mv failed" } # ============================================================================ # PACKAGE INSTALL # ============================================================================ do_install_pkg() { _pkg="" for _tok in $1; do case "$_tok" in please|help|me|to|install|install*|add|setup|deploy|package|pkg|\ the|a|an|of|for|in|on|at|by|with|from|about|\?*) : ;; *) _pkg="$_tok" ;; esac done if [ -z "$_pkg" ]; then error "No package name. Example: 'install htop'"; return 1 fi confirm "Install package '$_pkg' ?" || return 1 # Direct per-manager dispatch — NO eval, avoids command injection. if command -v apt-get >/dev/null 2>&1; then info "Running: apt-get install -y $_pkg" apt-get install -y -- "$_pkg" && success "Package '$_pkg' installed" || error "Install failed" elif command -v dnf >/dev/null 2>&1; then info "Running: dnf install -y $_pkg" dnf install -y -- "$_pkg" && success "Package '$_pkg' installed" || error "Install failed" elif command -v yum >/dev/null 2>&1; then info "Running: yum install -y $_pkg" yum install -y -- "$_pkg" && success "Package '$_pkg' installed" || error "Install failed" elif command -v apk >/dev/null 2>&1; then info "Running: apk add $_pkg" apk add -- "$_pkg" && success "Package '$_pkg' installed" || error "Install failed" elif command -v pacman >/dev/null 2>&1; then info "Running: pacman -Syu $_pkg" pacman -Syu -- "$_pkg" && success "Package '$_pkg' installed" || error "Install failed" else error "No known package manager (apt/dnf/yum/apk/pacman) found" return 1 fi } # ============================================================================ # REPL — the main loop # ============================================================================ run_one() { _input="$*" # skip empty [ -z "$(printf '%s' "$_input" | tr -d '[:space:]')" ] && return 0 # strip purely grammatical fillers (please, me, to, a, the, ...) _clean="$(strip_fillers $_input)" [ -z "$_clean" ] && { warn "Nothing meaningful left after parsing."; return 1; } # shellcheck disable=SC2086 set -- $_clean # Skip leading "guide" verbs (help, what, please-as-verb) when there is # more content after them — they act as polite lead-ins, not as intents. # e.g. "please help me check disk" → strip "please" in fillers → then # "help check disk" → skip leading "help" → real verb is "check". while [ $# -gt 1 ]; do case "$1" in help|what|how|why|where|when|who|which|can|could|would|should|might|may|must|please) shift ;; *) break ;; esac done if [ $# -eq 0 ]; then warn "Nothing meaningful left after parsing."; return 1 fi dispatch "$@" } repl() { # Graceful signal handling — Ctrl+C / TERM prints a newline and exits cleanly. trap 'echo; printf "Bye.\n"; exit 130' INT TERM printf "\n${C_CYAN}╔══════════════════════════════════════════════════════════════╗${C_RESET}\n" printf "${C_CYAN}║${C_RESET} ${C_BLUE}aiagent.sh${C_RESET} — POSIX-sh natural-language agent \n" printf "${C_CYAN}║${C_RESET} type ${C_GREEN}help${C_RESET} to see what I can do, ${C_GREEN}quit${C_RESET} to exit \n" printf "${C_CYAN}╚══════════════════════════════════════════════════════════════╝${C_RESET}\n\n" while :; do printf "${C_GREEN}aiagent> ${C_RESET}" # Read one line, preserving literal input if ! IFS= read -r _line; then echo ""; printf "Bye.\n"; break fi # Ctrl-C inside read → empty line, retry case "$_line" in '') continue ;; esac run_one "$_line" _rc=$? # tiny blank line for readability echo done } # ============================================================================ # Entry point # ============================================================================ main() { # If there are positional args, treat them as the command string if [ $# -gt 0 ]; then run_one "$*" else repl fi } main "$@"