diff --git a/.devcontainer/Makefile b/.devcontainer/Makefile new file mode 100644 index 0000000..55382b2 --- /dev/null +++ b/.devcontainer/Makefile @@ -0,0 +1,37 @@ +# Makefile for hermes-codespace (inside .devcontainer/) + +# === Upstream Sync Configuration === +# Update these to match your upstream template repository +EXTERNAL_DIR := /tmp/vendor/hermes-codespace$(shell bash -c 'echo $$RANDOM') +EXTERNAL_REPO := https://github.com/gitricko/hermes-codespace +EXTERNAL_DEVC := $(EXTERNAL_DIR)/.devcontainer + +# === Development Commands === + +install: + @echo "Installing dependencies..." + @bash post-create-cmd.sh + +ext-install: + @echo "Installing external dependencies..." + mkdir -p ~/.cline/data + cp globalState.json ~/.cline/data/ + cp secrets.json ~/.cline/data/ + code --force --install-extension saoudrizwan.claude-dev +# === Template Update Commands === + +# Use this command to update your .devcontainer folder with latest from upstream +# After you've converted your forked repo to private (Settings → Danger Zone → Leave fork network) +.PHONY: update-deps + +update-deps: + @echo "Syncing .devcontainer from upstream..." + @if [ ! -f "$(EXTERNAL_DEVC)" ]; then \ + echo "Cloning external dependency..."; \ + mkdir -p $(dir $(EXTERNAL_DEVC)); \ + git clone --depth 1 --branch main $(EXTERNAL_REPO) $(EXTERNAL_DIR); \ + fi + rsync -av --exclude='.*' "$(EXTERNAL_DEVC)/" "." + @echo "" + @echo "Done! Review changes with: git diff .devcontainer/" + @echo "If satisfied, commit: git add -A && git commit -m 'Update .devcontainer from upstream'" diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..89a945c --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,9 @@ +{ + "name": "Hermes-Coding-Agent", + "customizations": { + "vscode": { + "extensions": ["joaompfp.hermes-ai-agent"] + } + }, + "postCreateCommand": "bash ./.devcontainer/post-create.sh > /tmp/post-create.log" +} diff --git a/.devcontainer/free-disk.sh b/.devcontainer/free-disk.sh new file mode 100644 index 0000000..8ed5536 --- /dev/null +++ b/.devcontainer/free-disk.sh @@ -0,0 +1,483 @@ +#!/usr/bin/env bash +# free-disk-space.sh +# Replicates jlumbroso/free-disk-space GitHub Action + adds Codespace-specific cleanup +# Run as: sudo bash free-disk-space.sh [--dry-run] +# +# Options: +# --dry-run Show what would be removed without deleting anything +# --force Skip confirmation prompts +# --keep-go Skip Go cleanup +# --keep-llvm Skip LLVM/Clang cleanup +# --keep-conda Skip Conda cleanup +# --keep-nvm Skip Node.js/NVM cleanup +# --keep-java Skip Java cleanup +# --keep-ruby Skip Ruby/RVM cleanup +# --skip Skip a specific step (android, dotnet, haskell, apt-large, +# docker-images, swap, conda, llvm, go, valgrind, cmake, +# tmp, python-dev, gcc, nvm, java, ruby) + +set -eo pipefail + +# --- Helpers --- + +print_sep() { + local ch="${1:-=}" width="${2:-80}" + printf '%*s\n' "$width" '' | tr ' ' "$ch" +} + +get_available_kb() { + df -a "$1" | awk 'NR > 1 {sum += $4} END {print sum+0}' +} + +# Human-readable byte formatter — pure awk, no numfmt/bc required +format_human() { + local kb=$1 + awk -v kb="$kb" ' + BEGIN { + if (kb >= 1048576) { printf "%.1f GB", kb/1048576 } + else if (kb >= 1024) { printf "%.1f MB", kb/1024 } + else if (kb > 0) { printf "%.0f KB", kb } + else { printf "0 KB" } + }' +} + +# Estimate size of a path in KB before deleting it +estimate_size_kb() { + local path=$1 + if [[ -d "$path" ]]; then + du -sk "$path" 2>/dev/null | awk '{print $1}' + elif [[ -f "$path" ]]; then + stat -c%s "$path" 2>/dev/null | awk '{print int($1/1024)}' + else + echo 0 + fi +} + +saved() { + local before=$1 title="${2:-}" + local after end_saved + [[ -z "$before" ]] && return + after=$(get_available_kb '/') + end_saved=$((after - before)) + if [[ -n "$title" ]]; then + print_sep '*' + printf "=> %s: Saved %s\n" "$title" "$(format_human $end_saved)" + print_sep '*' + echo + fi +} + +warn() { echo "[WARN] $*" >&2; } +info() { echo "[INFO] $*"; } +die() { echo "[ERROR] $*" >&2; exit 1; } + +remove() { + local label="$1"; shift + local target="$1" + local estimated + + if [[ ! -e "$target" ]] && [[ ! -e $(dirname "$target" 2>/dev/null) ]]; then + info "[skip] $label — not found" + return 0 + fi + + estimated=$(estimate_size_kb "$target") + if [[ "$DRY_RUN" == "1" ]]; then + info "[DRY-RUN] Would remove: $target ($(format_human $estimated))" + return 0 + fi + + if [[ "$FORCE" != "1" ]] && [[ "$estimated" -gt 0 ]]; then + info "Removing $label ($(format_human $estimated))..." + fi + sudo rm -rf "$target" 2>/dev/null || warn "Failed to remove: $target" +} + +snapshot_kb() { get_available_kb '/'; } + +# --- Argument parsing --- + +DRY_RUN="0" +FORCE="0" +SKIP_DOCKER="0" +SKIP_GO="0" +SKIP_LLVM="0" +SKIP_CONDA="0" +SKIP_NVM="0" +SKIP_JAVA="0" +SKIP_RUBY="0" +SKIP_OTHERS="" + +should_skip() { + local step=$1 + for s in $SKIP_OTHERS; do + [[ "$s" == "$step" ]] && return 0 + done + case "$step" in + docker-images) [[ "$SKIP_DOCKER" == "1" ]] && return 0 ;; + go) [[ "$SKIP_GO" == "1" ]] && return 0 ;; + llvm) [[ "$SKIP_LLVM" == "1" ]] && return 0 ;; + conda) [[ "$SKIP_CONDA" == "1" ]] && return 0 ;; + nvm) [[ "$SKIP_NVM" == "1" ]] && return 0 ;; + java) [[ "$SKIP_JAVA" == "1" ]] && return 0 ;; + ruby) [[ "$SKIP_RUBY" == "1" ]] && return 0 ;; + esac + return 1 +} + +for arg in "$@"; do + case "$arg" in + --dry-run) DRY_RUN="1" ;; + --force) FORCE="1" ;; + --keep-docker) SKIP_DOCKER="1" ;; + --keep-go) SKIP_GO="1" ;; + --keep-llvm) SKIP_LLVM="1" ;; + --keep-conda) SKIP_CONDA="1" ;; + --keep-nvm) SKIP_NVM="1" ;; + --keep-java) SKIP_JAVA="1" ;; + --keep-ruby) SKIP_RUBY="1" ;; + --skip) + # handled in next iteration; accumulate following args as skip targets + ;; + --skip=*) + SKIP_OTHERS="${SKIP_OTHERS} ${arg#--skip=}" + ;; + --help) + echo "Usage: $0 [--dry-run] [--force] [--keep-docker] [--keep-go]" + echo " [--keep-llvm] [--keep-conda] [--keep-nvm] [--keep-java]" + echo " [--keep-ruby] [--skip ]" + echo "" + echo "Options:" + echo " --dry-run Preview what would be removed (no deletions)" + echo " --force Skip confirmation prompts" + echo " --keep-docker Skip Docker image prune" + echo " --keep-go Skip Go cleanup" + echo " --keep-llvm Skip LLVM/Clang cleanup" + echo " --keep-conda Skip Conda cleanup" + echo " --keep-nvm Skip Node.js/NVM cleanup" + echo " --keep-java Skip Java cleanup" + echo " --keep-ruby Skip Ruby/RVM cleanup" + echo " --skip Skip a specific step (see script header for list)" + exit 0 + ;; + *) + # collect positional skip targets (after --skip flag) + if [[ "$prev_arg" == "--skip" ]]; then + SKIP_OTHERS="${SKIP_OTHERS} $arg" + fi + ;; + esac + prev_arg="$arg" +done + +# ============================================================================= +# PRE-CLEANUP REPORT +# ============================================================================= + +echo "" +print_sep '=' +echo "DISK SPACE BEFORE CLEAN-UP:" +echo "" +df -h / +echo "" +print_sep '=' + +BEFORE_TOTAL=$(snapshot_kb) +TOTAL_SAVED=0 + +# ============================================================================= +# STEP 1: GitHub Actions runner cleanup (jlumbroso/free-disk-space) +# ============================================================================= + +echo "" +echo "=== GitHub Actions Runner Cleanup ===" + +# 1a. Android SDK +STEP_BEFORE=$(snapshot_kb) +remove "Android SDK" "/usr/local/lib/android" +saved $STEP_BEFORE "Android SDK" +[[ $? -eq 0 ]] && TOTAL_SAVED=$((TOTAL_SAVED + $(snapshot_kb) - STEP_BEFORE)) || true + +# 1b. .NET runtime +STEP_BEFORE=$(snapshot_kb) +remove ".NET runtime" "/usr/share/dotnet" +saved $STEP_BEFORE ".NET runtime" + +# 1c. Haskell GHC + GHCup +STEP_BEFORE=$(snapshot_kb) +remove "Haskell GHC" "/opt/ghc" +remove "GHCup" "/usr/local/.ghcup" +saved $STEP_BEFORE "Haskell GHC" + +# 1d. Large apt packages (regex-matched toolchains) +STEP_BEFORE=$(snapshot_kb) +if [[ "$DRY_RUN" == "1" ]]; then + info "[DRY-RUN] Would apt-get remove:" + info " aspnetcore-*, dotnet-*, llvm-*, php*, mongodb-*, mysql-*" + info " azure-cli, google-chrome-stable, firefox, powershell" + info " mono-devel, libgl1-mesa-dri, google-cloud-sdk, google-cloud-cli" +else + info "Removing large apt packages..." + sudo apt-get remove -y \ + '^aspnetcore-.*' \ + '^dotnet-.*' \ + '^llvm-.*' \ + 'php.*' \ + '^mongodb-.*' \ + '^mysql-.*' \ + azure-cli \ + google-chrome-stable \ + firefox \ + powershell \ + mono-devel \ + libgl1-mesa-dri \ + google-cloud-sdk \ + google-cloud-cli \ + --fix-missing 2>/dev/null || true + sudo apt-get autoremove -y 2>/dev/null || true + sudo apt-get clean -y 2>/dev/null || true +fi +saved $STEP_BEFORE "Large apt packages" + +# 1e. Docker images prune (non-destructive — just unused images) +if command -v docker &>/dev/null && ! should_skip docker-images; then + STEP_BEFORE=$(snapshot_kb) + if [[ "$DRY_RUN" == "1" ]]; then + info "[DRY-RUN] Would prune all Docker images" + else + info "Pruning Docker images..." + sudo docker image prune --all --force 2>/dev/null || true + fi + saved $STEP_BEFORE "Docker images" +elif should_skip docker-images; then + info "[skip] Docker images — disabled via --keep-docker / --skip docker-images" +fi + +# 1f. Tool cache ($AGENT_TOOLSDIRECTORY) +if [[ -n "${AGENT_TOOLSDIRECTORY:-}" ]]; then + STEP_BEFORE=$(snapshot_kb) + remove "Tool cache" "$AGENT_TOOLSDIRECTORY" + saved $STEP_BEFORE "Tool cache" +fi + +# 1g. Swap +STEP_BEFORE=$(snapshot_kb) +if [[ "$DRY_RUN" == "1" ]]; then + info "[DRY-RUN] Would disable swap and remove /mnt/swapfile" +else + sudo swapoff -a 2>/dev/null || true + sudo rm -f /mnt/swapfile 2>/dev/null || true +fi +saved $STEP_BEFORE "Swap storage" + +# ============================================================================= +# STEP 2: Codespace-specific cleanup +# ============================================================================= + +echo "" +echo "=== Codespace/GitHub Codespaces Cleanup ===" + +# 2a. Conda +if ! should_skip conda; then + STEP_BEFORE=$(snapshot_kb) + remove "Conda" "/opt/conda" + saved $STEP_BEFORE "Conda (/opt/conda)" +else + info "[skip] Conda — disabled via --keep-conda / --skip conda" +fi + +# 2b. LLVM/Clang +if ! should_skip llvm; then + STEP_BEFORE=$(snapshot_kb) + remove "LLVM-18 toolchain" "/usr/lib/llvm-18" + saved $STEP_BEFORE "LLVM-18 toolchain" +else + info "[skip] LLVM-18 toolchain — disabled via --keep-llvm / --skip llvm" +fi + +# 2c. Docker/Moby packages +# By default: SKIPPED (user needs docker) +# To remove: --skip docker-images (removes moby packages, keeps daemon) +# To skip (keep): already default — use --keep-docker or --skip docker-images +if command -v docker &>/dev/null; then + if should_skip docker-images; then + info "[skip] Docker/Moby packages — preserved via --keep-docker / --skip docker-images" + fi +fi + +# 2d. Go +if ! should_skip go; then + STEP_BEFORE=$(snapshot_kb) + remove "Go" "/usr/local/go" + saved $STEP_BEFORE "Go (/usr/local/go)" +else + info "[skip] Go — disabled via --keep-go / --skip go" +fi + +# 2e. Valgrind +if ! should_skip valgrind; then + STEP_BEFORE=$(snapshot_kb) + if [[ "$DRY_RUN" == "1" ]]; then + info "[DRY-RUN] Would remove valgrind packages" + else + info "Removing valgrind..." + sudo apt-get remove -y valgrind 2>/dev/null || true + sudo apt-get autoremove -y 2>/dev/null || true + fi + saved $STEP_BEFORE "Valgrind" +else + info "[skip] Valgrind — disabled via --skip valgrind" +fi + +# 2f. vim-runtime (full vim docs — use vim-tiny or neovim instead) +STEP_BEFORE=$(snapshot_kb) +remove "vim-runtime" "/usr/share/vim/vimfiles" 2>/dev/null || true +remove "vim-runtime" "/usr/share/vim/addons" 2>/dev/null || true +# Only remove the docs, keep the binary +if [[ -d /usr/share/vim/vim91/doc ]] || [[ -d /usr/share/vim/vim90/doc ]]; then + if [[ "$DRY_RUN" != "1" ]]; then + sudo rm -rf /usr/share/vim/*/doc/*.txt /usr/share/vim/*/doc/*.help 2>/dev/null || true + else + info "[DRY-RUN] Would trim vim runtime docs" + fi +fi +saved $STEP_BEFORE "vim runtime docs" + +# 2g. cmake +if ! should_skip cmake; then + STEP_BEFORE=$(snapshot_kb) + if [[ "$DRY_RUN" == "1" ]]; then + info "[DRY-RUN] Would remove cmake" + else + sudo apt-get remove -y cmake 2>/dev/null || true + sudo apt-get autoremove -y 2>/dev/null || true + fi + saved $STEP_BEFORE "cmake" +else + info "[skip] cmake — disabled via --skip cmake" +fi + +# 2h. /tmp cleanup +if ! should_skip tmp; then + STEP_BEFORE=$(snapshot_kb) + if [[ "$DRY_RUN" == "1" ]]; then + info "[DRY-RUN] Would clean /tmp (excluding tmpfs)" + else + if [[ -d /tmp ]] && ! mountpoint -q /tmp 2>/dev/null; then + sudo rm -rf /tmp/* 2>/dev/null || true + fi + fi + saved $STEP_BEFORE "/tmp files" +else + info "[skip] /tmp — disabled via --skip tmp" +fi + +# 2i. Python dev headers +if ! should_skip python-dev; then + STEP_BEFORE=$(snapshot_kb) + if [[ "$DRY_RUN" == "1" ]]; then + info "[DRY-RUN] Would remove libpython3.12-dev" + else + sudo apt-get remove -y libpython3.12-dev 2>/dev/null || true + sudo apt-get autoremove -y 2>/dev/null || true + fi + saved $STEP_BEFORE "Python dev headers" +else + info "[skip] Python dev headers — disabled via --skip python-dev" +fi + +# 2j. GCC/G++ dev packages +if ! should_skip gcc; then + STEP_BEFORE=$(snapshot_kb) + if [[ "$DRY_RUN" == "1" ]]; then + info "[DRY-RUN] Would remove GCC/G++ dev packages" + else + sudo apt-get remove -y gcc-13 g++-13 cpp-13 gcc-13-x86-64-linux-gnu 2>/dev/null || true + sudo apt-get autoremove -y 2>/dev/null || true + fi + saved $STEP_BEFORE "GCC/G++ dev packages" +else + info "[skip] GCC/G++ dev packages — disabled via --skip gcc" +fi + +# 2k. Node.js / NVM (Node Version Manager) +# NVM stores versions under /usr/local/share/nvm/versions/node (~426 MB for v24) +# Also clean ~/.cache/node to recover npm cache +if ! should_skip nvm; then + STEP_BEFORE=$(snapshot_kb) + if [[ "$DRY_RUN" == "1" ]]; then + nvm_size=$(du -sk /usr/local/share/nvm 2>/dev/null | awk '{print $1}' || echo 0) + info "[DRY-RUN] Would remove NVM (${nvm_size} KB)" + else + info "Removing NVM..." + sudo rm -rf /usr/local/share/nvm 2>/dev/null || true + sudo rm -rf ~/.nvm ~/.cache/node 2>/dev/null || true + # Clean up shell integration + sudo sed -i '/NVM_DIR/d' ~/.bashrc 2>/dev/null || true + info "NVM removed. Re-install with: curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash" + fi + saved $STEP_BEFORE "Node.js / NVM" +else + info "[skip] Node.js / NVM — disabled via --keep-nvm / --skip nvm" +fi + +# 2l. Java / JDK +# Codespaces uses /home/codespace/java/current as the managed JDK path +# Also remove /usr/lib/jvm (system-wide OpenJDK packages) +if ! should_skip java; then + STEP_BEFORE=$(snapshot_kb) + if [[ "$DRY_RUN" == "1" ]]; then + java_size=$(du -sk /home/codespace/java 2>/dev/null | awk '{print $1}' || echo 0) + info "[DRY-RUN] Would remove Java (${java_size} KB)" + else + info "Removing Java..." + sudo rm -rf /home/codespace/java 2>/dev/null || true + sudo apt-get remove -y --allow-change-held-packages \ + openjdk-.*-jre-headless openjdk-.*-jdk-headless 2>/dev/null || true + sudo apt-get autoremove -y 2>/dev/null || true + fi + saved $STEP_BEFORE "Java / JDK" +else + info "[skip] Java / JDK — disabled via --keep-java / --skip java" +fi + +# 2m. Ruby / RVM +# RVM installs rubies under /usr/local/rvm/rubies (~152 MB for ruby-3.4.7) +# Also removes gem cache and gemsets +if ! should_skip ruby; then + STEP_BEFORE=$(snapshot_kb) + if [[ "$DRY_RUN" == "1" ]]; then + ruby_size=$(du -sk /usr/local/rvm 2>/dev/null | awk '{print $1}' || echo 0) + info "[DRY-RUN] Would remove Ruby/RVM (${ruby_size} KB)" + else + info "Removing Ruby/RVM..." + sudo rm -rf /usr/local/rvm 2>/dev/null || true + sudo rm -rf /usr/local/rvm /var/lib/gems/2.7 /var/lib/gems/3.0 /var/lib/gems/3.4 2>/dev/null || true + sudo rm -rf ~/.rvm 2>/dev/null || true + # Clean up shell integration + sudo sed -i '/rvm\/scripts\/rvm/d' ~/.bashrc 2>/dev/null || true + sudo rm -f /etc/profile.d/rvm.sh 2>/dev/null || true + fi + saved $STEP_BEFORE "Ruby / RVM" +else + info "[skip] Ruby / RVM — disabled via --keep-ruby / --skip ruby" +fi + +# ============================================================================= +# SUMMARY +# ============================================================================= + +AFTER_TOTAL=$(snapshot_kb) +TOTAL_FREED=$((AFTER_TOTAL - BEFORE_TOTAL)) + +echo "" +print_sep '=' +echo "DISK SPACE AFTER CLEAN-UP:" +echo "" +df -h / +echo "" +print_sep '=' +echo "" +echo "Total space recovered: $(format_human $TOTAL_FREED)" +echo "" +echo "Done." diff --git a/.devcontainer/globalState.json b/.devcontainer/globalState.json new file mode 100644 index 0000000..9fd11f9 --- /dev/null +++ b/.devcontainer/globalState.json @@ -0,0 +1,51 @@ +{ + "welcomeViewCompleted": true, + "__vscodeMigrationVersion": 1, + "clineVersion": "3.83.0", + "remoteRulesToggles": {}, + "remoteWorkflowToggles": {}, + "remoteSkillsToggles": {}, + "openAiHeaders": {}, + "sapAiCoreUseOrchestrationMode": true, + "ocaMode": "internal", + "planModeApiProvider": "openai", + "actModeApiProvider": "openai", + "openAiBaseUrl": "http://localhost:7352/v1", + "planModeOpenAiModelId": "auto-fastest", + "actModeOpenAiModelId": "auto-fastest", + "azureApiVersion": "", + "lastShownAnnouncementId": "3.83", + "dismissedBanners": [ + { + "bannerId": "bnr-01KQ02NN40X7WTJMKQC610GS5M", + "dismissedAt": 1778856185168 + } + ], + "autoApprovalSettings": { + "version": 2, + "enabled": true, + "favorites": [], + "maxRequests": 20, + "actions": { + "readFiles": true, + "readFilesExternally": false, + "editFiles": false, + "editFilesExternally": false, + "executeSafeCommands": true, + "executeAllCommands": false, + "useBrowser": false, + "useMcp": true + }, + "enableNotifications": false + }, + "workspaceRoots": [ + { + "path": "/config/Desktop", + "name": "Desktop", + "vcs": "none" + } + ], + "primaryRootIndex": 0, + "globalWorkflowToggles": {}, + "globalClineRulesToggles": {} +} \ No newline at end of file diff --git a/.devcontainer/post-create.sh b/.devcontainer/post-create.sh new file mode 100755 index 0000000..de45047 --- /dev/null +++ b/.devcontainer/post-create.sh @@ -0,0 +1,27 @@ +#!/bin/bash + +# Install modelrelay globally +sudo npm install modelrelay -g --prefix /usr/local/lib/modelrelay +sudo ln -sf /usr/local/lib/modelrelay/bin/modelrelay /usr/local/bin/modelrelay +sudo npm cache clean --force + +echo "[post-create-cmd.sh] Checking modelrelay..." +if command -v modelrelay &>/dev/null; then + if pgrep -f modelrelay > /dev/null; then + echo "[post-create-cmd.sh] modelrelay is already running, skipping" + else + echo "[post-create-cmd.sh] Starting modelrelay in the background..." + setsid /usr/local/bin/modelrelay >> /tmp/modelrelay.log 2>&1 & + fi +else + echo "[post-create-cmd.sh] modelrelay not found, skipping start" +fi + +# Install Cline with default configuration +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +echo "[post-create-cmd.sh] Installing Cline with default configuration..." +mkdir -p "$HOME/.cline/data" +cp "${SCRIPT_DIR}/globalState.json" "$HOME/.cline/data/globalState.json" +cp "${SCRIPT_DIR}/secrets.json" "$HOME/.cline/data/secrets.json" +bash -c 'code --force --install-extension saoudrizwan.claude-dev' +npm install -g cline diff --git a/.devcontainer/post-start.sh b/.devcontainer/post-start.sh new file mode 100755 index 0000000..9598af7 --- /dev/null +++ b/.devcontainer/post-start.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +echo "[post-start-cmd.sh] Checking modelrelay..." +if command -v modelrelay &>/dev/null; then + if pgrep -f modelrelay > /dev/null; then + echo "[post-start-cmd.sh] modelrelay is already running, skipping" + else + echo "[post-start-cmd.sh] Starting modelrelay in the background..." + setsid /usr/local/bin/modelrelay >> /tmp/modelrelay.log 2>&1 & + fi +else + echo "[post-start-cmd.sh] modelrelay not found, skipping start" +fi + +# so that the script doesn't exit immediately before modelrelay has a chance to start properly +sleep 60 \ No newline at end of file diff --git a/.devcontainer/screen-shot.png b/.devcontainer/screen-shot.png new file mode 100644 index 0000000..fcad522 Binary files /dev/null and b/.devcontainer/screen-shot.png differ diff --git a/.devcontainer/secrets.json b/.devcontainer/secrets.json new file mode 100644 index 0000000..8fa649d --- /dev/null +++ b/.devcontainer/secrets.json @@ -0,0 +1,3 @@ +{ + "openAiApiKey": " " +} \ No newline at end of file diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..20b0346 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,11 @@ +version: 2 +updates: + - package-ecosystem: "docker" + directory: "/" + schedule: + interval: "weekly" + + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" \ No newline at end of file diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml new file mode 100644 index 0000000..5ebe594 --- /dev/null +++ b/.github/workflows/docker-publish.yml @@ -0,0 +1,126 @@ +name: Build and Push Docker Image + +permissions: + contents: read + +on: + push: + branches: [ main ] + paths: + - 'docker/**' + - '.github/workflows/**' + + pull_request: + branches: [ main ] + paths: + - 'docker/**' + - '.github/workflows/**' + + release: + types: [ published ] + workflow_dispatch: + inputs: + manual_push: + description: 'Set to "yes" to push tag :test image to GHCR for testing before merging to main' + required: false + default: 'no' + type: choice + options: + - 'no' + - 'yes' + +jobs: + + build-test: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Build Docker image + run: | + docker build -f ./docker/Dockerfile -t tradingview-agent:test ./docker + + - name: Test Docker image + run: | + docker run --rm tradingview-agent:test > /tmp/test.log & + sleep 60 + + # if grep -q "Data WebSocket Server listening on port 8082" /tmp/test.log; then + # echo "Build successful" + # else + # echo "Build failed!" + # exit 1 + # fi + + push-to-ghcr: + runs-on: ubuntu-latest + # Run if on main branch, or if manual_push is yes from workflow_dispatch + if: >- + (github.ref == 'refs/heads/main') || + (github.event_name == 'workflow_dispatch' && github.event.inputs.manual_push == 'yes') + needs: build-test + permissions: + contents: read + packages: write + + steps: + # --- Free space becos the docker build is large --- + - name: Free Disk Space + uses: jlumbroso/free-disk-space@main + with: + tool-cache: true + android: true + dotnet: true + haskell: true + large-packages: true + swap-storage: true + + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v4 + # 👆 This is needed ARM emulation on AMD64 runner + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v4 + # 👆 This is need for QEMU to work + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v4 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Set lowercase repository name + run: | + echo "LOWERCASE_REPO=$(echo "${{ github.repository }}" | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV + + - name: Set IMAGE_TAG for build-test + run: | + if [ "${{ github.event_name }}" = "workflow_dispatch" ] && [ "${{ github.event.inputs.manual_push }}" = "yes" ]; then + echo "IMAGE_TAG=test" >> $GITHUB_ENV + elif [ "${{ github.ref }}" = "refs/heads/main" ]; then + echo "IMAGE_TAG=latest" >> $GITHUB_ENV + fi + + - name: Extract metadata for Docker + id: meta + uses: docker/metadata-action@v6 + with: + images: ghcr.io/${{ env.LOWERCASE_REPO }} + + - name: Build and push + uses: docker/build-push-action@v7 + with: + context: ./docker + platforms: linux/amd64,linux/arm64 + file: ./docker/Dockerfile + push: true + tags: | + ghcr.io/${{ env.LOWERCASE_REPO }}:${{ env.IMAGE_TAG }} + ghcr.io/${{ env.LOWERCASE_REPO }}:${{ github.sha }} + labels: ${{ steps.meta.outputs.labels }} \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..46b85ed --- /dev/null +++ b/Makefile @@ -0,0 +1,79 @@ +DOCKER_NAME=tradingview-agent +DOCKER_IMAGE_NAME=tradingview-agent +VOLUME_NAME=tradingview-agent-config +BACKUP_FILE=tradingview-agent_config_backup.tar.gz +BACKUP_DIR=./backup + +.PHONY: backup restore clean + +colima-start: + colima start --profile tradingview-agent --cpu 4 --memory 4 --disk 100 + +colima-stop: + colima stop --profile tradingview-agent + +colima-delete: + colima delete -f --data --profile tradingview-agent + +start-locally-baked: + DOCKER_URI=tradingview-agent:latest \ + PUID=$(shell id -u) \ + PGID=$(shell id -g) \ + docker compose up -d + docker compose logs -f + +start: + PUID=$(shell id -u) \ + PGID=$(shell id -g) \ + docker compose up -d + docker compose logs -f + +stop: + docker compose down + +dev: + $(MAKE) stop + $(MAKE) docker-vol-clean + $(MAKE) docker-build + $(MAKE) start-locally-baked + +docker-image-clean: + # docker rm -f $$(docker ps -qa) + docker rm -f $(DOCKER_NAME) + +docker-vol-clean: + docker volume rm -f $(VOLUME_NAME) + +docker-clean: + $(MAKE) stop + $(MAKE) docker-vol-clean + docker system prune -af --volumes + +backup: + mkdir -p $(BACKUP_DIR) + @echo "Backing up volume: $(VOLUME_NAME) to $(BACKUP_DIR)/$(BACKUP_FILE)" + docker compose down + @mkdir -p $(BACKUP_DIR) + docker run --rm \ + -v $(VOLUME_NAME):/volume \ + -v $(shell pwd)/$(BACKUP_DIR):/backup \ + alpine \ + tar czf /backup/$(BACKUP_FILE) -C /volume . + + @echo "Backup complete! Size of file: $$(du -h backup/$(BACKUP_FILE) | awk '{print $$1}')" + docker compose up -d + +restore: + @test -f "$(BACKUP_DIR)/$(BACKUP_FILE)" || (echo "Error: $(BACKUP_DIR)/$(BACKUP_FILE) does not exist" && exit 1) + @echo "Restoring volume: $(VOLUME_NAME) from $(BACKUP_DIR)/$(BACKUP_FILE)" + docker compose down + docker run --rm \ + -v $(VOLUME_NAME):/volume \ + -v $(shell pwd)/$(BACKUP_DIR):/backup \ + alpine \ + sh -c "cd /volume && rm -rf * && tar xzf /backup/$(BACKUP_FILE)" + @echo "Restore complete!" + docker compose up -d + +docker-build: + docker build -t $(DOCKER_IMAGE_NAME) -f ./docker/Dockerfile ./docker \ No newline at end of file diff --git a/README.md b/README.md index 77dc1fd..7c19c96 100644 --- a/README.md +++ b/README.md @@ -1 +1,177 @@ -# tradingview-agent \ No newline at end of file +# 🚀 TradingView Agent — Web Top +_Run TradingView Agent inside a browser-based Linux desktop with free LLM support through ModelRelay._ + +

+ Your personal TradingView AI agent in the browser — no GPU required +

+ +

+ + License + + + GitHub issues + +

+ +**TradingView-Agent** gives you a **fully functional TradingView Agent AI assistant** in your browser in under 10 minutes — no powerful PC, no Docker on your machine, no GPU required. + +Just open this repo in a GitHub Codespace and you get: +- A complete Ubuntu MATE desktop (WebTop) +- CodeServer at port 8888 with Hermes Extension installed and preconfigured +- Ollama server pre-installed and auto-started +- ModelRelay pre-installed, auto-started and pre-configured as default model +- TradingView Agent dashboard accessible via desktop launcher +- Persistent volume for your config and settings + +When you're ready to go production, simply move the same Docker setup to your own machine or VPS. + +## ✨ Why This Exists + +TradingView Agent is an AI agent framework that connects LLMs directly to your communication platforms (WhatsApp, Telegram, Slack, Discord, etc.) and can run cron jobs, spawn sub-agents, speak/listen, and give you a beautiful dashboard. + +The only catch? You normally need a dedicated machine with GPU. +**TradingView-Agent removes that catch completely.** + +Perfect for: +- Trying TradingView Agent risk-free +- Free LLM APIs through [ModelRelay](https://github.com/ellipticmarketing/modelrelay) +- Students / hackers / evaluators +- Anyone who wants a personal AI assistant without breaking the bank + +## 📸 Demo + +(Demo video coming soon) + +## 🚀 Quick Start (5-10 minutes) + +1. **Open this repository in a GitHub Codespace** (big green "Code" button → Codespaces → New) + + It is recommended that you use 4 cpu core and 16G codespace. + +3. In the Codespace terminal run: + ```bash + make start + ``` + +4. Wait ~60 seconds. When the web desktop URL appears in the Codespace Ports tab, click it. + +5. Inside the WebTop desktop: + + - You see `TradingView Agent` is already running + - Run `hermes` from the terminal; OR: + - Go to chromium at `http://localhost:9119` to access Hermes WebUI + - Recommendation to use CodeServer instead of WebTop + +## 🔧 Features + +- **Zero local install** — everything runs in browser via GitHub Codespaces +- **Free-tier friendly** — uses ModelRelay, Ollama daily cloud credits or NVIDIA Build API fallback +- **Persistent config** — docker volume backup and restore after Codespace recreation +- **Easy backup/restore** — `make backup` / `make restore` +- **One-command everything** — powerful Makefile + clean `docker-compose.yml` +- **Auto-start ModelRelay** — Default configuration for Free LLM API +- **Auto-start Ollama** — custom init script on WebTop boot +- **Colima / local Docker support** ready +- **Built-in code-server IDE** — browser-based VS Code on port `8888` + +## 🧑‍💻 Built-in code-server IDE + +This image includes `code-server` and exposes it on port `8888`. + +- `code-server` is installed automatically in the container. +- The desktop launcher `CodeServer` starts it inside the WebTop environment. +- In Codespaces, use the forwarded private port `8888`. +- Locally, open `http://localhost:8888`. + +> Note: this setup may use `code-server --auth none` in development, so keep port `8888` private. For local production use, secure it with an authenticated reverse proxy or firewall. + +- Hermes Agent's Extension is preinstalled and configured in VSCode +- Cline Extension is also preinstall and configured to ModelRelay +- Start Hacking away in VSCode, use WebTop if you need to monitor agent do desktop-use operations. eg: Non-Headless Chrome debugging for instance. + +## 🔒 Security: Protected by GitHub Authentication + +**The WebTop URI is automatically protected — no one else can reach it.** + +GitHub Codespaces forwards ports **privately by default** (this is the setting the `make start` command uses). According to official [GitHub documentation](https://docs.github.com/en/enterprise-cloud@latest/codespaces/reference/security-in-github-codespaces): + +> "All forwarded ports are private by default, which means that you will need to authenticate before you can access the port." +> "Privately forwarded ports: Are accessible on the internet, but **only the codespace creator can access them, after authenticating to GitHub**." + +### How the protection actually works + +- The URL you click in the **Ports** tab (`https://-3000.app.github.dev`) is guarded by **GitHub authentication cookies**. +- These cookies expire every **3 hours** — you'll simply be asked to log in again (super quick). +- If someone tries to open the link in an incognito window, via curl, or from another computer without being logged into **your** GitHub account, they are redirected to the GitHub login page or blocked. +- You (and only you) can access the full Ubuntu desktop, the browser inside it, Ollama, Hermes, and everything else. + +### Extra security layers built-in + +- The entire environment runs in an **isolated GitHub-managed VM** — not on your laptop. +- Codespaces are **ephemeral**: delete the codespace and everything disappears (except the backed-up volume you control). +- TLS encryption is handled automatically by GitHub. +- The `GITHUB_TOKEN` inside the codespace is scoped only to this repo and expires when you stop/restart. +- We never set the port to "Public" or even "Private to Organization" — it stays strictly private to you. + +**Bottom line**: This is actually **more secure** for experimentation than running Docker locally on your personal machine (no accidental exposure, no firewall holes, no persistent processes on your hardware). + +**For production use** we still recommend moving the same Docker image to your own VPS or server with additional hardening (firewall, HTTPS reverse proxy, strong secrets, etc.). This Codespace version is perfect for safe testing and development. + +## 💾 Backup & Restore + +Your configuration and settings are persisted in a Docker volume. +The project includes convenient `make` targets to back up and restore this data in codespace: + +```bash +make backup # creates backup/tradingview-agent_config_backup.tar.gz +make restore # restores from backup/tradingview-agent_config_backup.tar.gz +``` + +### When to Use It + +- Migrating from GitHub Codespaces to a local machine or VPS +- Testing experimental changes without risking your current setup +- Quickly cloning your working environment into a fresh Codespace or container + +### How to Migrate to a New Environment + +- In your current environment, run `make backup`. +- Download the generated file: `backup/tradingview-agent_config_backup.tar.gz`. +- Place the file in the `backup/` folder of the new environment. +- Run `make restore`. + +**💡 Tip:** Always back up before making significant changes. The restore process will overwrite the existing volume data, so test in a separate environment first if you're unsure. + +## 🛠️ Advanced Usage + +Run locally (no Codespaces) + +```bash +make build-local # especially if you modified the ./docker/Dockerfile +make start-locally-baked # start from your local baked image +``` + +## ⚠️ Current Limitations (honest) + +- GitHub Codespaces free tier has monthly limits (great for testing, less ideal for 24/7 as Codespace auto-shutdown during inactivity) +- Ollama cloud [credits](https://ollama.com/settings) are daily — heavy use will push you to paid/local models. Or if you have multiple accounts, just `ollama signout` and `ollama signin` with different account. +- Browser desktop has slight latency vs native (expected). You can shutdown your codespace and [change](https://docs.github.com/en/codespaces/customizing-your-codespace/changing-the-machine-type-for-your-codespace) to 4-core codespace to improve responsiveness or the need to run heavy applications. + +## 🛣️ Roadmap + +- [ ] More screenshots + video demo +- [ ] Pre-built Docker image tags for stable releases +- [ ] Community templates (Telegram-only, WhatsApp-only, etc.) +- [ ] One-click "deploy to VPS" guide (Railway / Fly.io / cheap VPS) + +## 🤝 Contributing + +This is a community project — every star, issue, or PR helps enormously! +Feel free to open issues for bugs or feature requests. + +[![Star History Chart](https://api.star-history.com/svg?repos=gitricko/tradingview-agent&type=date&legend=top-left)](https://star-history.com/#gitricko/tradingview-agent&type=date&legend=top-left) + +## 📄 License + +MIT — see [LICENSE](./LICENSE) \ No newline at end of file diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..034e848 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,21 @@ +# Security Policy + +## Supported Versions + +Use this section to tell people about which versions of your project are +currently being supported with security updates. + +| Version | Supported | +| ------- | ------------------ | +| 5.1.x | :white_check_mark: | +| 5.0.x | :x: | +| 4.0.x | :white_check_mark: | +| < 4.0 | :x: | + +## Reporting a Vulnerability + +Use this section to tell people how to report a vulnerability. + +Tell them where to go, how often they can expect to get an update on a +reported vulnerability, what to expect if the vulnerability is accepted or +declined, etc. diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..a7c1eb1 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,55 @@ +name: tradingview-agent +services: + tradingview-agent: + # Uses the specific Ubuntu-based MATE tag from LinuxServer.io + image: ${DOCKER_URI:-ghcr.io/gitricko/tradingview-agent:latest} + container_name: tradingview-agent + + # Optional: Needed for some modern GUI apps to function properly on older hosts/kernels + # security_opt: + # - seccomp:unconfined + + environment: + # Set your User ID and Group ID to match the host user (run 'id $USER' in your terminal) + - PUID=${DOCKER_PUID:-501} + - PGID=${DOCKER_PGID:-20} + # Set your timezone + - TZ=America/New_York + # Optional: for reverse proxies + - SUBFOLDER=/ + + volumes: + # Change /path/to/data to the directory on your host for persistent config/files + - tradingview-agent-config:/config + - .:/codespace + # Uncomment the next line if you want to run Docker inside Webtop + - /var/run/docker.sock:/var/run/docker.sock + + ports: + # Access the Webtop GUI on port 3000 (http) + - 3000:3000 + # tradingview-agent dashboard specific ports (adjust as needed) + - 9119:9119 + # modelrelay specific ports (adjust as needed) + - 7352:7352 + # OmniRoute specific ports (adjust as needed) + - 20128:20128 + # Code-server specific ports (adjust as needed) + - 8888:8888 + # Recommended to prevent modern web browsers from crashing + shm_size: "1gb" + + # Ensures the container restarts automatically unless you explicitly stop it + restart: unless-stopped + + networks: + - tradingview-agent-net + +networks: + tradingview-agent-net: + driver: bridge + +volumes: + tradingview-agent-config: + external: false + name: tradingview-agent-config \ No newline at end of file diff --git a/docker/CodeServer.desktop b/docker/CodeServer.desktop new file mode 100644 index 0000000..73a125a --- /dev/null +++ b/docker/CodeServer.desktop @@ -0,0 +1,8 @@ +[Desktop Entry] +Version=1.0 +Type=Application +Terminal=false +Exec=mate-terminal --title="CodeServer" -e "bash -c 'code-server --trusted-origins=* --auth none --bind-addr 0.0.0.0:8888'" +Name[en_US]=CodeServer +Name=CodeServer +GenericName[en_US.UTF-8]=CodeServer diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000..fe6c04c --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,101 @@ +ARG HERMES_VERSION="v2026.5.7" +ARG OMNIROUTE_VERSION=3.7.9 +ARG MODELRELAY_VERSION=1.17.1 +ARG OLLAMA_VERSION=0.21.0 +ARG NODE_VERSION=24 +ARG CODE_SERVER_VERSION=4.118.0 +ARG MNEMON_VERSION=0.1.3 +ARG TARGETARCH + +# Use the official Ollama image to get the binary +FROM ollama/ollama:${OLLAMA_VERSION} AS ollama-bin + +# Get the binaries from official Node image +FROM node:${NODE_VERSION}-slim AS node-bin + +# Use your preferred Webtop flavor as the base (e.g., Ubuntu, Alpine) +FROM lscr.io/linuxserver/webtop:ubuntu-mate + +# Install help utilities and clean up apt cache to reduce image size +RUN apt-get update && apt-get install -y htop zsh ripgrep && rm -rf /var/lib/apt/lists/* + +# Copy Node.js binaries and libraries +COPY --from=node-bin --chown=abc:abc /usr/local/bin/node /usr/local/bin/ +COPY --from=node-bin --chown=abc:abc /usr/local/lib/node_modules /usr/local/lib/node_modules +RUN ln -s /usr/local/lib/node_modules/npm/bin/npm-cli.js /usr/local/bin/npm + +# Copy the Ollama binary from the official image +COPY --from=ollama-bin --chown=abc:abc /usr/bin/ollama /usr/local/bin/ollama + +# Start desktop launchers and bootstrap scripts automatically when the desktop loads +RUN mkdir -p /custom-cont-init.d +COPY --chown=abc:abc *.desktop /custom-cont-init.d +COPY --chown=abc:abc *.sh /custom-cont-init.d +COPY --chown=abc:abc *.json /custom-cont-init.d + +# Install Hermes Agent (TradingView Edition) +ARG HERMES_VERSION +RUN curl -fsSL "https://raw.githubusercontent.com/NousResearch/hermes-agent/${HERMES_VERSION}/scripts/install.sh" | bash -s -- --skip-setup && \ + npm cache clean --force && \ + rm -rf /var/lib/apt/lists/* && \ + chown abc:abc -R /usr/local/lib/hermes-agent + +# Install ModelRelay and start automatically when desktop loads +ARG MODELRELAY_VERSION +RUN npm install -g modelrelay@${MODELRELAY_VERSION} && \ + npm cache clean --force && \ + chown abc:abc -R /usr/local/lib/node_modules/modelrelay /usr/local/bin/modelrelay + +# Install OmniRoute and start automatically when desktop loads +ARG OMNIROUTE_VERSION +RUN npm install -g omniroute@${OMNIROUTE_VERSION} && \ + npm cache clean --force && \ + mkdir -p /usr/local/lib/node_modules/omniroute/app/logs/application && \ + chown abc:abc -R /usr/local/lib/node_modules/omniroute /usr/local/bin/omniroute + +# Install code-server and start automatically when desktop loads +ARG CODE_SERVER_VERSION +RUN curl -fsSL https://code-server.dev/install.sh | sh -s -- --version=${CODE_SERVER_VERSION} --method=standalone --prefix=/usr/local && \ + ln -fs /usr/local/lib/code-server-${CODE_SERVER_VERSION} /usr/local/lib/code-server && \ + ln -fs /usr/local/lib/code-server/bin/code-server /usr/local/bin/code && \ + rm -rf ~/.cache +COPY default.conf /defaults/default.conf + +# Switch to the abc user and set zsh as the default shell for both root and abc +RUN usermod -s $(which zsh) root; usermod -s $(which zsh) abc + +# Install mnemon +ARG MNEMON_VERSION +ARG TARGETARCH +RUN echo "Building for architecture: $TARGETARCH" +RUN set -eux; \ + MNEMON_ARCH="${MNEMON_ARCH:-${TARGETARCH:-amd64}}"; \ + case "${MNEMON_ARCH}" in \ + amd64|x86_64) MNEMON_ARCH=amd64 ;; \ + arm64|aarch64) MNEMON_ARCH=arm64 ;; \ + *) echo "Unsupported MNEMON_ARCH: ${MNEMON_ARCH}" >&2; exit 1 ;; \ + esac; \ + curl -sL "https://github.com/mnemon-dev/mnemon/releases/download/v${MNEMON_VERSION}/mnemon_${MNEMON_VERSION}_linux_${MNEMON_ARCH}.tar.gz" -o /tmp/mnemon.tar.gz && \ + tar xzf /tmp/mnemon.tar.gz -C /tmp && \ + cp /tmp/mnemon /usr/local/bin/mnemon && \ + chmod +x /usr/local/bin/mnemon && \ + rm -rf /tmp/mnemon.tar.gz /tmp/mnemon + +# --------------------------------------------------------------------------- +# Install TradingView Linux Desktop +# --------------------------------------------------------------------------- +# URL discovered from TradingView's JS bundle: +# https://tvd-packages.tradingview.com/ubuntu/stable/latest/jammy/tradingview_amd64.deb +RUN echo "Installing TradingView Linux Desktop..." && \ + curl -fsSL \ + "https://tvd-packages.tradingview.com/ubuntu/stable/latest/jammy/tradingview_amd64.deb" \ + -o /tmp/TradingView.deb \ + && apt-get update \ + && apt-get install -y --no-install-recommends /tmp/TradingView.deb \ + && rm -f /tmp/TradingView.deb \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* \ + && chown -R abc:abc /opt/TradingView \ + && echo "[TradingView] Installed successfully" + +EXPOSE 8888 diff --git a/docker/Dockerfile.old b/docker/Dockerfile.old new file mode 100644 index 0000000..e0c5fe2 --- /dev/null +++ b/docker/Dockerfile.old @@ -0,0 +1,76 @@ +ARG HERMES_VERSION="v2026.5.7" +ARG OMNIROUTE_VERSION=3.7.9 +ARG MODELRELAY_VERSION=1.17.1 +ARG OLLAMA_VERSION=0.21.0 +ARG NODE_VERSION=24 +ARG CODE_SERVER_VERSION=4.118.0 +ARG MNEMON_VERSION=0.1.3 +ARG TARGETARCH + +# Get the binaries from official Node image +FROM node:${NODE_VERSION}-slim AS node-bin + +# Use your preferred Webtop flavor as the base (e.g., Ubuntu, Alpine) +FROM lscr.io/linuxserver/webtop:ubuntu-mate + +# Install help utilities, Firefox, and clean up apt cache to reduce image size +# Remove Chromium if present (webtop includes it by default) +RUN apt-get update && \ + apt-get install -y htop zsh firefox && \ + apt-get remove -y chromium chromium-browser chromium-common libchromium* 2>/dev/null || true && \ + rm -rf /var/lib/apt/lists/* + +# Copy Node.js binaries and libraries +COPY --from=node-bin --chown=abc:abc /usr/local/bin/node /usr/local/bin/ +COPY --from=node-bin --chown=abc:abc /usr/local/lib/node_modules /usr/local/lib/node_modules +RUN ln -s /usr/local/lib/node_modules/npm/bin/npm-cli.js /usr/local/bin/npm + +# Start desktop launchers and bootstrap scripts automatically when the desktop loads +RUN mkdir -p /custom-cont-init.d +COPY --chown=abc:abc *.desktop /custom-cont-init.d +COPY --chown=abc:abc *.sh /custom-cont-init.d +COPY --chown=abc:abc *.json /custom-cont-init.d + + +# Install ModelRelay and start automatically when desktop loads +ARG MODELRELAY_VERSION +RUN npm install -g modelrelay@${MODELRELAY_VERSION} && \ + npm cache clean --force && \ + chown abc:abc -R /usr/local/lib/node_modules/modelrelay /usr/local/bin/modelrelay + +# Install OmniRoute and start automatically when desktop loads +ARG OMNIROUTE_VERSION +RUN npm install -g omniroute@${OMNIROUTE_VERSION} && \ + npm cache clean --force && \ + mkdir -p /usr/local/lib/node_modules/omniroute/app/logs/application && \ + chown abc:abc -R /usr/local/lib/node_modules/omniroute /usr/local/bin/omniroute + +# Install code-server and start automatically when desktop loads +ARG CODE_SERVER_VERSION +RUN curl -fsSL https://code-server.dev/install.sh | sh -s -- --version=${CODE_SERVER_VERSION} --method=standalone --prefix=/usr/local && \ + ln -fs /usr/local/lib/code-server-${CODE_SERVER_VERSION} /usr/local/lib/code-server && \ + ln -fs /usr/local/lib/code-server/bin/code-server /usr/local/bin/code && \ + rm -rf ~/.cache +COPY default.conf /defaults/default.conf + +# Switch to the abc user and set zsh as the default shell for both root and abc +RUN usermod -s $(which zsh) root; usermod -s $(which zsh) abc + +# --------------------------------------------------------------------------- +# Install TradingView Linux Desktop +# --------------------------------------------------------------------------- +# URL discovered from TradingView's JS bundle: +# https://tvd-packages.tradingview.com/ubuntu/stable/latest/jammy/tradingview_amd64.deb +RUN echo "Installing TradingView Linux Desktop..." && \ + curl -fsSL \ + "https://tvd-packages.tradingview.com/ubuntu/stable/latest/jammy/tradingview_amd64.deb" \ + -o /tmp/TradingView.deb \ + && apt-get update \ + && apt-get install -y --no-install-recommends /tmp/TradingView.deb \ + && rm -f /tmp/TradingView.deb \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* \ + && chown -R abc:abc /opt/TradingView \ + && echo "[TradingView] Installed successfully" + +EXPOSE 8888 diff --git a/docker/ModelRelay.desktop b/docker/ModelRelay.desktop new file mode 100644 index 0000000..367ff64 --- /dev/null +++ b/docker/ModelRelay.desktop @@ -0,0 +1,8 @@ +[Desktop Entry] +Version=1.0 +Type=Application +Terminal=false +Exec=mate-terminal --title="ModelRelay" -e "bash -c 'modelrelay --disable; modelrelay; exec bash'" +Name[en_US]=ModelRelay +Name=ModelRelay +GenericName[en_US.UTF-8]=ModelRelay diff --git a/docker/OmniRoute.desktop b/docker/OmniRoute.desktop new file mode 100644 index 0000000..9bd199c --- /dev/null +++ b/docker/OmniRoute.desktop @@ -0,0 +1,8 @@ +[Desktop Entry] +Version=1.0 +Type=Application +Terminal=false +Exec=mate-terminal --title="OmniRoute" -e "bash -c 'omniroute --no-open; exec bash'" +Name[en_US]=OmniRoute +Name=OmniRoute +GenericName[en_US.UTF-8]=OmniRoute \ No newline at end of file diff --git a/docker/TradingViewAgent.desktop b/docker/TradingViewAgent.desktop new file mode 100644 index 0000000..01595da --- /dev/null +++ b/docker/TradingViewAgent.desktop @@ -0,0 +1,8 @@ +[Desktop Entry] +Version=1.0 +Type=Application +Terminal=false +Exec=/opt/TradingView/tradingview --no-sandbox --remote-debugging-port=9222 %U +Name[en_US]=TradingView Agent +Name=TradingView Agent +GenericName[en_US.UTF-8]=TradingView Agent Launcher diff --git a/docker/common.sh b/docker/common.sh new file mode 100755 index 0000000..279cc5d --- /dev/null +++ b/docker/common.sh @@ -0,0 +1,57 @@ +#!/bin/bash + +# Function to safely sync a desktop file if it has changed +# Usage: sync_desktop_file +sync_desktop_file() { + local SRC="$1" + local DEST="$2" + local DEST_DIR + local DEST_BASE + local TMP_DEST + + if [ ! -r "$SRC" ]; then + echo "Error: source file $SRC is missing or not readable." >&2 + return 1 + fi + + DEST_DIR="$(dirname "$DEST")" + DEST_BASE="$(basename "$DEST")" + + # Ensure directory exists and has correct ownership + mkdir -p "$DEST_DIR" + chown abc:abc "$DEST_DIR" + + TMP_DEST="$(mktemp "${DEST_DIR}/.${DEST_BASE}.tmp.XXXXXX")" || return 1 + + if [ -f "$DEST" ]; then + # Check if the file content is different + if ! cmp -s "$SRC" "$DEST"; then + echo "Updating $DEST (content changed). Preparing replacement" + if ! cp "$SRC" "$TMP_DEST"; then + rm -f "$TMP_DEST" + return 1 + fi + if ! chown abc:abc "$TMP_DEST"; then + rm -f "$TMP_DEST" + return 1 + fi + # Use a backup just in case, but overwrite it next time + mv "$DEST" "${DEST}.bak" 2>/dev/null || true + mv "$TMP_DEST" "$DEST" + else + echo "$DEST is already up to date." + rm -f "$TMP_DEST" + fi + else + echo "Creating $DEST" + if ! cp "$SRC" "$TMP_DEST"; then + rm -f "$TMP_DEST" + return 1 + fi + if ! chown abc:abc "$TMP_DEST"; then + rm -f "$TMP_DEST" + return 1 + fi + mv "$TMP_DEST" "$DEST" + fi +} diff --git a/docker/default.conf b/docker/default.conf new file mode 100644 index 0000000..2569729 --- /dev/null +++ b/docker/default.conf @@ -0,0 +1,143 @@ +server { + #auth_basic "Login"; + #auth_basic_user_file /etc/nginx/.htpasswd; + listen 3000 default_server; + listen [::]:3000 default_server; + location SUBFOLDER { + alias /usr/share/selkies/web/; + index index.html index.htm; + try_files $uri $uri/ =404; + } + location /devmode { + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_http_version 1.1; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + proxy_connect_timeout 3600s; + proxy_buffering off; + client_max_body_size 10M; + proxy_pass http://127.0.0.1:5173; + } + location SUBFOLDERwebsocket { + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_http_version 1.1; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + proxy_connect_timeout 3600s; + proxy_buffering off; + client_max_body_size 10M; + proxy_pass http://127.0.0.1:CWS; + } + location SUBFOLDERfiles { + fancyindex on; + fancyindex_footer SUBFOLDERnginx/footer.html; + fancyindex_header SUBFOLDERnginx/header.html; + alias REPLACE_DOWNLOADS_PATH/; + if (-f $request_filename) { + add_header Content-Disposition "attachment"; + add_header X-Content-Type-Options "nosniff"; + } + } + location SUBFOLDERcode/ { + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_http_version 1.1; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + proxy_connect_timeout 3600s; + proxy_buffering off; + client_max_body_size 10M; + proxy_pass http://127.0.0.1:8888/; + } + error_page 500 502 503 504 /50x.html; + location = SUBFOLDER50x.html { + root /usr/share/selkies/web/; + } +} + +server { + #auth_basic "Login"; + #auth_basic_user_file /etc/nginx/.htpasswd; + listen 3001 ssl; + listen [::]:3001 ssl; + ssl_certificate /config/ssl/cert.pem; + ssl_certificate_key /config/ssl/cert.key; + location SUBFOLDER { + alias /usr/share/selkies/web/; + index index.html index.htm; + try_files $uri $uri/ =404; + } + location /devmode { + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_http_version 1.1; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + proxy_connect_timeout 3600s; + proxy_buffering off; + client_max_body_size 10M; + proxy_pass http://127.0.0.1:5173; + } + location SUBFOLDERwebsocket { + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_http_version 1.1; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + proxy_connect_timeout 3600s; + proxy_buffering off; + client_max_body_size 10M; + proxy_pass http://127.0.0.1:CWS; + } + location SUBFOLDERfiles { + fancyindex on; + fancyindex_footer SUBFOLDERnginx/footer.html; + fancyindex_header SUBFOLDERnginx/header.html; + alias REPLACE_DOWNLOADS_PATH/; + if (-f $request_filename) { + add_header Content-Disposition "attachment"; + add_header X-Content-Type-Options "nosniff"; + } + } + location SUBFOLDERcode/ { + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_http_version 1.1; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + proxy_connect_timeout 3600s; + proxy_buffering off; + client_max_body_size 10M; + proxy_pass http://127.0.0.1:8888/; + } + error_page 500 502 503 504 /50x.html; + location = SUBFOLDER50x.html { + root /usr/share/selkies/web/; + } +} \ No newline at end of file diff --git a/docker/globalState.json b/docker/globalState.json new file mode 100644 index 0000000..9fd11f9 --- /dev/null +++ b/docker/globalState.json @@ -0,0 +1,51 @@ +{ + "welcomeViewCompleted": true, + "__vscodeMigrationVersion": 1, + "clineVersion": "3.83.0", + "remoteRulesToggles": {}, + "remoteWorkflowToggles": {}, + "remoteSkillsToggles": {}, + "openAiHeaders": {}, + "sapAiCoreUseOrchestrationMode": true, + "ocaMode": "internal", + "planModeApiProvider": "openai", + "actModeApiProvider": "openai", + "openAiBaseUrl": "http://localhost:7352/v1", + "planModeOpenAiModelId": "auto-fastest", + "actModeOpenAiModelId": "auto-fastest", + "azureApiVersion": "", + "lastShownAnnouncementId": "3.83", + "dismissedBanners": [ + { + "bannerId": "bnr-01KQ02NN40X7WTJMKQC610GS5M", + "dismissedAt": 1778856185168 + } + ], + "autoApprovalSettings": { + "version": 2, + "enabled": true, + "favorites": [], + "maxRequests": 20, + "actions": { + "readFiles": true, + "readFilesExternally": false, + "editFiles": false, + "editFilesExternally": false, + "executeSafeCommands": true, + "executeAllCommands": false, + "useBrowser": false, + "useMcp": true + }, + "enableNotifications": false + }, + "workspaceRoots": [ + { + "path": "/config/Desktop", + "name": "Desktop", + "vcs": "none" + } + ], + "primaryRootIndex": 0, + "globalWorkflowToggles": {}, + "globalClineRulesToggles": {} +} \ No newline at end of file diff --git a/docker/secrets.json b/docker/secrets.json new file mode 100644 index 0000000..8fa649d --- /dev/null +++ b/docker/secrets.json @@ -0,0 +1,3 @@ +{ + "openAiApiKey": " " +} \ No newline at end of file diff --git a/docker/start-1-tradingview-agent.sh b/docker/start-1-tradingview-agent.sh new file mode 100755 index 0000000..20a7c02 --- /dev/null +++ b/docker/start-1-tradingview-agent.sh @@ -0,0 +1,19 @@ +#!/bin/bash +source /custom-cont-init.d/common.sh || exit 1 + +SRC="/custom-cont-init.d/TradingViewAgent.desktop" + +chown abc:abc -R /usr/local/lib/hermes-agent/web +chown abc:abc -R /usr/local/lib/hermes-agent & + +sync_desktop_file "$SRC" "/config/.config/autostart/TradingViewAgent.desktop" +sync_desktop_file "$SRC" "/config/Desktop/TradingViewAgent.desktop" + +if [ -d "$HOME/.hermes/sessions" ] && [ -z "$(ls -A "$HOME/.hermes/sessions")" ]; then + echo "[start-1-tradingview-agent.sh] No sessions found in $HOME/.hermes/sessions, setting up default configuration for custom provider" + hermes config set model.provider custom + hermes config set model.base_url http://localhost:7352/v1 + hermes config set model.default auto-fastest +fi + +chown abc:abc -R ~/.hermes diff --git a/docker/start-2-modelrelay.sh b/docker/start-2-modelrelay.sh new file mode 100755 index 0000000..cca86e7 --- /dev/null +++ b/docker/start-2-modelrelay.sh @@ -0,0 +1,46 @@ +#!/bin/bash +source /custom-cont-init.d/common.sh || exit 1 + +SRC="/custom-cont-init.d/ModelRelay.desktop" + +add_model_if_missing() { + local file="$1" + + if ! jq -e '.model_list | any(.model_name == "modelrelay")' "$file" > /dev/null; then + echo "[start-2-modelrelay] Adding modelrelay model to $file" + jq '.model_list += [{ + "model_name": "modelrelay", + "model": "openai/auto-fastest", + "api_base": "http://localhost:7352/v1" + }]' "$file" > "${file}.tmp" && mv "${file}.tmp" "$file" + fi + + # Set modelrelay as default for agents if not default was set + if jq -e '.agents.defaults.model_name | select(. == null or . == "")' "$file" > /dev/null; then + echo "[start-2-modelrelay] Setting modelrelay as defaults for agents in $file" + jq '.agents |= (. // {}) | .agents.defaults |= (. // {}) | .agents.defaults.model_name = "modelrelay"' "$file" > "${file}.tmp" && mv "${file}.tmp" "$file" + fi + +} + +# Prep nodejs npm for ModelRelay +rm -rf /config/.npm +chown abc:abc -R /usr/local/lib/node_modules/modelrelay & +chown abc:abc -R /usr/local/bin/modelrelay & + +# Sync desktop file for autostart and desktop icon +sync_desktop_file "$SRC" "/config/.config/autostart/ModelRelay.desktop" +sync_desktop_file "$SRC" "/config/Desktop/ModelRelay.desktop" + +# Add modelrelay model to hermes's config as soon as it appears, and set it as default for agents if no default was set +# /config/.hermes/config.json +( + for i in {0..60}; do + if [ -f "/config/.hermes/config.json" ]; then + add_model_if_missing "/config/.hermes/config.json" + chown abc:abc "/config/.hermes/config.json" + break + fi + sleep 5 + done +) & diff --git a/docker/start-2-omniroute.sh b/docker/start-2-omniroute.sh new file mode 100755 index 0000000..8eebfb9 --- /dev/null +++ b/docker/start-2-omniroute.sh @@ -0,0 +1,14 @@ +#!/bin/bash +source /custom-cont-init.d/common.sh || exit 1 + +SRC="/custom-cont-init.d/OmniRoute.desktop" + +# Prep nodejs npm for OmniRoute +rm -rf /config/.npm +chown abc:abc -R /usr/local/lib/node_modules/omniroute/app/logs +chown abc:abc -R /usr/local/lib/node_modules/omniroute & +chown abc:abc -R /usr/local/bin/omniroute & + +# Sync desktop file for autostart and desktop icon +sync_desktop_file "$SRC" "/config/.config/autostart/OmniRoute.desktop" +sync_desktop_file "$SRC" "/config/Desktop/OmniRoute.desktop" \ No newline at end of file diff --git a/docker/start-3-ollama.sh b/docker/start-3-ollama.sh new file mode 100755 index 0000000..bfee693 --- /dev/null +++ b/docker/start-3-ollama.sh @@ -0,0 +1,2 @@ +#!/bin/bash +runuser -l abc -c 'ollama serve &' \ No newline at end of file diff --git a/docker/start-4-codeserver.sh b/docker/start-4-codeserver.sh new file mode 100755 index 0000000..2a1b13c --- /dev/null +++ b/docker/start-4-codeserver.sh @@ -0,0 +1,49 @@ +#!/bin/bash +source /custom-cont-init.d/common.sh || exit 1 +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +SRC="/custom-cont-init.d/CodeServer.desktop" + +sync_desktop_file "$SRC" "/config/.config/autostart/CodeServer.desktop" +sync_desktop_file "$SRC" "/config/Desktop/CodeServer.desktop" + +chown abc:abc /usr/local/lib/node_modules +chown abc:abc /usr/local/bin + +# Add VSCode Extension vscode's config as soon as it appears +( + for i in {0..999}; do + if [ -d "/config/.local/share/code-server/User" ]; then + echo "[start-4-codeserver] code-server configuration found in ~/.local/share/code-server, setting permissions and installing extensions" + break + else + echo "[start-4-codeserver] Force code-server to initialize by calling URI" + curl -s http://localhost:8888 > /dev/null 2>&1 + fi + sleep 5 + done + + chown -R abc:abc /config/.local/share/code-server + + EXTENSION=saoudrizwan.claude-dev + if code --list-extensions | grep -q "${EXTENSION}"; then + echo "[start-4-codeserver] Extension ${EXTENSION} already installed, skip" + else + echo "[start-4-codeserver] Installing Extension ${EXTENSION}..." + mkdir -p "$HOME/.cline/data" + cp "${SCRIPT_DIR}/globalState.json" "$HOME/.cline/data/globalState.json" + cp "${SCRIPT_DIR}/secrets.json" "$HOME/.cline/data/secrets.json" + runuser -l abc -c "code --install-extension ${EXTENSION}" + chown -R abc:abc $HOME/.cline/data + + fi + + EXTENSION=joaompfp.hermes-ai-agent + if code --list-extensions | grep -q "${EXTENSION}"; then + echo "[start-4-codeserver] Extension ${EXTENSION} already installed, skip" + else + echo "[start-4-codeserver] Installing Extension ${EXTENSION}..." + runuser -l abc -c "curl -sL https://github.com/joaompfp/hermes-vscode/releases/download/v2.0.0/hermes-ai-agent-2.0.0.vsix -o /tmp/hermes-ai-agent.vsix && code --install-extension /tmp/hermes-ai-agent.vsix --force && rm /tmp/hermes-ai-agent.vsix" + fi + +) & \ No newline at end of file diff --git a/docker/start-5-ohmyzsh.sh b/docker/start-5-ohmyzsh.sh new file mode 100755 index 0000000..c7ca95d --- /dev/null +++ b/docker/start-5-ohmyzsh.sh @@ -0,0 +1,2 @@ +#!/bin/bash +runuser -l abc -c 'sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" --unattended &' \ No newline at end of file