Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 106 additions & 0 deletions .devcontainer/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
# ── Baked Devcontainer Image ──────────────────────────────────────────
# Pre-installs all heavy tooling so containers start in seconds, not minutes.
# Rebuild: docker build -t hermes-codespace:latest -f .devcontainer/Dockerfile .
# ────────────────────────────────────────────────────────────────────────

FROM mcr.microsoft.com/devcontainers/base:ubuntu

# ── Versions (single source of truth) ─────────────────────────────────
ARG HERMES_VERSION=v2026.7.7.2
ARG OMNIROUTE_VERSION=3.8.48
ARG OLLAMA_VERSION=0.32.1
ARG NODE_VERSION=24.18.0
ARG MNEMON_VERSION=0.1.17

# Export as env so install scripts can see them
ENV OLLAMA_VERSION=${OLLAMA_VERSION}
ENV OLLAMA_NO_START=1
ENV DEBIAN_FRONTEND=noninteractive

# ── System packages + ALL heavy installs in ONE layer ──────────────────
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
zsh ripgrep jq curl git ca-certificates gnupg zstd \
&& rm -rf /var/lib/apt/lists/* \
\
# ── Ollama ────────────────────────────────────────────────────────
&& curl -fsSL https://ollama.com/install.sh | sh \
\
# ── Hermes Agent ─────────────────────────────────────────────────
&& curl -fsSL "https://raw.githubusercontent.com/NousResearch/hermes-agent/${HERMES_VERSION}/scripts/install.sh" \
| bash -s -- --skip-setup \
&& npm cache clean --force \
\
# ── agent-client-protocol (inside hermes venv) ────────────────────
&& if [ -x "$HOME/.hermes/hermes-agent/venv/bin/python" ]; then \
"$HOME/.hermes/hermes-agent/venv/bin/python" -m pip install "agent-client-protocol>=0.9.0,<1.0"; \
fi \
\
# ── ModelRelay ────────────────────────────────────────────────────
&& npm install github:gitricko/modelrelay -g --prefix /usr/local/lib/modelrelay \
&& ln -sf /usr/local/lib/modelrelay/bin/modelrelay /usr/local/bin/modelrelay \
&& npm cache clean --force \
\
# ── OmniRoute ─────────────────────────────────────────────────────
&& npm install omniroute@${OMNIROUTE_VERSION} -g --prefix /usr/local/lib/omniroute \
&& ln -sf /usr/local/lib/omniroute/bin/omniroute /usr/local/bin/omniroute \
&& npm cache clean --force \
\
# ── OmniRoute dist/ dep repair (hollow deps workaround) ──────────
&& omni_root="/usr/local/lib/omniroute/lib/node_modules/omniroute" \
&& dist_nm="$omni_root/dist/node_modules" \
&& parent_nm="$omni_root/node_modules" \
&& if [ -d "$dist_nm" ]; then \
for dst in $(find "$dist_nm" -mindepth 1 -maxdepth 1 -type d 2>/dev/null); do \
rel="${dst#"$dist_nm"/}" \
&& src="$parent_nm/$rel" \
&& if [ -d "$src" ] && [ ! "$(find "$dst" \( -name '*.js' -o -name '*.mjs' -o -name '*.node' \) -type f 2>/dev/null | head -1)" ] \
&& [ "$(find "$src" \( -name '*.js' -o -name '*.mjs' -o -name '*.node' \) -type f 2>/dev/null | head -1)" ]; then \
rm -rf "$dst" && cp -r "$src" "$dst" \
&& echo "Repaired hollow dep: $rel"; \
fi; \
done; \
fi \
\
# ── TailScale ─────────────────────────────────────────────────────
&& mkdir -p /var/run/tailscale /var/lib/tailscale \
&& (curl -fsSL https://tailscale.com/install.sh | sh || echo "WARN: tailscale install failed, continuing") \
&& rm -rf /var/lib/apt/lists/* \
\
# ── Mnemon ────────────────────────────────────────────────────────
&& ARCH=amd64 \
&& curl -sL "https://github.com/mnemon-dev/mnemon/releases/download/v${MNEMON_VERSION}/mnemon_${MNEMON_VERSION}_linux_${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 \
\
# ── Cline ─────────────────────────────────────────────────────────
&& npm install -g cline \
\
# ── Claude CLI ────────────────────────────────────────────────────
&& curl -fsSL https://claude.ai/install.sh | bash \
\
# ── Final cleanup ─────────────────────────────────────────────────
&& apt-get autoremove -y \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/* /root/.npm /tmp/* /var/tmp/* \
&& rm -rf /root/.cache/pip 2>/dev/null || true

# ── Copy config files ─────────────────────────────────────────────────
COPY .devcontainer/CLAUDE.md /tmp/devcontainer-config/CLAUDE.md
COPY .devcontainer/claude-term-settings.json /tmp/devcontainer-config/claude-term-settings.json
COPY .devcontainer/.claude.json /tmp/devcontainer-config/.claude.json
COPY .devcontainer/skill-memory-automation.md /tmp/devcontainer-config/skill-memory-automation.md
COPY .devcontainer/.hermes.md /tmp/devcontainer-config/.hermes.md
COPY .devcontainer/cline-globalState.json /tmp/devcontainer-config/cline-globalState.json
COPY .devcontainer/cline-secrets.json /tmp/devcontainer-config/cline-secrets.json
COPY .devcontainer/self-check.sh /usr/local/bin/self-check.sh

# ── Entrypoint: lightweight service start + config placement ──────────
COPY .devcontainer/entrypoint.sh /usr/local/bin/entrypoint.sh
RUN chmod +x /usr/local/bin/entrypoint.sh /usr/local/bin/self-check.sh

ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
CMD ["sleep", "infinity"]
11 changes: 8 additions & 3 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
{
"name": "Hermes-Coding-Agent",
"build": {
"dockerfile": "Dockerfile",
"context": ".."
},
"runArgs": [
"--name", "hermes-codespace"
],
"customizations": {
"vscode": {
"extensions": [
Expand All @@ -8,7 +15,5 @@
"saoudrizwan.claude-dev"
]
}
},
"postCreateCommand": "bash ./.devcontainer/post-create-cmd.sh >> /tmp/hermes-codespace.log 2>&1",
"postStartCommand": "bash ./.devcontainer/start-hermes.sh >> /tmp/hermes-codespace.log 2>&1"
}
}
160 changes: 160 additions & 0 deletions .devcontainer/entrypoint.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
#!/bin/bash
# ── Entrypoint: copies baked configs to $HOME and starts services ─────
# This replaces both post-create-cmd.sh and start-hermes.sh.
# Heavy installs are already in the image; this only does runtime setup.
set -e

SCRIPT_NAME="entrypoint.sh"
echo "***** Hermes Codespace — Baked Image Entrypoint *****"

# ── Place config files into $HOME (only if not already customized) ───
place_config() {
local src="$1" dst="$2"
if [ ! -f "$dst" ] || ! cmp -s "$src" "$dst" 2>/dev/null; then
mkdir -p "$(dirname "$dst")"
cp "$src" "$dst"
echo "[$SCRIPT_NAME] Placed $(basename "$dst")"
fi
}

place_config /tmp/devcontainer-config/CLAUDE.md "$HOME/.claude/CLAUDE.md"
place_config /tmp/devcontainer-config/claude-term-settings.json "$HOME/.claude/settings.json"
place_config /tmp/devcontainer-config/.claude.json "$HOME/.claude.json"
place_config /tmp/devcontainer-config/.hermes.md "$HOME/.hermes.md"
place_config /tmp/devcontainer-config/cline-globalState.json "$HOME/.cline/data/globalState.json"
place_config /tmp/devcontainer-config/cline-secrets.json "$HOME/.cline/data/secrets.json"
mkdir -p "$HOME/.hermes/skills/memory-automation"
place_config /tmp/devcontainer-config/skill-memory-automation.md "$HOME/.hermes/skills/memory-automation/SKILL.md"

# ── Hermes config defaults (first session only) ──────────────────────
if command -v hermes &>/dev/null \
&& [ -d "$HOME/.hermes/sessions" ] && [ -z "$(ls -A "$HOME/.hermes/sessions" 2>/dev/null)" ]; then
echo "[$SCRIPT_NAME] Setting up default Hermes config..."
hermes config set model.default auto-fastest
hermes config set model.provider omniroute
hermes config set providers.omniroute.base_url http://localhost:20128/v1
hermes config set providers.omniroute.api_key no-key-needed
hermes config set providers.modelrelay.base_url http://localhost:7352/v1
hermes config set providers.modelrelay.api_key no-key-needed
hermes config set fallback_providers.provider modelrelay
hermes config set fallback_providers.model auto-fastest
hermes config set auxiliary.title_generation.model auto-fastest
hermes config set auxiliary.title_generation.provider modelrelay
hermes config set auxiliary.vision.model auto-fastest
hermes config set auxiliary.vision.provider modelrelay
hermes config set auxiliary.compression.model auto-fastest
hermes config set auxiliary.compression.provider modelrelay
hermes config set approvals.mode off
hermes config set memory.memory_enabled true
hermes config set memory.user_profile_enabled true
hermes config set memory.provider mnemon
hermes config set agent.max_turns 120
hermes config set kanban.failure_limit 3
fi

# ── Mnemon USER.md ───────────────────────────────────────────────────
if [ ! -f "$HOME/.hermes/memories/USER.md" ]; then
mkdir -p "$HOME/.hermes/memories"
cat > "$HOME/.hermes/memories/USER.md" <<'USEREOF'
Always use Mnemon (mnemon_remember / mnemon_recall) as primary memory provider instead of the standard memory() tool. Mnemon has no char limit. Only fall back to memory() for structured preference data (target=user or memory).
USEREOF
echo "[$SCRIPT_NAME] Created USER.md for Mnemon"
fi

# ── Start services (only if not already running) ─────────────────────
start_service() {
local name="$1" cmd="$2"
local logfile="/tmp/$(echo "$name" | tr ' ' '-').log"
if pgrep -f "$name" > /dev/null 2>&1; then
echo "[$SCRIPT_NAME] $name already running, skipping"
else
echo "[$SCRIPT_NAME] Starting $name..."
setsid $cmd >> "$logfile" 2>&1 &
fi
}

start_service "ollama serve" "/usr/local/bin/ollama serve"
start_service "modelrelay" "/usr/local/bin/modelrelay"
start_service "omniroute" "/usr/local/bin/omniroute --no-open --log"

# Pull nomic-embed-text in background after 60s
( sleep 60 && ollama pull nomic-embed-text >> /tmp/ollama-pull.log 2>&1 ) &

# ── OmniRoute: wait for ready, disable login, create combo ────────────
MAX_ATTEMPTS=10
for ((attempt=1; attempt<=MAX_ATTEMPTS; attempt++)); do
if curl -s --max-time 3 -o /dev/null -w "%{http_code}" http://localhost:20128/v1/models 2>/dev/null | grep -q "200"; then
break
fi
[ "$attempt" -eq "$MAX_ATTEMPTS" ] && echo "[$SCRIPT_NAME] WARNING: OmniRoute not ready"
sleep 1
done

# Disable login requirement
if [ -f "$HOME/.omniroute/storage.sqlite" ]; then
python3 -c "
import sqlite3
conn = sqlite3.connect('$HOME/.omniroute/storage.sqlite')
conn.execute('UPDATE key_value SET value = ? WHERE key = ?', ('false', 'requireLogin'))
conn.commit()
conn.close()
" 2>/dev/null
fi

# Create auto-fastest combo (idempotent)
for ((i=1; i<=5; i++)); do
omniroute combo create auto-fastest --strategy auto 2>/dev/null && break
sleep 2
done

# Configure combo models
COMBO_ID=$(omniroute combo list --json 2>/dev/null | grep -v "📋" | \
python3 -c "import sys,json; d=json.load(sys.stdin); print([c['id'] for c in d['combos'] if c['name']=='auto-fastest'][0])" 2>/dev/null)
if [ -n "$COMBO_ID" ]; then
curl -s -X PUT "http://localhost:20128/api/combos/$COMBO_ID" \
-H "Content-Type: application/json" \
-d '{
"models": ["oc/deepseek-v4-flash-free","oc/big-pickle","opencode-zen/deepseek-v4-flash-free","opencode-zen/hy3-free","opencode-zen/mimo-v2.5-free","opencode-zen/north-mini-code-free","opencode-zen/nemotron-3-ultra-free","opencode-zen/big-pickle"],
"strategy": "auto",
"config": {"maxRetries": 2, "retryDelayMs": 1000, "timeoutMs": 120000, "healthCheckEnabled": true}
}' >/dev/null
fi

# Enable MCP
if ! omniroute mcp status --json 2>/dev/null | python3 -c "import sys,json;exit(0 if json.load(sys.stdin).get('enabled') else 1)" 2>/dev/null; then
curl -s -X PATCH http://localhost:20128/api/settings \
-H "Content-Type: application/json" -d '{"mcpEnabled":true}' >/dev/null
fi

# Add omniroute MCP to hermes
yes Y 2>/dev/null | hermes mcp add omniroute --command omniroute --args --mcp 2>/dev/null || true

# ── Hermes gateway + dashboard ────────────────────────────────────────
# Update mnemon plugin
rm -rf /tmp/mnemon_repo
if git clone https://github.com/gitricko/hermes-plugin-mnemon /tmp/mnemon_repo 2>/dev/null; then
if [ ! -d "$HOME/.hermes/plugins/mnemon" ] || ! diff -r -q -x __pycache__ "$HOME/.hermes/plugins/mnemon" "/tmp/mnemon_repo/mnemon" >/dev/null 2>&1; then
mkdir -p "$HOME/.hermes/plugins"
rm -rf "$HOME/.hermes/plugins/mnemon"
cp -r "/tmp/mnemon_repo/mnemon" "$HOME/.hermes/plugins/mnemon"
fi
rm -rf /tmp/mnemon_repo
fi
Comment on lines +133 to +142

start_service "hermes gateway" "hermes gateway run --no-supervise"
start_service "hermes dashboard" "hermes dashboard --port 9119 --no-open"

# Telegram bot deps
$HOME/.hermes/hermes-agent/venv/bin/python -m ensurepip --upgrade 2>/dev/null || true
ln -sf $HOME/.hermes/hermes-agent/venv/bin/pip3 $HOME/.hermes/hermes-agent/venv/bin/pip 2>/dev/null || true
$HOME/.hermes/hermes-agent/venv/bin/pip install python-telegram-bot 2>/dev/null || true

# Mnemon -> claude-code integration
mnemon setup --yes --global --target claude-code 2>/dev/null || true

echo "[$SCRIPT_NAME] All services started."
echo "[$SCRIPT_NAME] Running self-check..."
/usr/local/bin/self-check.sh 2>/dev/null || echo "[$SCRIPT_NAME] WARNING: self-check reported issues"

# ── Execute the CMD (default: sleep infinity) ─────────────────────────
exec "$@"
2 changes: 1 addition & 1 deletion .devcontainer/self-check.sh
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ if ! should_skip "services"; then
# Poll all service ports until all respond or timeout
PORT_POLL_TIMEOUT=60
POLL_STARTED_AT=$(date +%s)
declare -A RESPONDED=([3000]="" [8888]="" [7352]="" [20128]="", [9119]="")
declare -A RESPONDED=([3000]="" [8888]="" [7352]="" [20128]="" [9119]="")

while true; do
NOW=$(date +%s)
Expand Down
10 changes: 10 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
.git
.github
.vscode
*.md
!README.md
node_modules
*.log
tmp
.cache
.hermes
Loading
Loading