Files
shell-ai/aiagent.sh
T
2026-08-31 21:53:57 +08:00

1051 lines
38 KiB
Bash
Executable File

#!/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 -- <tokens>).
# 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 <<EOF
${C_CYAN}╔══════════════════════════════════════════════════════════════╗${C_RESET}
${C_CYAN}║ aiagent — a tiny POSIX-sh natural-language agent ║${C_RESET}
${C_CYAN}╚══════════════════════════════════════════════════════════════╝${C_RESET}
Just type what you want in plain English (or abbreviated):
${C_GREEN}# System info${C_RESET}
please show disk usage
check memory
please check cpu load
what is the uptime
show kernel version
who is logged in
${C_GREEN}# Files${C_RESET}
please list files in /var/log
show me the content of /etc/hosts
find files named "*.log" in /tmp
please create directory /data
copy /etc/hosts /tmp/hosts.bak
move /tmp/a.log /var/log/a.log
${C_GREEN}# Destructive (y/N confirmation required)${C_RESET}
please remove the tmp directory
delete file /tmp/bad.log
clean old log files in /var/log
kill process nginx
kill the process on port 8080
${C_GREEN}# Services${C_RESET}
please restart nginx
start docker service
stop mysql
check status of sshd
${C_GREEN}# Network${C_RESET}
check port 443
show listening ports
ping host google.com
${C_GREEN}# Packages${C_RESET}
please install package htop
check installed packages
${C_GREEN}# Logs${C_RESET}
tail nginx log
follow /var/log/syslog
${C_GREEN}# Misc${C_RESET}
help / quit
Tip: leading "please", "help me", "to", articles, etc. are all optional —
you can be as terse or as polite as you like.
EOF
}
# ============================================================================
# SYSTEM INFO commands
# ============================================================================
do_show_disk() {
info "Disk usage:"
if command -v df >/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 "$@"