Apex: An Agentic Coding Environment
Steps to setup a full agentic coding environment.
Features
- Sandboxed Proxmox LXC Ubuntu with AI agentes running with full permissions and root privilieges
- Base tools: node, npm, npx, uv, uvx
- Support tools: git, gh, tea, rg
- AI agents: claude code, codex, agy
- MCP: context7, jcodemunch
- Meta harness: omnigent (needs omnigent server setup)
Note: all tools run with npx, uvx and auto udpate on each run
Steps
- Setup a minimal Ubuntu Server LTS LXC in proxmox
- Run ubuntu-base and ubuntu-docker
- Add user
apexand provide passwordless sudo access to this user - Install below
agent-harness-install.pyin/usr/local/binand run it - Install below
apex-runnersystemd service
Filename: agent-harness-install.py
Change example.com to your domain where this file is hosted
#!/usr/bin/env python3
"""
Agent Installation and Maintenance Script for Linux.
This script automates the setup and update of development tools and AI agent wrappers:
1. Node.js: Fetches the latest LTS or Current version and installs it to a specified directory.
2. uv: Fetches and installs the latest version of the astral-sh/uv package manager.
3. gh: Fetches and installs the latest version of the GitHub CLI.
4. tea: Fetches and installs the latest version of the Gitea CLI.
5. Agent Wrappers: Installs shell script wrappers for AI agents into a binary directory:
- codex: Wrapper for @openai/codex (bypasses approvals/sandbox)
- gemini: Wrapper for @google/gemini-cli (enables yolo mode)
- claude: Wrapper for @anthropic-ai/claude-code (skips permissions check)
- bun: Wrapper for bun via npx
- bunx: Wrapper for bun x via npx
- omnigent: Wrapper for omnigent via uvx
Usage:
sudo python3 agent-install.py [options]
Options:
--node-latest Install latest Node.js release instead of the default LTS.
--install-dir Directory to install Node.js (default: /opt/node).
--symlink-dir Directory for binary symlinks and scripts (default: /usr/local/bin).
--skip-node Skip Node.js and agent script installation.
--skip-uv Skip uv, uvx installation.
--skip-gh Skip GitHub CLI installation.
--skip-tea Skip Gitea CLI installation.
--skip-bun Skip bun wrapper installation.
--skip-bunx Skip bunx wrapper installation.
--skip-omnigent Skip omnigent wrapper installation.
--self-update Update this installer script from the stable web URL and exit.
--skip-self-update-check
Skip checking whether a newer installer script is available.
Prerequisites:
- Linux operating system.
- Write access to the symlink and installation directories (usually requires sudo).
"""
import argparse
import json
import logging
import os
import platform
import re
import shlex
import shutil
import subprocess
import sys
import tarfile
import tempfile
import urllib.request
from pathlib import Path
from typing import Optional, List, Dict, Any
# --- Configuration & Constants ---
NODE_INDEX_URL = "https://nodejs.org/dist/index.tab"
DEFAULT_INSTALL_DIR = Path("/opt/node")
SYMLINK_DIR = Path("/usr/local/bin")
UV_REPO = "astral-sh/uv"
SELF_UPDATE_URL = "https://go.example.com/go/agent-harness-install.py"
HTTP_USER_AGENT = "agent-harness-install/1.0 (+https://go.example.com/go/agent-harness-install.py)"
CODEX_SCRIPT_CONTENT = """#!/bin/bash
# If omnigent is available, wrap codex with `omnigent codex`
# Omnigent is installed and we're not already running inside it,
# wrap codex inside omnigent
if command -v omnigent &>/dev/null && [ "${OMNIGENT}" != "1" ]; then
exec omnigent codex "$@"
fi
# Run the latest codex using npx
exec npx -y @openai/codex@latest --dangerously-bypass-approvals-and-sandbox "$@"
"""
GEMINI_SCRIPT_CONTENT = """#!/bin/bash
exec npx -y @google/gemini-cli@latest --yolo "$@"
"""
CLAUDE_SCRIPT_CONTENT = """#!/bin/bash
# If omnigent is available, wrap claude with `omnigent claude`
# Omnigent is installed and we're not already running inside it,
# wrap claude inside omnigent
if command -v omnigent &>/dev/null && [ "${OMNIGENT}" != "1" ]; then
exec omnigent claude "$@"
fi
# Run the latest claude using npx
exec npx -y @anthropic-ai/claude-code@latest --dangerously-skip-permissions "$@"
"""
BUN_SCRIPT_CONTENT = """#!/bin/bash
exec npx -y bun@latest "$@"
"""
BUNX_SCRIPT_CONTENT = """#!/bin/bash
exec npx -y bun@latest x "$@"
"""
AGY_SCRIPT_CONTENT = """#!/bin/bash
# If omnigent is available, wrap agy with `omnigent agy`
# Omnigent is installed and we're not already running inside it,
# wrap agy inside omnigent
if command -v omnigent &>/dev/null && [ "${OMNIGENT}" != "1" ]; then
exec omnigent antigravity "$@"
fi
# Run agy.bin directly
exec agy.bin --dangerously-skip-permissions "$@"
"""
OMNIGENT_SCRIPT_CONTENT = """#!/bin/bash
exec uvx omnigent "$@"
"""
JCODEMUNCH_MCP_SCRIPT_CONTENT = """#!/bin/bash
# For times when jcodemuch is delisted from pip and needs direct git install
#exec uvx --from git+https://github.com/jgravelle/jcodemunch-mcp.git jcodemunch-mcp "$@"
exec uvx jcodemunch-mcp "$@"
"""
CONTEXT7_MCP_SCRIPT_CONTENT = """#!/bin/bash
exec npx -y @upstash/context7-mcp "$@"
"""
# Setup logging
logging.basicConfig(
level=logging.INFO,
format="%(levelname)s: %(message)s"
)
logger = logging.getLogger(__name__)
class InstallationError(Exception):
"""Base class for installation errors."""
pass
class SystemUtils:
"""System-level utility functions."""
@staticmethod
def get_arch_map() -> Dict[str, str]:
return {
"x86_64": "x64",
"amd64": "x64",
"aarch64": "arm64",
"arm64": "arm64",
"armv7l": "armv7l",
}
@classmethod
def get_node_arch(cls) -> str:
machine = platform.machine().lower()
return cls.get_arch_map().get(machine, machine)
@staticmethod
def get_uv_arch() -> Optional[str]:
machine = platform.machine().lower()
if machine in ("x86_64", "amd64"):
return "x86_64-unknown-linux-musl"
elif machine in ("aarch64", "arm64"):
return "aarch64-unknown-linux-musl"
elif machine == "armv7l":
return "armv7-unknown-linux-musleabihf"
return None
@staticmethod
def check_permissions(path: Path) -> bool:
"""Check if the current user has write access to the path."""
if os.getuid() == 0:
return True
return os.access(path, os.W_OK)
class Downloader:
"""Handles network requests and downloads."""
@staticmethod
def fetch_text(url: str) -> str:
try:
req = urllib.request.Request(url, headers={"User-Agent": HTTP_USER_AGENT})
with urllib.request.urlopen(req) as response:
return response.read().decode('utf-8')
except Exception as e:
raise InstallationError(f"Failed to fetch data from {url}: {e}")
@staticmethod
def download_file(url: str, dest: Path):
logger.info(f"Downloading {url}...")
try:
req = urllib.request.Request(url, headers={"User-Agent": HTTP_USER_AGENT})
with urllib.request.urlopen(req) as response, dest.open("wb") as output:
shutil.copyfileobj(response, output)
except Exception as e:
raise InstallationError(f"Download failed for {url}: {e}")
class SelfUpdater:
"""Checks for and applies updates to this installer script."""
def __init__(self, url: str):
self.url = url
self.script_path = Path(__file__).resolve()
def fetch_latest(self) -> bytes:
return Downloader.fetch_text(self.url).encode("utf-8")
def is_update_available(self) -> bool:
if not self.script_path.exists():
logger.warning(f"Cannot check self-update; script not found: {self.script_path}")
return False
try:
latest = self.fetch_latest()
current = self.script_path.read_bytes()
except InstallationError as e:
logger.warning(str(e))
return False
except OSError as e:
logger.warning(f"Cannot read current installer script: {e}")
return False
return latest != current
def notify_if_update_available(self):
if not self.is_update_available():
return
quoted_script = shlex.quote(str(self.script_path))
logger.warning("A newer version of this installer is available.")
logger.warning(f"Run this to update the installer script: sudo python3 {quoted_script} --self-update")
logger.warning("Then rerun your original install/update command.")
def update(self):
if not self.script_path.exists():
raise InstallationError(f"Cannot self-update; script not found: {self.script_path}")
latest = self.fetch_latest()
try:
latest_text = latest.decode("utf-8")
compile(latest_text, str(self.script_path), "exec")
except Exception as e:
raise InstallationError(f"Downloaded installer is not valid Python: {e}")
current = self.script_path.read_bytes()
if latest == current:
logger.info("Installer script is already up to date.")
return
script_stat = self.script_path.stat()
try:
with tempfile.NamedTemporaryFile(
mode="wb",
delete=False,
dir=str(self.script_path.parent),
prefix=f".{self.script_path.name}.",
) as tmp_file:
tmp_file.write(latest)
tmp_path = Path(tmp_file.name)
tmp_path.chmod(script_stat.st_mode)
if hasattr(os, "chown") and os.getuid() == 0:
os.chown(tmp_path, script_stat.st_uid, script_stat.st_gid)
os.replace(tmp_path, self.script_path)
except PermissionError as e:
raise InstallationError(
f"Permission denied updating {self.script_path}. Run with sudo: "
f"sudo python3 {shlex.quote(str(self.script_path))} --self-update"
) from e
except OSError as e:
raise InstallationError(f"Failed to update {self.script_path}: {e}") from e
finally:
if "tmp_path" in locals() and tmp_path.exists():
tmp_path.unlink()
logger.info(f"Installer script updated: {self.script_path}")
class NodeManager:
"""Manages Node.js installation and updates."""
def __init__(self, install_dir: Path, symlink_dir: Path):
self.install_dir = install_dir
self.symlink_dir = symlink_dir
self.arch = SystemUtils.get_node_arch()
def get_target_version(self, lts_only: bool = True) -> Optional[str]:
lines = Downloader.fetch_text(NODE_INDEX_URL).splitlines()
if not lines:
return None
header = lines[0].split('\t')
try:
lts_col = header.index('lts')
ver_col = header.index('version')
except ValueError:
raise InstallationError("Unexpected Node.js index.tab format")
for line in lines[1:]:
parts = line.split('\t')
if len(parts) <= max(lts_col, ver_col):
continue
version = parts[ver_col].strip()
lts_val = parts[lts_col].strip().lower()
is_lts = lts_val not in ('false', '', '-')
if lts_only and not is_lts:
continue
return version
return None
def get_current_version(self) -> str:
node_path = self.install_dir / "bin" / "node"
if node_path.exists():
try:
return subprocess.check_output([str(node_path), "-v"], stderr=subprocess.DEVNULL).decode().strip()
except (subprocess.CalledProcessError, OSError):
return "none"
return "none"
def install(self, version: str):
logger.info(f"Updating Node.js to {version}...")
arch_str = f"linux-{self.arch}"
ext = "tar.xz"
url = f"https://nodejs.org/dist/{version}/node-{version}-{arch_str}.{ext}"
with tempfile.TemporaryDirectory() as tmp_dir_str:
tmp_dir = Path(tmp_dir_str)
archive_path = tmp_dir / f"node.{ext}"
Downloader.download_file(url, archive_path)
logger.info("Extracting Node.js archive...")
try:
with tarfile.open(archive_path, "r:xz") as tar:
extract_kwargs: Dict[str, Any] = {"path": tmp_dir}
if sys.version_info >= (3, 12):
extract_kwargs["filter"] = "data"
tar.extractall(**extract_kwargs)
except Exception as e:
raise InstallationError(f"Extraction failed: {e}")
extracted_folder = tmp_dir / f"node-{version}-{arch_str}"
self.install_dir.mkdir(parents=True, exist_ok=True)
logger.info(f"Syncing Node.js files to {self.install_dir}...")
# Clean up existing installation
for item in self.install_dir.iterdir():
if item.is_dir():
shutil.rmtree(item)
else:
item.unlink()
# Copy new files
for item in extracted_folder.iterdir():
dest = self.install_dir / item.name
if item.is_symlink():
dest.symlink_to(os.readlink(str(item)))
elif item.is_dir():
shutil.copytree(item, dest, symlinks=True)
else:
shutil.copy2(item, dest)
def create_symlinks(self):
for binary in ("node", "npm", "npx"):
src = self.install_dir / "bin" / binary
dst = self.symlink_dir / binary
if not src.exists():
logger.warning(f"Source binary not found: {src}")
continue
# Use relative path for symlink if possible
try:
rel_src = os.path.relpath(src, dst.parent)
except ValueError:
rel_src = str(src)
if os.path.lexists(str(dst)):
if dst.is_symlink() and os.readlink(str(dst)) == rel_src:
logger.info(f"Symlink OK: {dst} -> {rel_src}")
continue
dst.unlink()
try:
dst.symlink_to(rel_src)
logger.info(f"Symlink created: {dst} -> {rel_src}")
except OSError as e:
logger.error(f"Error creating symlink for {binary}: {e}")
class UVManager:
"""Manages uv installation and updates."""
def __init__(self, symlink_dir: Path):
self.symlink_dir = symlink_dir
self.arch = SystemUtils.get_uv_arch()
def get_latest_version(self) -> Optional[str]:
api_url = f"https://api.github.com/repos/{UV_REPO}/releases/latest"
try:
req = urllib.request.Request(api_url)
with urllib.request.urlopen(req) as response:
data = json.loads(response.read().decode())
return data["tag_name"].lstrip('v')
except Exception as e:
logger.warning(f"GitHub API fetch failed for uv: {e}. Trying fallback.")
try:
with urllib.request.urlopen(f"https://github.com/{UV_REPO}/releases/latest") as resp:
return resp.geturl().split('/')[-1].lstrip('v')
except Exception:
return None
def get_current_version(self) -> str:
binary_path = self.symlink_dir / "uv"
if not binary_path.exists():
return "none"
try:
output = subprocess.check_output([str(binary_path), "--version"], stderr=subprocess.STDOUT).decode().strip()
return output.split()[1]
except (subprocess.CalledProcessError, OSError, IndexError):
return "none"
def install(self, version: str):
if not self.arch:
raise InstallationError(f"Unsupported architecture for uv: {platform.machine()}")
logger.info(f"Updating uv to {version}...")
# uv download URL can vary, but usually it matches the tag name.
# If the version doesn't start with 'v', we might need to add it,
# but for recent uv releases the tag name itself is used in the URL.
# Since get_latest_version lstrips 'v', and some releases use 'v' tags,
# we'll try to be smart about it.
url = f"https://github.com/{UV_REPO}/releases/download/{version}/uv-{self.arch}.tar.gz"
with tempfile.TemporaryDirectory() as tmp_dir_str:
tmp_dir = Path(tmp_dir_str)
archive_path = tmp_dir / "uv.tar.gz"
Downloader.download_file(url, archive_path)
logger.info("Extracting uv archive...")
try:
with tarfile.open(archive_path, "r:gz") as tar:
tar.extractall(path=tmp_dir)
except Exception as e:
raise InstallationError(f"Extraction failed: {e}")
extracted_dir = tmp_dir / f"uv-{self.arch}"
if not extracted_dir.exists():
# Fallback: search for uv binary
for root, _, files in os.walk(tmp_dir):
if "uv" in files:
extracted_dir = Path(root)
break
for binary in ("uv", "uvx"):
src = extracted_dir / binary
dst = self.symlink_dir / binary
if src.exists():
logger.info(f"Installing {binary} to {self.symlink_dir}...")
if dst.exists():
dst.unlink()
shutil.copy2(src, dst)
dst.chmod(0o755)
else:
logger.warning(f"Binary {binary} not found in archive.")
class GHManager:
"""Manages GitHub CLI installation and updates."""
def __init__(self, symlink_dir: Path):
self.symlink_dir = symlink_dir
self.arch = SystemUtils.get_node_arch() # x64, arm64, etc.
# Map to GH arch names
self.gh_arch = {
"x64": "amd64",
"arm64": "arm64",
"armv7l": "armv6",
}.get(self.arch, self.arch)
def get_latest_version(self) -> Optional[str]:
api_url = "https://api.github.com/repos/cli/cli/releases/latest"
try:
req = urllib.request.Request(api_url)
# GitHub API requires User-Agent
req.add_header("User-Agent", "Mozilla/5.0")
with urllib.request.urlopen(req) as response:
data = json.loads(response.read().decode())
return data["tag_name"].lstrip('v')
except Exception as e:
logger.warning(f"GitHub API fetch failed for gh: {e}")
return None
def get_current_version(self) -> str:
binary_path = self.symlink_dir / "gh"
if not binary_path.exists():
return "none"
try:
output = subprocess.check_output([str(binary_path), "--version"], stderr=subprocess.STDOUT).decode().strip()
# gh version 2.47.0 (2024-04-03)
# Find first word that starts with digit and has a dot
match = re.search(r"version\s+v?(\d+\.\d+\.\d+)", output, re.IGNORECASE)
if match:
return match.group(1)
return "none"
except (subprocess.CalledProcessError, OSError):
return "none"
def install(self, version: str):
logger.info(f"Updating gh to {version}...")
url = f"https://github.com/cli/cli/releases/download/v{version}/gh_{version}_linux_{self.gh_arch}.tar.gz"
with tempfile.TemporaryDirectory() as tmp_dir_str:
tmp_dir = Path(tmp_dir_str)
archive_path = tmp_dir / "gh.tar.gz"
Downloader.download_file(url, archive_path)
logger.info("Extracting gh archive...")
try:
with tarfile.open(archive_path, "r:gz") as tar:
tar.extractall(path=tmp_dir)
except Exception as e:
raise InstallationError(f"Extraction failed: {e}")
# gh extracts to gh_{version}_linux_{gh_arch}/bin/gh
extracted_dir = tmp_dir / f"gh_{version}_linux_{self.gh_arch}"
src = extracted_dir / "bin" / "gh"
if not src.exists():
# Fallback: find gh binary
for root, _, files in os.walk(tmp_dir):
if "gh" in files and "bin" in root:
src = Path(root) / "gh"
break
dst = self.symlink_dir / "gh"
if src.exists():
logger.info(f"Installing gh to {self.symlink_dir}...")
if dst.exists():
dst.unlink()
shutil.copy2(src, dst)
dst.chmod(0o755)
else:
raise InstallationError("gh binary not found in archive.")
class TeaManager:
"""Manages Gitea CLI (tea) installation and updates."""
def __init__(self, symlink_dir: Path):
self.symlink_dir = symlink_dir
self.arch = SystemUtils.get_node_arch()
# Map to tea arch names
self.tea_arch = {
"x64": "amd64",
"arm64": "arm64",
"armv7l": "arm-7",
}.get(self.arch, self.arch)
def get_latest_version(self) -> Optional[str]:
api_url = "https://gitea.com/api/v1/repos/gitea/tea/releases/latest"
try:
with urllib.request.urlopen(api_url) as response:
data = json.loads(response.read().decode())
return data["tag_name"].lstrip('v')
except Exception as e:
logger.warning(f"Gitea API fetch failed for tea: {e}")
return None
def get_current_version(self) -> str:
binary_path = self.symlink_dir / "tea"
if not binary_path.exists():
return "none"
try:
# Version: 0.14.0 golang: 1.26.2 go-sdk: v0.24.1
output = subprocess.check_output([str(binary_path), "--version"], stderr=subprocess.STDOUT).decode().strip()
# The output may contain ANSI color codes (e.g., ^[[1m0.14.0^[[0m)
# and may be separated by tabs or spaces.
parts = output.split()
if len(parts) > 1 and "version" in parts[0].lower():
# Pick the second part (the version) and strip anything that's not a digit or dot
match = re.search(r"(\d+\.\d+\.\d+)", parts[1])
if match:
return match.group(1)
return "none"
except (subprocess.CalledProcessError, OSError):
return "none"
def install(self, version: str):
logger.info(f"Updating tea to {version}...")
# tea-0.14.0-linux-amd64
url = f"https://gitea.com/gitea/tea/releases/download/v{version}/tea-{version}-linux-{self.tea_arch}"
dst = self.symlink_dir / "tea"
with tempfile.NamedTemporaryFile(delete=False) as tmp_file:
tmp_path = Path(tmp_file.name)
try:
Downloader.download_file(url, tmp_path)
logger.info(f"Installing tea to {self.symlink_dir}...")
if dst.exists():
dst.unlink()
shutil.copy2(tmp_path, dst)
dst.chmod(0o755)
finally:
if tmp_path.exists():
tmp_path.unlink()
class AgyManager:
"""Manages antigravity-cli (agy) installation and updates."""
INSTALL_SCRIPT_URL = "https://antigravity.google/cli/install.sh"
def __init__(self, install_dir: Path):
self.install_dir = install_dir
def is_installed(self) -> bool:
return (self.install_dir / "agy.bin").exists()
def cleanup_old_backups(self):
for f in self.install_dir.glob("agy.bin.*.old"):
try:
f.unlink()
logger.info(f"Deleted agy backup: {f}")
except OSError as e:
logger.warning(f"Failed to delete agy backup {f}: {e}")
def update(self):
logger.info("agy already installed. Running 'agy.bin update'...")
try:
subprocess.check_call([str(self.install_dir / "agy.bin"), "update"])
except subprocess.CalledProcessError as e:
raise InstallationError(f"'agy.bin update' failed: {e}")
self.cleanup_old_backups()
def install(self):
logger.info("Installing antigravity-cli...")
with tempfile.TemporaryDirectory() as tmp_dir_str:
script_path = Path(tmp_dir_str) / "install.sh"
Downloader.download_file(self.INSTALL_SCRIPT_URL, script_path)
script_path.chmod(0o755)
try:
# The antigravity install.sh takes a target *directory* via
# --dir and writes "<dir>/agy". It ignores env vars and
# otherwise defaults to "$HOME/.local/bin" (i.e. /root/.local/bin
# under sudo), so pass --dir explicitly to land in install_dir.
subprocess.check_call(
["bash", str(script_path), "--dir", str(self.install_dir)],
)
except subprocess.CalledProcessError as e:
raise InstallationError(f"antigravity-cli install script failed: {e}")
# Rename agy -> agy.bin so the user's /usr/local/bin/agy wrapper is not clobbered.
agy_plain = self.install_dir / "agy"
agy_bin = self.install_dir / "agy.bin"
if agy_plain.exists() and not agy_plain.is_symlink():
if agy_bin.exists():
agy_bin.unlink()
agy_plain.rename(agy_bin)
logger.info(f"Renamed {agy_plain} -> {agy_bin}")
elif not agy_bin.exists():
raise InstallationError("antigravity-cli install did not produce agy or agy.bin")
logger.info("antigravity-cli installed.")
def install_or_update(self):
if self.is_installed():
self.update()
else:
self.install()
self.cleanup_old_backups()
class ScriptManager:
"""Manages shell script installation."""
def __init__(self, name: str, content: str, symlink_dir: Path):
self.name = name
self.content = content
self.dest_path = symlink_dir / name
def install(self):
logger.info(f"Checking {self.name} script at {self.dest_path}...")
needs_write = True
if self.dest_path.exists():
try:
current_content = self.dest_path.read_text()
if current_content == self.content:
logger.info(f"{self.name} script is already up to date.")
needs_write = False
else:
logger.info(f"{self.name} script is different. Overwriting...")
except Exception as e:
logger.warning(f"Failed to read existing {self.name} script: {e}. Overwriting...")
if needs_write:
logger.info(f"Writing {self.name} script to {self.dest_path}...")
try:
self.dest_path.write_text(self.content)
self.dest_path.chmod(0o755)
logger.info(f"{self.name} script installed successfully.")
except Exception as e:
raise InstallationError(f"Failed to install {self.name} script: {e}")
def main():
if platform.system().lower() != "linux":
logger.error("This script is only supported on Linux.")
sys.exit(1)
parser = argparse.ArgumentParser(description="Install or update Node.js and uv on Linux")
parser.add_argument("--node-latest", action="store_true", help="Install latest Node.js release")
parser.add_argument("--node-lts", action="store_true", default=True, help="Install latest Node.js LTS (default)")
parser.add_argument("--install-dir", type=Path, default=DEFAULT_INSTALL_DIR, help="Node.js install directory")
parser.add_argument("--symlink-dir", type=Path, default=SYMLINK_DIR, help="Binary symlink directory")
parser.add_argument("--skip-node", action="store_true", help="Skip Node.js installation")
parser.add_argument("--skip-uv", action="store_true", help="Skip uv installation")
parser.add_argument("--skip-gh", action="store_true", help="Skip GitHub CLI installation")
parser.add_argument("--skip-tea", action="store_true", help="Skip Gitea CLI installation")
parser.add_argument("--skip-bun", action="store_true", help="Skip bun wrapper installation")
parser.add_argument("--skip-bunx", action="store_true", help="Skip bunx wrapper installation")
parser.add_argument("--install-gemini", action="store_true", help="Install gemini wrapper (deprecated, opt-in only)")
parser.add_argument("--skip-omnigent", action="store_true", help="Skip omnigent wrapper installation")
parser.add_argument("--skip-agy", action="store_true", help="Skip antigravity-cli installation")
parser.add_argument("--skip-jcodemunch-mcp", action="store_true", help="Skip jcodemunch-mcp wrapper installation")
parser.add_argument("--skip-context7-mcp", action="store_true", help="Skip context7-mcp wrapper installation")
parser.add_argument("-U", "--self-update", action="store_true", help="Update this installer script and exit")
parser.add_argument(
"--skip-self-update-check",
action="store_true",
help="Skip checking whether a newer installer script is available",
)
args = parser.parse_args()
self_updater = SelfUpdater(SELF_UPDATE_URL)
if args.self_update:
try:
self_updater.update()
except InstallationError as e:
logger.error(str(e))
sys.exit(1)
return
if not args.skip_self_update_check:
self_updater.notify_if_update_available()
# Permission check
if not SystemUtils.check_permissions(args.symlink_dir):
logger.error(f"Need 'sudo' or write access to {args.symlink_dir}")
sys.exit(1)
try:
# 1. Node.js
if not args.skip_node:
node = NodeManager(args.install_dir, args.symlink_dir)
target_node = node.get_target_version(lts_only=not args.node_latest)
current_node = node.get_current_version()
logger.info(f"Node.js - Current: {current_node}, Target: {target_node}")
if target_node and current_node != target_node:
node.install(target_node)
node.create_symlinks()
else:
logger.info("Node.js is already up to date.")
node.create_symlinks()
# 1.5 Shell scripts
ScriptManager("codex", CODEX_SCRIPT_CONTENT, args.symlink_dir).install()
if args.install_gemini:
ScriptManager("gemini", GEMINI_SCRIPT_CONTENT, args.symlink_dir).install()
ScriptManager("claude", CLAUDE_SCRIPT_CONTENT, args.symlink_dir).install()
if not args.skip_bun:
ScriptManager("bun", BUN_SCRIPT_CONTENT, args.symlink_dir).install()
if not args.skip_bunx:
ScriptManager("bunx", BUNX_SCRIPT_CONTENT, args.symlink_dir).install()
# 2. uv
if not args.skip_uv:
uv = UVManager(args.symlink_dir)
target_uv = uv.get_latest_version()
current_uv = uv.get_current_version()
logger.info(f"uv - Current: {current_uv}, Target: {target_uv}")
if target_uv and current_uv != target_uv:
uv.install(target_uv)
else:
logger.info("uv is already up to date.")
if not args.skip_omnigent:
ScriptManager("omnigent", OMNIGENT_SCRIPT_CONTENT, args.symlink_dir).install()
# 3. gh
if not args.skip_gh:
gh = GHManager(args.symlink_dir)
target_gh = gh.get_latest_version()
current_gh = gh.get_current_version()
logger.info(f"gh - Current: {current_gh}, Target: {target_gh}")
if target_gh and current_gh != target_gh:
gh.install(target_gh)
else:
logger.info("gh is already up to date.")
# 4. tea
if not args.skip_tea:
tea = TeaManager(args.symlink_dir)
target_tea = tea.get_latest_version()
current_tea = tea.get_current_version()
logger.info(f"tea - Current: {current_tea}, Target: {target_tea}")
if target_tea and current_tea != target_tea:
tea.install(target_tea)
else:
logger.info("tea is already up to date.")
# 5. agy (antigravity-cli)
if not args.skip_agy:
AgyManager(args.symlink_dir).install_or_update()
ScriptManager("agy", AGY_SCRIPT_CONTENT, args.symlink_dir).install()
# 6. MCP servers
if not args.skip_jcodemunch_mcp:
ScriptManager("jcodemunch-mcp", JCODEMUNCH_MCP_SCRIPT_CONTENT, args.symlink_dir).install()
if not args.skip_context7_mcp:
ScriptManager("context7-mcp", CONTEXT7_MCP_SCRIPT_CONTENT, args.symlink_dir).install()
logger.info("Installation/Update process complete.")
except InstallationError as e:
logger.error(str(e))
sys.exit(1)
except Exception as e:
logger.exception(f"An unexpected error occurred: {e}")
sys.exit(1)
if __name__ == "__main__":
main()Filename: apex-runner.service
# Filename: /etc/systemd/system/apex-runner.service
#
# Copy this file to /etc/systemd/system/apex-runner.service and run:
# omnigent login https://apex.example.com
# systemctl daemon-reload; systemctl enable --now apex-runner
#
# Note:
# 1. Change apex.example.com
# 2. Change example_user
[Unit]
Description=Omnigent Runner
Documentation=https://omnigent.io
After=network-online.target
Wants=network-online.target
StartLimitIntervalSec=300
StartLimitBurst=5
[Service]
Type=simple
# Run as non-root
User=example_user
Group=example_user
WorkingDirectory=/home/example_user
# Ensure the binary can be found
Environment="PATH=/usr/local/bin:/usr/local/sbin:/usr/bin:/usr/sbin:/bin:/sbin"
# Login command
ExecStart=/usr/local/bin/omnigent host https://apex.example.com
# Restart policy
Restart=on-failure
RestartSec=15s
# Graceful shutdown
TimeoutStartSec=60
TimeoutStopSec=30
KillMode=process
# Logging
StandardOutput=journal
StandardError=journal
SyslogIdentifier=apex-runner
[Install]
WantedBy=multi-user.target