1051 lines
38 KiB
Bash
Executable File
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 "$@"
|