commit e8833b172d53e5a20a27df1dbf06866783cc2dc0 Author: CancerAi Date: Mon Aug 31 21:53:57 2026 +0800 init diff --git a/README.md b/README.md new file mode 100644 index 0000000..462b4dd --- /dev/null +++ b/README.md @@ -0,0 +1,476 @@ +# aiagent.sh — 100% POSIX-sh 自然语言运维 Agent + +> 一行英文句子 = 一条运维命令。不用背参数,不用记 flag。 + +`aiagent.sh` 是一个**纯 `/bin/sh` 实现**的轻量级"AI Agent"。它不需要 Python、不需要 node、不需要任何运行时——单文件、零依赖,却能把你输入的**自然英语句子**解析成真实的运维操作。 + +```sh +$ ./aiagent.sh +aiagent> please show disk usage +[INFO] Disk usage: +Filesystem Size Used Avail Use% Mounted on +/dev/sda1 100G 42G 58G 42% / + +aiagent> what is the uptime +[INFO] System uptime: + 14:32:11 up 12 days, 3:17, 2 users, load average: 0.12, 0.08, 0.01 + +aiagent> check port 8080 +[INFO] Checking port 8080 ... +[OK] Port 8080 is IN USE +``` + +--- + +## ✨ 核心特性 + +| 特性 | 说明 | +|------|------| +| 🗣️ **自然语言输入** | 接受完整英文句子,不需要任何特殊语法 | +| 🧠 **意图识别** | 自动提取动词、名词、路径、端口、服务名 | +| 🔄 **同义词归一** | `show/display/list` → 统一处理,`kill/stop/terminate` → 统一处理 | +| 🎯 **名词推断动作** | 只说 `uptime` 也能理解成"show uptime" | +| 🔒 **安全确认** | 所有破坏性操作(删除、杀进程、装包)强制 `y/N` 二次确认 | +| 🛡️ **零 eval 注入** | 没有任何字符串拼接执行,包管理器直接调用 | +| ⚙️ **自动适配环境** | 自动检测 `systemctl`/`service`、`ss`/`netstat`、`apt`/`dnf`/`yum`/`apk`/`pacman` | +| 🧩 **可扩展** | 每个 `do_xxx()` 函数独立,新增操作只需加一个 case 分支和一个函数 | +| 📦 **单文件分发** | 约 1050 行,直接 `cp` 到目标机就能跑 | + +--- + +## 🚀 快速上手 + +### 1. 获取脚本 + +```bash +# 从git仓库获取 + git clone https://github.com/CancerXiaoAi/shell-ai.git + git clone https://git.cancerai.top/cancerai/shell-ai.git +``` + +### 2. 两种使用方式 + +```bash +# 方式 A:交互式 REPL(推荐日常使用) +./aiagent.sh + +# 方式 B:一行命令模式(适合脚本/CI) +./aiagent.sh "please check port 443" +./aiagent.sh "what is the kernel version" +./aiagent.sh "health check" +``` + +### 3. 最简示例 + +```bash +# 系统一句话 +./aiagent.sh uptime # 直接说名词就行 +./aiagent.sh disk +./aiagent.sh health + +# 完整句子也没问题 +./aiagent.sh "please help me show memory usage" +./aiagent.sh "could you ping google.com for me" +./aiagent.sh "i want to restart nginx" +``` + +--- + +## 📖 完整命令参考 + +### 💡 语法约定 + +脚本对输入的容忍度极高。以下写法**全部等价**: + +``` +please help me check disk usage +help me check disk +check disk +disk +``` + +规则: +- `please`、`help me`、`to`、`a`、`the`、`can you`、`would you` 等纯语法词**全部自动忽略** +- 只带名词(如 `uptime`)会被自动推断为"show + 该名词" +- 只带动词加短语(如 `check port 80`)直接路由 + +### 🖥️ 系统信息 — System Info + +| 输入示例 | 实际调用 | 说明 | +|---------|---------|------| +| `show disk usage` / `disk` | `df -h` | 磁盘使用情况 | +| `check memory` / `memory` / `ram` | `/proc/meminfo` 或 `free -h` | 内存占用 | +| `check cpu load` / `cpu` / `load` | `/proc/loadavg` | CPU 负载 | +| `what is the uptime` / `uptime` | `uptime` | 系统运行时长 | +| `show kernel version` / `version` | `/etc/os-release` + `uname` | OS 和内核版本 | +| `show listening ports` | `ss -tulnp` 或 `netstat -tulnp` | 监听端口 | +| `who is logged in` / `show users` | `who` | 当前登录用户 | +| `health check` / `check` | 一键跑 uptime + CPU + 内存 + 磁盘 | 健康检查 | + +### 📂 文件操作 — Files + +| 输入示例 | 实际调用 | 说明 | +|---------|---------|------| +| `list files in /var/log` | `ls -lah /var/log` | 列出目录 | +| `show me content of /etc/hosts` | `ls /etc/hosts` | 查看文件 | +| `find files named *.log in /tmp` | `find /tmp -type f -name "*.log"` | 查找文件 | +| `find process nginx` | `ps aux | grep nginx` | 按名字找进程 | +| `create directory /data` | `mkdir -p /data` | 创建目录 | +| `create file /tmp/hello.txt` | `: > /tmp/hello.txt` | 创建空文件 | +| `copy /etc/hosts /tmp/hosts.bak` | `cp -r/-f` | 自动判断文件/目录复制 | +| `move /tmp/a.log /var/log/` | `mv -f` | 移动/重命名 | + +### ⚠️ 破坏性操作 — Destructive(强制 y/N 确认) + +| 输入示例 | 实际调用 | 说明 | +|---------|---------|------| +| `please remove /tmp/cache` | `rm -rf`(确认后) | 递归删目录 | +| `delete file /tmp/bad.log` | `rm -f`(确认后) | 删除文件 | +| `clean old log files in /var/log` | `find -delete`(确认后) | 清理日志 | +| `kill process nginx` | `kill` + PID 列表(确认后) | 按名字杀进程 | +| `kill the process on port 8080` | 先 lsof/ss 找 PID(确认后) | 按端口杀进程 | +| `stop nginx` | `systemctl stop`(确认后) | 停服务 | + +### 🔌 服务管理 — Services + +| 输入示例 | 实际调用 | 说明 | +|---------|---------|------| +| `check status of sshd` | `systemctl status` | 查看服务状态 | +| `start docker service` | `systemctl start` | 启动服务 | +| `restart nginx` | `systemctl restart` | 重启服务 | +| `nginx` | 自动识别服务名 → 查状态 | 裸服务名默认查状态 | + +### 🌐 网络 — Network + +| 输入示例 | 实际调用 | 说明 | +|---------|---------|------| +| `check port 443` / `port 443` | `ss`/`netstat`/`nc` | 检查端口监听状态 | +| `ping google.com` | `ping -c 3 -W 2` | 连通性测试(3 包) | +| `show listening ports` | `ss -tulnp` | 所有监听端口 | + +### 📦 包管理 — Packages + +| 输入示例 | 实际调用 | 说明 | +|---------|---------|------| +| `please install package htop` | 自动检测 `apt-get`/`dnf`/`yum`/`apk`/`pacman` 并调用 | 安装系统包(确认后) | + +### 📜 日志 — Logs + +| 输入示例 | 实际调用 | 说明 | +|---------|---------|------| +| `tail /var/log/syslog` | `tail -30` | 直接路径 | +| `tail nginx log` | 自动匹配 `/var/log/nginx/access.log` | 按服务名猜路径 | +| `follow docker log` | glob `/var/log/*docker*` | 模糊匹配 | + +### 🎛️ 其他 + +| 输入 | 说明 | +|------|------| +| `help` | 打印帮助手册 | +| `quit` / `exit` / `bye` | 退出 REPL | + +--- + +## 🧠 自然语言解析原理 + +整个解析管线分为 **5 层**,每一层都只使用 POSIX sh 原语实现: + +``` +用户输入:please help me check port 8080 + │ + ▼ +┌─────────────────────────────────┐ +│ 第 1 层:strip_fillers() │ +│ 丢弃纯语法词:please, help, me │ +└─────────────────────────────────┘ + │ 得到:check port 8080 + ▼ +┌─────────────────────────────────┐ +│ 第 2 层:跳过引导词 │ +│ 跳过开头的 help/what/can 等客气词│ +└─────────────────────────────────┘ + │ 得到:check port 8080 + ▼ +┌─────────────────────────────────┐ +│ 第 3 层:canonical_verb() │ +│ 动词→同义词归一 │ +│ 名词→推断动作(health→check) │ +└─────────────────────────────────┘ + │ verb=check, phrase="port 8080" + ▼ +┌─────────────────────────────────┐ +│ 第 4 层:dispatch() 一级匹配 │ +│ case "$phrase" in *port*) │ +│ do_check_port; _matched=1 ;; │ +└─────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────┐ +│ 第 5 层:do_check_port() │ +│ 从短语中提取 "8080" │ +│ 依次尝试 ss → netstat → nc │ +│ 输出 [OK]/[WARN] 状态 │ +└─────────────────────────────────┘ +``` + +### 动词同义词映射(canonical_verb) + +``` +show ← show, display, list, print, tell, what, reveal, dump +check ← check, monitor, verify, inspect, diagnose +find ← find, locate, search, lookup, track +remove← remove, delete, erase, rm, wipe, cleanup, clean +kill ← kill, stop, terminate, shutdown, halt +start ← start, launch, run, begin, enable +... +``` + +### 名词 → 推断动作 + +``` +uptime, disk, memory, cpu, version, port, process ... → 自动当作 show +health, status, connection, reachability → 自动当作 check +``` + +### 三级 fallback 匹配机制 + +每个规范动词的路由都有三级兜底: + +1. **一级关键词匹配**:`*disk*` → do_show_disk,`*port*` → do_check_port +2. **二级兜底**:短语是纯数字 → 当作端口号;短语含已知服务名 → do_service_status +3. **三级兜底**:`check` 空短语 → do_health_check;其他 → 提示用户重述 + +--- + +## 🔒 安全设计 + +### 强制二次确认 + +以下操作**必须**输入 `y` 或 `yes` 才会继续,其他任何输入都视为取消: + +- 删除目录/文件(`remove`, `delete`) +- 杀进程(`kill`) +- 停服务(`stop service`) +- 安装系统包(`install`) + +``` +[CONFIRM] Recursively remove directory '/tmp/cache' ? (y/N): +``` + +### 零 eval 注入 + +整个脚本**没有任何 `eval` 调用**。包管理器的调用采用直接分支方式: + +```sh +if command -v apt-get >/dev/null 2>&1; then + apt-get install -y -- "$_pkg" # 直接调用,参数安全引用 +elif command -v dnf >/dev/null 2>&1; then + dnf install -y -- "$_pkg" +... +``` + +### 路径安全 + +`rm -rf`、`mv`、`cp` 等危险操作都使用了 `--` 分隔符,防止路径以 `-` 开头被解释为选项。 + +### 信号处理 + +REPL 模式下按 Ctrl+C 会优雅退出,不会留下终端异常状态: + +```sh +trap 'echo; printf "Bye.\n"; exit 130' INT TERM +``` + +--- + +## 🏗️ 架构总览 + +``` +aiagent.sh (~1050 lines, 单文件) +├── 辅助函数层 +│ ├── info() / warn() / error() / success() # 带颜色的日志输出 +│ └── confirm() # y/N 安全确认 +│ +├── 解析层(第 1–3 层) +│ ├── strip_fillers() # 丢弃纯语法词 +│ ├── canonical_verb() # 同义词归一 + 名词推断 +│ ├── _extract_path() # 从短语中提取路径 +│ ├── _extract_svcname() # 从短语中提取服务名 +│ └── _SERVICES_RE # 已知服务名正则 +│ +├── 路由层(第 4 层) +│ └── dispatch() # 大 case 分支,三级 fallback +│ +├── 执行层(第 5 层) +│ ├── do_show_disk / _memory / _cpu / _uptime / _version / ... +│ ├── do_check_port / do_ping_host / do_service_status / do_health_check +│ ├── do_find_process / do_find_file +│ ├── do_remove_dir / _file / _log +│ ├── do_kill_process / do_kill_by_port +│ ├── do_service_start / _restart / _stop +│ ├── do_create_dir / _file +│ ├── do_copy_path / do_move_path +│ ├── do_install_pkg +│ └── do_tail_log +│ +├── REPL 层 +│ ├── run_one() # 单次命令处理管线 +│ └── repl() # 交互式循环 +│ +└── 入口 + └── main() # 分交互式/一行模式 +``` + +### 依赖关系图 + +``` +main() ─→ repl() 或 run_one() + │ + ▼ + dispatch() + │ + ┌─────────┼─────────────┬──────────┬────────────┐ + ▼ ▼ ▼ ▼ ▼ + show() check() find() remove() install() + │ │ │ │ │ + ▼ ▼ ▼ ▼ ▼ + do_show_* do_check_* do_find_* do_remove_* do_install_pkg + │ + ▼ + confirm() ←── 所有 destructive 函数必经 +``` + +--- + +## ⚙️ 运行要求 + +### 最低要求 + +| 项目 | 要求 | +|------|------| +| Shell 解释器 | `/bin/sh`(POSIX sh,任何实现均可:dash / busybox / bash --posix) | +| 操作系统 | Linux(主要目标);macOS 可读但部分功能受限(无 systemctl/ss) | +| 权限 | 普通用户即可运行,执行破坏性/管理类命令时需要相应权限 | + +### 可选依赖(根据操作自动探测) + +| 操作 | 依赖(择一) | +|------|-------------| +| 磁盘/内存/CPU | `df`, `/proc/meminfo` 或 `free`, `/proc/loadavg` | +| 服务管理 | `systemctl` 或 `service` | +| 端口检查 | `ss` / `netstat` / `nc` | +| 进程查找 | `ps`, `pgrep`(可选) | +| 进程/端口杀 | `pgrep`, `lsof`, `fuser`(择一) | +| 网络 | `ping` | +| 文件查找 | `find` | +| 包安装 | `apt-get` / `dnf` / `yum` / `apk` / `pacman`(择一) | + +### 跨包管理器支持 + +| 发行版 | 自动使用 | +|--------|---------| +| Debian / Ubuntu | `apt-get` | +| RHEL 8+ / Fedora / CentOS 8+ | `dnf` | +| RHEL 7 / CentOS 7 | `yum` | +| Alpine | `apk` | +| Arch / Manjaro | `pacman` | + +--- + +## ⚡ 性能 + +解析管线全部使用 POSIX sh 内建实现(`case` / `for` / `if` / `read` / `printf`),**不 fork 任何外部进程**——除了 `grep -qE` 和 `awk` 在少数 fallback 分支里会被调用。整个"strip → canonical → dispatch → route"管线的纯解析开销**可以忽略不计**,实际耗时主要来自底层命令本身(如 `df -h`、`ps aux`、`systemctl` 等)。 + +--- + +## 🧪 快速验证 + +在 Docker 容器里跑一下(无需在本机安装任何东西): + +```bash +# Debian +docker run --rm -it -v $(pwd)/aiagent.sh:/aiagent.sh debian:latest \ + sh -c 'chmod +x /aiagent.sh && /aiagent.sh "health check"' + +# Alpine +docker run --rm -it -v $(pwd)/aiagent.sh:/aiagent.sh alpine:latest \ + sh -c 'chmod +x /aiagent.sh && /aiagent.sh "check port 22"' + +# CentOS / Rocky / AlmaLinux +docker run --rm -it -v $(pwd)/aiagent.sh:/aiagent.sh rocky:9 \ + sh -c 'chmod +x /aiagent.sh && /aiagent.sh "restart sshd"' +``` + +--- + +## 🛠️ 扩展指南 + +想新增一个操作(比如"备份数据库")?三个步骤: + +**步骤 1**:加一个规范动词映射(如果是新动词) + +```sh +canonical_verb() { + ... + backup|dump) echo "backup" ;; + ... +} +``` + +**步骤 2**:在 `dispatch()` 的 case 分支里加路由 + +```sh +dispatch() { + ... + backup) + do_backup_db "$phrase" ;; + ... +} +``` + +**步骤 3**:实现 `do_backup_db()` 函数 + +```sh +do_backup_db() { + _db="" + for _tok in $1; do + case "$_tok" in mysql|postgres|redis|mongodb) _db="$_tok" ;; esac + done + [ -z "$_db" ] && { error "No DB type. Example: 'backup mysql'"; return 1; } + confirm "Backup $_db ?" || return 1 + # 实际备份逻辑 + case "$_db" in + mysql) mysqldump ... ;; + postgres) pg_dump ... ;; + ... + esac +} +``` + +完成。现在 `./aiagent.sh "please backup mysql database"` 就能工作了。 + +--- + +## 🐛 已知限制 + +| 限制 | 说明 | +|------|------| +| 只能识别**英文** | 整个关键词库是英语 hardcode 的,不支持中文 | +| 正则只支持 POSIX ERE | 不支持 PCRE(但对运维场景已足够) | +| 交互式 REPL 不支持命令历史 | POSIX sh 的 `read` 没有 readline;可以通过 `rlwrap ./aiagent.sh` 获得历史 | +| 端口号从短语提取用的是 `[0-9]*` | 如果句子里有多个数字,会取最后一个(对"check port 80 on server 192.168.1.1"这种会取 1) | +| macOS 功能受限 | 没有 `systemctl`/`ss`/`/proc/*`,部分操作会降级或报依赖缺失 | +| 包管理器自动检测 | 只是检测命令是否存在,不检查是否真的可用(例如在 chroot 里) | + +--- + +## 📄 License + +MIT License. 复制、修改、商用均自由,唯一要求保留版权声明。 + +--- + +## 🙏 致谢 + +- POSIX 标准委员会:没有你们就没有可移植的 shell +- 所有写出漂亮运维命令的前辈:你们的 `df -h`、`ps aux`、`ss -tulnp` 是这个项目的基石 diff --git a/aiagent.sh b/aiagent.sh new file mode 100755 index 0000000..b32ca77 --- /dev/null +++ b/aiagent.sh @@ -0,0 +1,1050 @@ +#!/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 "$@"