Compare commits

..

2 Commits

Author SHA1 Message Date
gramps 2685a73897 fix: update README section text for consistency 2026-07-19 15:38:59 -07:00
gramps f5d020e39a feat: add uninstall/teardown scripts + uninstall section in README 2026-07-19 15:38:28 -07:00
5 changed files with 386 additions and 5 deletions
+47 -1
View File
@@ -24,7 +24,7 @@ cAIc splits the workload across two machine roles:
**Coordinator** (ultron — Ryzen 7 7840HS, no discrete GPU) runs the FastAPI app, RAG vector search (Qdrant), text embedding (Ollama on CPU), query triage (Phi-4-mini), web search (SearXNG), message broker (RabbitMQ), and all SQLite-backed services — memory, profiles, conversations, settings. Every CPU-bound task stays here. **Coordinator** (ultron — Ryzen 7 7840HS, no discrete GPU) runs the FastAPI app, RAG vector search (Qdrant), text embedding (Ollama on CPU), query triage (Phi-4-mini), web search (SearXNG), message broker (RabbitMQ), and all SQLite-backed services — memory, profiles, conversations, settings. Every CPU-bound task stays here.
**Workers** (jarvis — RX 6600 XT 12 GB / corsair — RTX 5070 Ti 16 GB) run only llama-server for GPU inference. The coordinator never touches a model; workers never touch the database. Workers register via AMQP, receive ping/pong health checks, and accept model-swap commands when triage determines a different model is needed for the current query. **Workers** (jarvis — RX 6600 XT 8 GB / corsair — RTX 5070 Ti 16 GB) run only llama-server for GPU inference. The coordinator never touches a model; workers never touch the database. Workers register via AMQP, receive ping/pong health checks, and accept model-swap commands when triage determines a different model is needed for the current query.
This split keeps the UI responsive during inference (the coordinator isn't blocked by GPU compute) and lets workers focus VRAM entirely on model weights rather than browser sessions or API orchestration. This split keeps the UI responsive during inference (the coordinator isn't blocked by GPU compute) and lets workers focus VRAM entirely on model weights rather than browser sessions or API orchestration.
@@ -526,6 +526,52 @@ Settings are stored in the `settings` table and include:
- `skills_enabled` — Skills framework (true/false) - `skills_enabled` — Skills framework (true/false)
- `default_model` — Default inference model - `default_model` — Default inference model
## Uninstalling cAIc
Three scripts are provided in `scripts/`. Each accepts `-y` for unattended execution.
### Bare-metal / systemd removal
Stop the service and remove `/opt/caic/`, the systemd unit, AMQP secret, and optionally the pip packages:
```bash
sudo ./scripts/uninstall.sh # interactive
sudo ./scripts/uninstall.sh -y # unattended
```
Removes: systemd `caic` service, `/opt/caic/` + venv, `~/.caic_amqp_secret`, `/tmp/caic_uploads`, `hardware_state.json`. Preserves `caic.db` if it lives outside `/opt/caic/`.
### Docker stack teardown
Stop all containers, remove volumes/images, and delete generated files:
```bash
cd <docker-deploy-directory>
../scripts/teardown-docker.sh # interactive
../scripts/teardown-docker.sh -y # unattended
```
Removes: containers + volumes (`caic_data`, `caic_uploads`, `searxng_config`, `qdrant_storage`, `ollama_models`, `rabbitmq`), images (`caic`, `searxng`, `Qdrant`, `RabbitMQ`, `llama-server`, `Ollama`), `.env`, `secrets/`, `searxng/`, `setup.log`. Preserves `models/*.gguf` unless confirmed.
### Nuclear clean (everything)
Removes bare-metal install AND Docker stack AND config AND temp data. Double-confirmation required:
```bash
sudo ./scripts/nuclear-clean.sh # double prompt, then unattended
```
Removes: everything from the two scripts above plus `/var/lib/caic/` and temp directories. Offers to delete the repository itself. Does NOT remove Docker Engine, pip packages, GPU drivers, or WireGuard config.
### Partial / manual clean
Files and components not tracked by the scripts:
- `caic.db` (SQLite database at custom `CAIC_DB_PATH`)
- Reverse proxy configs (Caddyfile, nginx)
- WireGuard tunnel configurations
- Docker Engine itself (`sudo apt remove docker containerd runc; sudo rm -rf /var/lib/docker`)
## Testing ## Testing
```bash ```bash
+125
View File
@@ -0,0 +1,125 @@
#!/usr/bin/env bash
# cAIc — nuclear clean uninstall
# Removes every trace of cAIc: bare-metal systemd install AND Docker stack.
# Prompts for each step. Use -y to skip ALL prompts (fully automatic).
# Use with extreme caution — will delete data and model files.
set -euo pipefail
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; NC='\033[0m'
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
SKIP=false
[ "${1:-}" = "-y" ] && SKIP=true
prompt_yn() {
if $SKIP; then return 0; fi
local msg=$1; local default=${2:-n}
if [[ "$default" == "y" ]]; then
read -p "$msg [Y/n] " r; [[ -z "$r" || "$r" =~ ^[Yy] ]]
else
read -p "$msg [y/N] " r; [[ "$r" =~ ^[Yy] ]]
fi
}
warn() { echo -e "${YELLOW}$1${NC}"; }
if ! $SKIP; then
echo ""
echo "${RED}================================================${NC}"
echo "${RED} cAIc NUCLEAR CLEAN - removes all traces${NC}"
echo "${RED} This will delete data, models, and the app.${NC}"
echo "${RED}================================================${NC}"
echo ""
echo "cAIc NUCLEAR CLEAN"
echo "=================="
read -p "Type 'yes' to continue: " confirm
if [ "$confirm" != "yes" ]; then
echo "Aborted."
exit 1
else
SKIP=true
fi
fi
if ! $SKIP; then
prompt_yn "Run nuclear clean?" y || { echo "Aborted."; exit 0; }
fi
echo ""
echo "========== PART 1: Docker stack =========="
if command -v docker &>/dev/null; then
# Stop all cAIc-related containers
docker compose down 2>/dev/null || docker-compose down 2>/dev/null || echo " no compose stack running"
docker ps --filter name=cai[c] --filter name=searxng --filter name=qdrant --filter name=rabbit --filter name=llama-server --filter name=ollama --filter name=caic -aq 2>/dev/null \
| xargs -r docker rm -f 2>/dev/null || true
docker volume rm caic_data caic_uploads searxng_config qdrant_storage rabbitmq_data ollama_models 2>/dev/null || echo " volumes already gone"
docker rmi caic:latest 2>/dev/null || echo " caic image already gone"
echo " Docker stack removed"
else
echo " Docker not installed — skipping"
fi
echo ""
echo "========== PART 2: Systemd service ========="
SERVICE_FILE="/etc/systemd/system/caic.service"
if [ -f "$SERVICE_FILE" ]; then
stop caic 2>/dev/null || true
systemctl disable caic 2>/dev/null || true
rm -f "$SERVICE_FILE"
systemctl daemon-reload
echo " systemd service removed"
else
echo " no systemd service found"
fi
echo ""
echo "========== PART 3: Install directory ========"
for dir in /opt/caic /var/lib/caic; do
if [ -d "$dir" ]; then
rm -rf "$dir"
echo " Removed: $dir"
else
echo " Skipped: $dir"
fi
done
echo ""
echo "========== PART 4: Config and state files ========"
for f in \
/home/gramps/.caic_amqp_secret \
hardware_state.json \
setup.log \
.env; do
rm -f "$f" 2>/dev/null || true
done
echo " Config/state files cleaned"
echo ""
echo "========== PART 5: Temp data ==========="
rm -rf /tmp/caic_uploads 2>/dev/null || true
echo " Temp data cleaned"
echo ""
echo "========== PART 6: User data (prompt each) ========="
if prompt_yn "Remove the repository (current dir)?" n; then
cd ..
rm -rf "$SCRIPT_DIR"
echo " Repository removed"
else
echo " Repository preserved"
fi
echo ""
echo "${GREEN}cAIc nuclear clean complete.${NC}"
echo ""
echo "Manually verify:"
echo " - ls /opt/caic/"
echo " - ls /etc/systemd/system/caic*"
echo " - docker images | grep -E \"caic|searx|qdrant|rabbit|llama|ollama\""
echo " - docker ps -a"
echo ""
echo "To remove Docker Engine itself:"
echo " sudo apt remove docker.io containerd runc"
echo " sudo rm -rf /var/lib/docker"
echo ""
echo "To remove python packages:"
echo " pip uninstall fastapi uvicorn httpx psutil jinja2 python-multipart pypdf aio-pika"
+109
View File
@@ -0,0 +1,109 @@
#!/usr/bin/env bash
# cAIc — Docker stack teardown
# Removes all containers, volumes, images, and generated files
# created by setup-wizard / docker compose.
# Run from the docker deployment directory. Use -y to skip prompts.
set -euo pipefail
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; NC='\033[0m'
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
prompt_yn() {
if [ "${1:-}" = "-y" ]; then return 0; fi
[ "$SKIP" = true ] && return 0
local msg=$1; local default=${2:-n}
if [[ "$default" == "y" ]]; then
read -p "$msg [Y/n] " r; [[ -z "$r" || "$r" =~ ^[Yy] ]]
else
read -p "$msg [y/N] " r; [[ "$r" =~ ^[Yy] ]]
fi
}
SKIP=false
[ "${1:-}" = "-y" ] && SKIP=true
echo "cAIc Docker stack teardown"
echo "=========================="
# ---- Step 1: docker compose down -v ----
if [ -f docker-compose.yml ] || [ -f compose.yml ]; then
if prompt_yn "Stop Docker stack and remove volumes?" n; then
docker compose down -v 2>/dev/null || docker-compose down -v 2>/dev/null || true
echo " Docker stack stopped and volumes removed"
else
warn " Skipped: Docker stack left intact"
fi
else
echo " Skipped: no compose file found"
fi
# ---- Step 2: Remove Docker images ----
if prompt_yn "Remove cAIc Docker image?" n; then
docker rmi caic:latest 2>/dev/null || true
echo " Removed caic:latest"
warn "Remove SearXNG / Qdrant / RabbitMQ / llama-server / Ollama images?"
if prompt_yn " Remove service images?" n; then
for tag in searxng/searxng qdrant/qdrant rabbitmq:4-management \
ghcr.io/ggml-org/llama.cpp ollama/ollama; do
docker rmi "$tag" 2>/dev/null || true
done
echo " Service images removed"
fi
fi
# ---- Step 3: Volumes ----
if prompt_yn "Remove remaining Docker volumes?" n; then
for vol in caic_data caic_uploads searxng_config qdrant_storage rabbitmq_data ollama_models; do
docker volume rm "$vol" 2>/dev/null || true
done
echo " Docker volumes removed"
fi
# ---- Step 4: .env ----
if [ -f .env ]; then
if prompt_yn "Remove .env file?" n; then
rm -f .env
echo " .env removed"
fi
fi
# ---- Step 5: generated directories ----
for dir in secrets searxng; do
if [ -d "$dir" ]; then
if prompt_yn "Remove $dir/ directory?" n; then
rm -rf "$dir"
echo " $dir/ removed"
fi
fi
done
# ---- Step 6: generated files ----
for f in setup.log docker-compose.yml compose.yml; do
[ -f "$f" ] || continue
if prompt_yn "Remove $f?" n; then
rm -f "$f"
echo " $f removed"
fi
done
# ---- Step 7: model files (with big warning) ----
if [ -d models ]; then
echo ""
echo -e "${RED}WARNING: model files (*.gguf) can be gigabytes each.${NC}"
if prompt_yn "Remove all model files from ./models/?" n; then
rm -rf models
echo " models/ removed"
else
echo " models/ preserved"
fi
fi
echo ""
echo -e "${GREEN}cAIc Docker stack teardown complete.${NC}"
echo "Preserved:"
echo " - ./models/ (unless you accepted removal)"
echo ""
echo "Note: Docker Engine itself is not removed. To remove it:"
echo " sudo apt-get remove docker docker-engine docker.io containerd runc"
+98
View File
@@ -0,0 +1,98 @@
#!/usr/bin/env bash
# cAIc — bare-metal/systemd uninstall
# Removes the cAIc install deployed via the README Fresh Install path.
# Run as root or with sudo. Use -y to skip prompts.
set -euo pipefail
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; NC='\033[0m'
SKIP_PROMPT=false
[ "${1:-}" = "-y" ] && SKIP_PROMPT=true
prompt_yn() {
if $SKIP_PROMPT; then return 0; fi
local msg=$1; local default=${2:-n}
if [[ "$default" == "y" ]]; then
read -p "$msg [Y/n] " r; [[ -z "$r" || "$r" =~ ^[Yy] ]]
else
read -p "$msg [y/N] " r; [[ "$r" =~ ^[Yy] ]]
fi
}
warn() { echo -e "${YELLOW}$1${NC}"; }
echo "cAIc Bare-metal / systemd uninstall"
echo "===================================="
# ---- Step 1: systemd service ----
SERVICE_FILE="/etc/systemd/system/caic.service"
if [ -f "$SERVICE_FILE" ]; then
warn "Stopping and disabling caic service..."
systemctl stop caic 2>/dev/null || true
systemctl disable caic 2>/dev/null || true
if prompt_yn "Remove caic systemd service?" n; then
rm -f "$SERVICE_FILE" /etc/systemd/system/caic.service
systemctl daemon-reload
echo " Removed systemd service"
fi
else
echo " Skipped: no systemd service at $SERVICE_FILE"
fi
# ---- Step 2: /opt/caic directory ----
CAIC_DIR="/opt/caic"
if [ -d "$CAIC_DIR" ]; then
if prompt_yn "Remove cAIc installation directory ($CAIC_DIR)?" n; then
rm -rf "$CAIC_DIR"
echo " Removed $CAIC_DIR"
else
warn " Skipped: $CAIC_DIR preserved"
fi
else
echo " Skipped: $CAIC_DIR does not exist"
fi
# ---- Step 3: AMQP secret file ----
if [ -f "/home/gramps/.caic_amqp_secret" ]; then
if prompt_yn "Remove AMQP secret file (/home/gramps/.caic_amqp_secret)?" n; then
rm -f /home/gramps/.caic_amqp_secret
echo " Removed AMQP secret"
fi
fi
# ---- Step 4: Upload temp directory ----
UPLOAD_DIR="/tmp/caic_uploads"
if [ -d "$UPLOAD_DIR" ]; then
if prompt_yn "Remove upload temp directory ($UPLOAD_DIR)?" n; then
rm -rf "$UPLOAD_DIR"
echo " Removed upload temp"
fi
fi
# ---- Step 5: hardware_state.json in cwd ----
if [ -f "hardware_state.json" ]; then
if prompt_yn "Remove cached hardware state?" n; then
rm -f hardware_state.json
echo " Removed cached hardware state"
fi
fi
# ---- Step 6: pip packages if user wants ----
if command -v pip &>/dev/null; then
if prompt_yn "Attempt to uninstall cAIc-related pip packages?" n; then
pip uninstall -y fastapi uvicorn httpx psutil aio-pika jinja2 python-multipart pypdf 2>/dev/null || true
echo " Uninstalled pip packages"
fi
fi
echo ""
echo -e "${GREEN}cAIc bare-metal/uninstall complete.${NC}"
echo "The following may remain:"
echo " - /opt/caic/ (if you chose to preserve)"
echo " - ~/.caic_amqp_secret (if preserved)"
echo " - hardware_state.json (if preserved)"
echo " - installed pip packages (if you chose to skip)"
echo " - Python venv at /opt/caic/venv (removed with /opt/caic)"
echo ""
echo "Caic data stored outside install:"
echo " - caic.db (if configured via CAIC_DB_PATH)"
echo " - claude/opencode .jsonc updates (not tracked by this script)"
+7 -4
View File
@@ -227,8 +227,8 @@ body { font-family: var(--font-body); background: var(--bg-primary); color: var(
.chat-container { flex:1; overflow-y:auto; padding:20px 30px; display:flex; flex-direction:column; gap:8px; background-image:radial-gradient(rgba(255,255,255,0.015) 1px, transparent 1px); background-size:24px 24px; margin-right:28px; } .chat-container { flex:1; overflow-y:auto; padding:20px 30px; display:flex; flex-direction:column; gap:8px; background-image:radial-gradient(rgba(255,255,255,0.015) 1px, transparent 1px); background-size:24px 24px; margin-right:28px; }
.msg-pair { display:flex; flex-direction:column; gap:8px; border-radius:var(--radius); padding:12px 12px 8px; border-left:3px solid transparent; } .msg-pair { display:flex; flex-direction:column; gap:8px; border-radius:var(--radius); padding:12px 12px 8px; border-left:3px solid transparent; }
.msg-pair:nth-child(odd) { background:rgba(255,255,255,0.015); border-left-color:rgba(0,136,187,0.25); } .msg-pair:nth-child(odd) { background:rgba(255,255,255,0.015); border-left-color:rgba(0,136,187,0.30); }
.msg-pair:nth-child(even) { background:rgba(255,255,255,0.035); border-left-color:rgba(243,156,18,0.25); } .msg-pair:nth-child(even) { background:rgba(255,255,255,0.025); border-left-color:rgba(0,136,187,0.12); }
.welcome-screen { flex:1; display:flex; flex-direction:column; align-items:center; justify-content:center; color:var(--text-muted); text-align:center; gap:12px; } .welcome-screen { flex:1; display:flex; flex-direction:column; align-items:center; justify-content:center; color:var(--text-muted); text-align:center; gap:12px; }
.welcome-screen .logo { font-family:var(--font-mono); font-size:48px; color:var(--accent-dim); opacity:0.5; } .welcome-screen .logo { font-family:var(--font-mono); font-size:48px; color:var(--accent-dim); opacity:0.5; }
.welcome-screen .welcome-logo { max-width:70%; height:auto; object-fit:contain; opacity:0.85; } .welcome-screen .welcome-logo { max-width:70%; height:auto; object-fit:contain; opacity:0.85; }
@@ -329,7 +329,10 @@ body { font-family: var(--font-body); background: var(--bg-primary); color: var(
.message.assistant.search-result { }
.message.assistant.search-result .content { background:rgba(243,156,18,0.08); border:1px solid rgba(243,156,18,0.2); border-radius:var(--radius); padding:12px; } .message.assistant.search-result .content { background:rgba(243,156,18,0.08); border:1px solid rgba(243,156,18,0.2); border-radius:var(--radius); padding:12px; }
.message.assistant.search-result .role-label { color:var(--warning) !important; }
.message.assistant.search-result .content .text { color:var(--warning); }
.raw-results { margin-top:12px; background:var(--bg-tertiary); border:1px solid var(--border); border-radius:var(--radius); padding:8px 12px; font-size:12px; } .raw-results { margin-top:12px; background:var(--bg-tertiary); border:1px solid var(--border); border-radius:var(--radius); padding:8px 12px; font-size:12px; }
.raw-results summary { cursor:pointer; color:var(--accent); font-family:var(--font-mono); } .raw-results summary { cursor:pointer; color:var(--accent); font-family:var(--font-mono); }
.raw-results ul { margin:8px 0 0 0; padding-left:20px; list-style:none; } .raw-results ul { margin:8px 0 0 0; padding-left:20px; list-style:none; }
@@ -1652,7 +1655,7 @@ async function sendSearch() {
if (data.done) { if (data.done) {
const roleLabel = assistantDiv.querySelector('.role-label'); const roleLabel = assistantDiv.querySelector('.role-label');
if (roleLabel) { if (roleLabel) {
roleLabel.innerHTML += '<span class="search-badge-inline">🔍 web</span>'; roleLabel.textContent = 'web search';
if (ttr > 0) { if (ttr > 0) {
const ttrSec = ttr / 1000; const ttrSec = ttr / 1000;
roleLabel.innerHTML += `<span class="ttr-badge search">ttr: ${ttrSec.toFixed(1)}s</span>`; roleLabel.innerHTML += `<span class="ttr-badge search">ttr: ${ttrSec.toFixed(1)}s</span>`;
@@ -1843,7 +1846,7 @@ async function sendMessage() {
if (data.token) { if (firstToken) { textEl.innerHTML = ''; firstToken = false; ttr = performance.now() - ttrStart; tokenCount = 0; } fullText += data.token; tokenCount++; textEl.innerHTML = renderMarkdown(fullText); scrollToLatest(); } if (data.token) { if (firstToken) { textEl.innerHTML = ''; firstToken = false; ttr = performance.now() - ttrStart; tokenCount = 0; } fullText += data.token; tokenCount++; textEl.innerHTML = renderMarkdown(fullText); scrollToLatest(); }
if (data.done) { if (data.done) {
const roleLabel = assistantDiv.querySelector('.role-label'); const roleLabel = assistantDiv.querySelector('.role-label');
if (data.searched && roleLabel) roleLabel.innerHTML += '<span class="search-badge-inline">🔍 web</span>'; if (data.searched && roleLabel) roleLabel.textContent = 'web search';
if (typeof data.perplexity === 'number' && roleLabel) { const ppl = data.perplexity; const conf = Math.round(Math.max(0, Math.min(100, (1 - (ppl - 1) / 10) * 100))); const cls = conf >= 80 ? 'high' : conf >= 15 ? 'medium' : 'low'; roleLabel.innerHTML += `<span class="conf-badge ${cls}">${conf}%</span>`; } if (typeof data.perplexity === 'number' && roleLabel) { const ppl = data.perplexity; const conf = Math.round(Math.max(0, Math.min(100, (1 - (ppl - 1) / 10) * 100))); const cls = conf >= 80 ? 'high' : conf >= 15 ? 'medium' : 'low'; roleLabel.innerHTML += `<span class="conf-badge ${cls}">${conf}%</span>`; }
if (typeof data.tokens_per_sec === 'number' && data.tokens_per_sec > 0 && roleLabel) roleLabel.innerHTML += `<span class="tps-badge">${data.tokens_per_sec.toFixed(1)} t/s</span>`; if (typeof data.tokens_per_sec === 'number' && data.tokens_per_sec > 0 && roleLabel) roleLabel.innerHTML += `<span class="tps-badge">${data.tokens_per_sec.toFixed(1)} t/s</span>`;
if (roleLabel && ttr > 0) { if (roleLabel && ttr > 0) {