From 6b68c0a926e8ac2539111db10f5214b7fe979c69 Mon Sep 17 00:00:00 2001 From: Oussama Douhou Date: Sat, 10 Jan 2026 15:33:44 +0100 Subject: [PATCH] feat(logging): log full content - prompts, responses, tool I/O - Log full message content instead of just hash - Log full tool input arguments - Log full tool output/results - Add model info to session and token events No privacy restrictions for internal monitoring. --- .gitea/workflows/docker-publish.yaml | 59 +++ assets/oh-my-opencode.schema.json | 3 +- docker/Dockerfile.leader | 94 ++++ docker/docker-compose.yml | 34 ++ docker/shared-config/oh-my-opencode.json | 104 ++++ docker/shared-config/opencode.json | 52 ++ docker/shared-config/opencode.jsonc | 118 +++++ docker/shared-config/test-persistence.txt | 1 + reload.sh | 79 ++++ src/hooks/usage-logging/index.ts | 28 +- start.sh | 46 ++ tui.sh | 547 ++++++++++++++++++++++ 12 files changed, 1150 insertions(+), 15 deletions(-) create mode 100644 .gitea/workflows/docker-publish.yaml create mode 100644 docker/Dockerfile.leader create mode 100644 docker/docker-compose.yml create mode 100644 docker/shared-config/oh-my-opencode.json create mode 100644 docker/shared-config/opencode.json create mode 100644 docker/shared-config/opencode.jsonc create mode 100644 docker/shared-config/test-persistence.txt create mode 100755 reload.sh create mode 100755 start.sh create mode 100755 tui.sh diff --git a/.gitea/workflows/docker-publish.yaml b/.gitea/workflows/docker-publish.yaml new file mode 100644 index 0000000..b955be9 --- /dev/null +++ b/.gitea/workflows/docker-publish.yaml @@ -0,0 +1,59 @@ +name: Build and Push Docker Image + +on: + push: + branches: + - custom + paths: + - 'docker/**' + - 'src/**' + - 'package.json' + - 'bun.lock' + - '.gitea/workflows/**' + workflow_dispatch: + +env: + REGISTRY: 10.100.0.20:22222 + REGISTRY_PUBLIC: git.app.flexinit.nl + IMAGE_NAME: oussamadouhou/oh-my-opencode + +jobs: + build-and-push: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + with: + config-inline: | + [registry."10.100.0.20:22222"] + http = true + insecure = true + + - name: Login to Gitea Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ gitea.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=raw,value=latest,enable={{is_default_branch}} + type=sha,prefix= + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: . + file: ./docker/Dockerfile.leader + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} diff --git a/assets/oh-my-opencode.schema.json b/assets/oh-my-opencode.schema.json index 9b39d48..e1966c0 100644 --- a/assets/oh-my-opencode.schema.json +++ b/assets/oh-my-opencode.schema.json @@ -78,7 +78,8 @@ "prometheus-md-only", "start-work", "sisyphus-orchestrator", - "todo-codebase-compaction" + "todo-codebase-compaction", + "usage-logging" ] } }, diff --git a/docker/Dockerfile.leader b/docker/Dockerfile.leader new file mode 100644 index 0000000..70c087c --- /dev/null +++ b/docker/Dockerfile.leader @@ -0,0 +1,94 @@ +# Leader - The Orchestrator +FROM oven/bun:debian + +LABEL role="leader" +LABEL description="The orchestrator of the agent ecosystem" + +# Essential tools +RUN apt-get update && apt-get install -y --no-install-recommends \ + bash \ + less \ + curl \ + git \ + jq \ + tree \ + make \ + ca-certificates \ + ripgrep \ + && rm -rf /var/lib/apt/lists/* + +# Install yq +RUN curl -sL https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 -o /usr/local/bin/yq \ + && chmod +x /usr/local/bin/yq + +# Install ast-grep (sg) via npm +RUN bun install -g @ast-grep/cli && \ + ln -sf $(which ast-grep) /usr/local/bin/sg + +# Set bun paths +ENV BUN_INSTALL=/root/.bun +ENV PATH=$BUN_INSTALL/bin:/usr/local/bin:$PATH + +# Install OpenCode CLI +RUN bun install -g opencode-ai && \ + echo "=== OpenCode CLI installed ===" && \ + which opencode && \ + opencode --version + +# Copy and install oh-my-opencode from local source +COPY package.json bun.lock tsconfig.json /tmp/oh-my-opencode/ +COPY src/ /tmp/oh-my-opencode/src/ +COPY script/ /tmp/oh-my-opencode/script/ +COPY assets/ /tmp/oh-my-opencode/assets/ +RUN cd /tmp/oh-my-opencode && \ + bun install && \ + bun run build && \ + npm install -g . && \ + echo "=== oh-my-opencode installed from local source ===" && \ + rm -rf /tmp/oh-my-opencode + +# Create workspace and config directories +WORKDIR /workspace + +# Shared config directory - all agents read from here +RUN mkdir -p /shared/config \ + /shared/claude \ + /shared/skills \ + /shared/commands \ + /shared/agents \ + /data/artifacts \ + /data/reports \ + /data/logs + +# Set environment for shared config +ENV XDG_CONFIG_HOME=/shared/config +ENV HOME=/root +ENV NODE_ENV=development +ENV OH_MY_OPENCODE_ENABLED=true + +# Add convenient alias for attaching to local server +RUN echo "alias opencode='opencode attach http://localhost:8080'" >> /root/.bashrc + +# Copy config files (from docker directory) +COPY docker/shared-config/ /shared/config/ + +# Set bash as default shell +SHELL ["/bin/bash", "-c"] + +# Verify plugin installation and configuration +RUN echo "=== DEBUG: Final Plugin Verification ===" && \ + echo "OpenCode location:" && which opencode && \ + echo "OpenCode version:" && opencode --version && \ + echo "ripgrep version:" && rg --version | head -1 && \ + echo "ast-grep version:" && sg --version && \ + echo "Configuration check:" && \ + cat /shared/config/opencode.jsonc | jq '.plugin' 2>/dev/null || echo "Config not readable" && \ + echo "Plugin directory check:" && \ + ls -la /shared/config/ 2>/dev/null || echo "Config dir not accessible" && \ + echo "Testing basic OpenCode functionality:" && \ + timeout 5 opencode --help >/dev/null 2>&1 && echo "OpenCode CLI working" || echo "OpenCode CLI issue" && \ + echo "=== DEBUG: Verification complete ===" + +# Start OpenCode server +EXPOSE 8080 +CMD ["sh", "-c", "echo '=== Starting OpenCode Server ===' && echo 'Server will be available at http://localhost:8080' && opencode serve --hostname 0.0.0.0 --port 8080 --mdns 2>&1 | tee /var/log/opencode.log"] diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml new file mode 100644 index 0000000..0d64314 --- /dev/null +++ b/docker/docker-compose.yml @@ -0,0 +1,34 @@ +services: + leader: + build: + context: . + dockerfile: Dockerfile.leader + container_name: leader + hostname: leader + ports: + - "8085:8080" + volumes: + - ../:/workspace + - leader-data:/data + - ./shared-config:/shared/config + - shared-skills:/shared/skills + - shared-commands:/shared/commands + - shared-agents:/shared/agents + environment: + - ROLE=leader + - TEAM=marketing,sales,engineers + - OPENCODE_CONFIG_DIR=/shared/config + - XDG_CONFIG_HOME=/shared/config + restart: unless-stopped + +volumes: + leader-data: + name: leader-data + shared-config: + name: agent-shared-config + shared-skills: + name: agent-shared-skills + shared-commands: + name: agent-shared-commands + shared-agents: + name: agent-shared-agents diff --git a/docker/shared-config/oh-my-opencode.json b/docker/shared-config/oh-my-opencode.json new file mode 100644 index 0000000..837f4b5 --- /dev/null +++ b/docker/shared-config/oh-my-opencode.json @@ -0,0 +1,104 @@ +{ + "$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/master/assets/oh-my-opencode.schema.json", + "google_auth": false, + "disabled_mcps": ["websearch_exa"], + "disabled_hooks": [], + "sisyphus_agent": { + "disabled": false, + "temperature": 0.3 + }, + "agents": { + "Sisyphus": { + "model": "opencode/glm-4.7-free", + "temperature": 0.3 + }, + "oracle": { + "model": "opencode/gpt-5-nano", + "temperature": 0.2 + }, + "librarian": { + "model": "opencode/minimax-m2.1-free", + "temperature": 0.1 + }, + "explore": { + "model": "opencode/grok-code", + "temperature": 0.4 + }, + "frontend-ui-ux-engineer": { + "model": "opencode/glm-4.7-free", + "temperature": 0.3 + }, + "document-writer": { + "model": "opencode/gpt-5-nano", + "temperature": 0.2 + }, + "multimodal-looker": { + "model": "opencode/glm-4.7-free", + "temperature": 0.3 + } + }, + "experimental": { + "preemptive_compaction": true, + "preemptive_compaction_threshold": 0.70, + "truncate_all_tool_outputs": false, + "dynamic_context_pruning": { + "enabled": true, + "notification": "minimal", + "turn_protection": { + "enabled": true, + "turns": 3 + }, + "protected_tools": [ + "task", + "todowrite", + "todoread", + "lsp_diagnostics", + "lsp_code_actions", + "lsp_hover", + "lsp_goto_definition", + "lsp_find_references", + "lsp_rename", + "ast_grep_search", + "ast_grep_replace", + "grep", + "read", + "write", + "edit", + "run_terminal_cmd" + ], + "strategies": { + "deduplication": { + "enabled": true + }, + "supersede_writes": { + "enabled": true + }, + "purge_errors": { + "enabled": true, + "turns": 2 + }, + "code_artifact_protection": { + "enabled": true, + "protect_todos": true, + "protect_code_changes": true, + "preserve_recent_tool_outputs": 5, + "todo_codebase_compaction": { + "enabled": true, + "max_recent_messages": 5, + "structured_summaries": true, + "preserve_coding_context": true + } + } + } + } + }, + "background_task": { + "defaultConcurrency": 2, + "providerConcurrency": { + "opencode": 3 + } + }, + "notification": { + "force_enable": false + } +} \ No newline at end of file diff --git a/docker/shared-config/opencode.json b/docker/shared-config/opencode.json new file mode 100644 index 0000000..0b2d506 --- /dev/null +++ b/docker/shared-config/opencode.json @@ -0,0 +1,52 @@ +{ + "$schema": "https://opencode.ai/config.json", + "keybinds": { + "model_cycle_recent": "alt+m", + "model_cycle_recent_reverse": "alt+shift+m" + }, + "plugin": [ + "oh-my-opencode" + ], + "autoupdate": false, + "model": "opencode/glm-4.7-free", + "small_model": "opencode/glm-4.7-free", + "theme": "tokyonight", + "tools": { + "todoread": true, + "todowrite": true, + "webfetch": true + }, + "permission": { + "bash": "ask", + "write": "ask", + "edit": "ask", + "read": "allow", + "todowrite": "allow", + "todoread": "allow", + "task": "ask", + "grep": "allow", + "glob": "allow", + "list": "allow", + "webfetch": "allow", + "skill": { + "*": "allow" + } + }, + "provider": { + "opencode": { + "name": "OpenCode Free", + "models": { + "glm-4.7-free": { + "name": "GLM 4.7 Free", + "limit": { "context": 32768, "output": 4096 }, + "modalities": { "input": ["text"], "output": ["text"] } + }, + "gpt-5-nano": { + "name": "GPT-5 Nano", + "limit": { "context": 128000, "output": 4096 }, + "modalities": { "input": ["text"], "output": ["text"] } + } + } + } + } +} \ No newline at end of file diff --git a/docker/shared-config/opencode.jsonc b/docker/shared-config/opencode.jsonc new file mode 100644 index 0000000..825ff8d --- /dev/null +++ b/docker/shared-config/opencode.jsonc @@ -0,0 +1,118 @@ +{ + "plugin": [ + "oh-my-opencode" + ], + "$schema": "https://opencode.ai/config.json", + "autoupdate": false, + "model": "opencode/glm-4.7-free", + "small_model": "opencode/gpt-5-nano", + "theme": "tokyonight", + "keybinds": { + "model_cycle_recent": "alt+m", + "model_cycle_recent_reverse": "alt+shift+m" + }, + "tools": { + "todoread": true, + "todowrite": true, + "webfetch": true, + "lsp_diagnostics": true, + "lsp_code_actions": true + }, + "permission": { + "bash": "ask", + "write": "ask", + "edit": "ask", + "read": "allow", + "todowrite": "allow", + "todoread": "allow", + "task": "ask", + "grep": "allow", + "glob": "allow", + "list": "allow", + "webfetch": "allow", + "lsp_diagnostics": "allow", + "lsp_code_actions": "allow", + "skill": { + "*": "allow" + } + }, + "mcp": { + "graphiti-memory": { + "type": "remote", + "url": "http://10.100.0.17:8080/mcp/", + "enabled": true, + "oauth": false, + "timeout": 30000, + "headers": { + "X-API-Key": "0c1ab2355207927cf0ca255cfb9dfe1ed15d68eacb0d6c9f5cb9f08494c3a315" + } + } + }, + "provider": { + "opencode": { + "name": "OpenCode Free", + "models": { + "glm-4.7-free": { + "name": "GLM 4.7 Free", + "limit": { + "context": 32768, + "output": 4096 + }, + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + } + }, + "gpt-5-nano": { + "name": "GPT-5 Nano", + "limit": { + "context": 128000, + "output": 4096 + }, + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + } + }, + "minimax-m2.1-free": { + "name": "MiniMax M2.1 Free", + "limit": { + "context": 32768, + "output": 4096 + }, + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + } + }, + "grok-code": { + "name": "Grok Code", + "limit": { + "context": 128000, + "output": 4096 + }, + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + } + } + } + } + } +} \ No newline at end of file diff --git a/docker/shared-config/test-persistence.txt b/docker/shared-config/test-persistence.txt new file mode 100644 index 0000000..987e658 --- /dev/null +++ b/docker/shared-config/test-persistence.txt @@ -0,0 +1 @@ +TEST: This change should persist rebuilds diff --git a/reload.sh b/reload.sh new file mode 100755 index 0000000..dd94100 --- /dev/null +++ b/reload.sh @@ -0,0 +1,79 @@ +#!/bin/bash +# Oh My OpenCode Free - Reload Script +# Reloads configuration changes without full rebuild when possible + +set -e + +echo "🔄 Reloading Oh My OpenCode Free Edition..." + +if command -v docker &> /dev/null; then + CONTAINER_CMD="docker" +elif command -v podman &> /dev/null; then + CONTAINER_CMD="podman" +else + echo "❌ Neither Docker nor Podman found." + exit 1 +fi + +echo "đŸ“Ļ Using $CONTAINER_CMD" + +cd "$(dirname "$0")" + +CONTAINER_NAME="leader" + +# Check if container exists and is running +if ! $CONTAINER_CMD ps -q -f name="$CONTAINER_NAME" | grep -q .; then + echo "❌ Container '$CONTAINER_NAME' is not running." + echo "💡 Run './start.sh' first to start the container." + exit 1 +fi + +echo "✅ Container '$CONTAINER_NAME' is running." + +# Check if config files have changed (now requires rebuild) +CONFIG_CHANGED=false +if [ "docker/shared-config/oh-my-opencode.json" -nt ".last_reload" ] 2>/dev/null || \ + [ "docker/shared-config/opencode.jsonc" -nt ".last_reload" ] 2>/dev/null; then + REBUILD_NEEDED=true + echo "📝 Configuration files have changed - rebuild required." +fi + +# Check if Dockerfile or docker-compose changed (requires rebuild) +REBUILD_NEEDED=false +if [ "docker/Dockerfile.leader" -nt ".last_reload" ] 2>/dev/null || \ + [ "docker/docker-compose.yml" -nt ".last_reload" ] 2>/dev/null; then + REBUILD_NEEDED=true + echo "đŸ—ī¸ Docker files changed - full rebuild required." +fi + +if [ "$REBUILD_NEEDED" = true ]; then + echo "🔄 Performing full container rebuild..." + + # Stop current container + echo "🛑 Stopping current container..." + $CONTAINER_CMD compose -f docker/docker-compose.yml down + + # Rebuild and start + echo "đŸ—ī¸ Rebuilding and starting container..." + $CONTAINER_CMD compose -f docker/docker-compose.yml up -d --build + + echo "âŗ Waiting for rebuild to complete..." + sleep 10 + + # Verify new container is running + if $CONTAINER_CMD ps -q -f name="$CONTAINER_NAME" | grep -q .; then + echo "✅ Container rebuilt and running successfully!" + echo "🌐 Server available at: http://localhost:8080" + echo "📋 Attach with: opencode attach http://localhost:8080" + else + echo "❌ Failed to rebuild container. Check logs:" + echo " $CONTAINER_CMD logs $CONTAINER_NAME" + exit 1 + fi + +fi + +# Update last reload timestamp +touch .last_reload + +echo "đŸŽ¯ Reload complete! Configuration changes are now active." \ No newline at end of file diff --git a/src/hooks/usage-logging/index.ts b/src/hooks/usage-logging/index.ts index 2f7346a..35c48b6 100644 --- a/src/hooks/usage-logging/index.ts +++ b/src/hooks/usage-logging/index.ts @@ -1,5 +1,3 @@ -import { createHash } from 'crypto'; - interface LogEvent { timestamp: string; stack_name: string; @@ -18,10 +16,6 @@ interface UsageLoggingOptions { const DEFAULT_INGEST_URL = process.env.LOG_INGEST_URL || 'http://10.100.0.20:3102/ingest'; const DEFAULT_STACK_NAME = process.env.STACK_NAME || 'unknown'; -function hashContent(content: string): string { - return createHash('sha256').update(content).digest('hex').substring(0, 16); -} - function getWordCount(content: string): number { return content.split(/\s+/).filter(Boolean).length; } @@ -111,11 +105,13 @@ export function createUsageLoggingHook(options: UsageLoggingOptions = {}) { try { switch (event.type) { case 'session.created': { - const info = props.info as { id?: string } | undefined; + const info = props.info as { id?: string; model?: string; agent?: string } | undefined; if (info?.id) { getOrCreateSessionStats(info.id); queueEvent(info.id, 'session_start', { - start_time: Date.now() + start_time: Date.now(), + model: info.model, + agent: info.agent }); } break; @@ -141,7 +137,7 @@ export function createUsageLoggingHook(options: UsageLoggingOptions = {}) { case 'message.created': { const sessionID = props.sessionID as string | undefined; - const message = props.message as { role?: string; content?: string } | undefined; + const message = props.message as { role?: string; content?: unknown } | undefined; if (sessionID && message) { const stats = getOrCreateSessionStats(sessionID); stats.messageCount++; @@ -152,7 +148,7 @@ export function createUsageLoggingHook(options: UsageLoggingOptions = {}) { queueEvent(sessionID, 'message', { role: message.role || 'unknown', - content_hash: hashContent(content), + content: content, content_length: content.length, word_count: getWordCount(content), message_number: stats.messageCount @@ -163,10 +159,11 @@ export function createUsageLoggingHook(options: UsageLoggingOptions = {}) { case 'tool.started': { const sessionID = props.sessionID as string | undefined; - const tool = props.tool as { name?: string } | undefined; + const tool = props.tool as { name?: string; input?: unknown } | undefined; if (sessionID && tool?.name) { queueEvent(sessionID, 'tool_start', { tool: tool.name, + input: tool.input, start_time: Date.now() }); } @@ -175,14 +172,16 @@ export function createUsageLoggingHook(options: UsageLoggingOptions = {}) { case 'tool.completed': { const sessionID = props.sessionID as string | undefined; - const tool = props.tool as { name?: string } | undefined; - const result = props.result as { error?: unknown } | undefined; + const tool = props.tool as { name?: string; input?: unknown } | undefined; + const result = props.result as { error?: unknown; output?: unknown } | undefined; if (sessionID && tool?.name) { const stats = getOrCreateSessionStats(sessionID); stats.toolUseCount++; queueEvent(sessionID, 'tool_use', { tool: tool.name, + input: tool.input, + output: result?.output, success: !result?.error, error_message: result?.error ? String(result.error) : undefined, tool_use_number: stats.toolUseCount @@ -205,13 +204,14 @@ export function createUsageLoggingHook(options: UsageLoggingOptions = {}) { case 'tokens.used': { const sessionID = props.sessionID as string | undefined; - const usage = props.usage as { input_tokens?: number; output_tokens?: number } | undefined; + const usage = props.usage as { input_tokens?: number; output_tokens?: number; model?: string } | undefined; if (sessionID && usage) { const stats = getOrCreateSessionStats(sessionID); stats.tokensIn += usage.input_tokens || 0; stats.tokensOut += usage.output_tokens || 0; queueEvent(sessionID, 'tokens', { + model: usage.model, tokens_in: usage.input_tokens, tokens_out: usage.output_tokens, total_tokens_in: stats.tokensIn, diff --git a/start.sh b/start.sh new file mode 100755 index 0000000..14e9233 --- /dev/null +++ b/start.sh @@ -0,0 +1,46 @@ +#!/bin/bash + +set -e + +echo "🚀 Starting Oh My OpenCode Free - Custom Fork (Todo+Codebase Compaction)..." + +if command -v docker &> /dev/null; then + CONTAINER_CMD="docker" +elif command -v podman &> /dev/null; then + CONTAINER_CMD="podman" +else + echo "❌ Neither Docker nor Podman found. Please install one of them." + exit 1 +fi + +echo "đŸ“Ļ Using $CONTAINER_CMD" + +cd "$(dirname "$0")" + +echo "đŸ—ī¸ Building and starting OpenCode server container..." +$CONTAINER_CMD compose -f docker/docker-compose.yml up -d --build + +echo "âŗ Waiting for server to start..." +sleep 5 + +# Check if server is running +if $CONTAINER_CMD ps | grep -q leader; then + echo "✅ OpenCode server is running!" + echo "" + echo "🌐 Server URL: http://localhost:8080" + echo "" + echo "📋 To attach from another terminal:" + echo " opencode attach http://localhost:8080" + echo "" +echo "🔍 To check server logs:" +echo " $CONTAINER_CMD logs leader" + echo "" +echo "🛑 To stop the server:" +echo " $CONTAINER_CMD compose -f docker/docker-compose.yml down" + echo "" + echo "đŸŽ¯ Custom server is ready - Todo+Codebase compaction active!" +else + echo "❌ Failed to start server. Check logs:" + echo " docker logs leader" + exit 1 +fi \ No newline at end of file diff --git a/tui.sh b/tui.sh new file mode 100755 index 0000000..885ec1b --- /dev/null +++ b/tui.sh @@ -0,0 +1,547 @@ +#!/bin/bash +# Oh My OpenCode Free - Interactive TUI +# Beautiful terminal interface for managing the OpenCode server +# Created by: Oussama Douhou + +set -e + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +MAGENTA='\033[0;35m' +CYAN='\033[0;36m' +WHITE='\033[1;37m' +NC='\033[0m' + +BOLD='\033[1m' +DIM='\033[2m' + +PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +DOCKER_DIR="$PROJECT_DIR/docker" +CONFIG_DIR="$DOCKER_DIR/shared-config" + +CONTAINER_NAME="leader-custom" + +# Detect server port from running container +detect_server_port() { + if $CONTAINER_CMD ps -q -f name="$CONTAINER_NAME" | grep -q .; then + SERVER_PORT=$($CONTAINER_CMD inspect "$CONTAINER_NAME" --format='{{(index (index .NetworkSettings.Ports "8080/tcp") 0).HostPort}}' 2>/dev/null) + if [ -z "$SERVER_PORT" ]; then + SERVER_PORT="8085" # fallback to default external port + fi + else + SERVER_PORT="8085" # fallback when container not running + fi +} + +detect_server_port +SERVER_URL="http://localhost:$SERVER_PORT" + +# Check if dependencies are available +check_dependencies() { + local missing_deps=() + + if ! command -v docker &> /dev/null && ! command -v podman &> /dev/null; then + missing_deps+=("docker or podman") + fi + + if ! command -v curl &> /dev/null; then + missing_deps+=("curl") + fi + + if ! command -v jq &> /dev/null; then + missing_deps+=("jq") + fi + + if [ ${#missing_deps[@]} -ne 0 ]; then + echo -e "${RED}❌ Missing dependencies: ${missing_deps[*]}${NC}" + echo -e "${YELLOW}Please install them and try again.${NC}" + exit 1 + fi + + if command -v docker &> /dev/null; then + CONTAINER_CMD="docker" + else + CONTAINER_CMD="podman" + fi +} + +# ASCII Art Header +show_header() { + clear + echo -e "${CYAN}" + cat << 'EOF' +╔══════════════════════════════════════════════════════════════╗ +║ ║ +║ ███████╗ ██╗ ██╗ ███╗ ███╗██╗ ██╗ ║ +║ ██╔════╝ ██║ ██║ ████╗ ████║╚██╗ ██╔╝ ║ +║ ███████╗ ███████║ ██╔████╔██║ ╚████╔╝ ║ +║ ╚════██║ ██╔══██║ ██║╚██╔╝██║ ╚██╔╝ ║ +║ ███████║ ██║ ██║ ██║ ╚═╝ ██║ ██║ ║ +║ ╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ║ +║ ║ +║ ██████╗ ██████╗ ███████╗███╗ ██╗ ██████╗ ║ +║ ██╔═══██╗██╔══██╗██╔════╝████╗ ██║██╔════╝ ║ +║ ██║ ██║██████╔╝█████╗ ██╔██╗ ██║██║ ███╗ ║ +║ ██║ ██║██╔═══╝ ██╔══╝ ██║╚██╗██║██║ ██║ ║ +║ ╚██████╔╝██║ ███████╗██║ ╚████║╚██████╔╝ ║ +║ ╚═════╝ ╚═╝ ╚══════╝╚═╝ ╚═══╝ ╚═════╝ ║ +║ ║ +║ ███████╗██████╗ ███████╗███████╗ ║ +║ ██╔════╝██╔══██╗██╔════╝██╔════╝ ║ +║ █████╗ ██████╔╝█████╗ █████╗ ║ +║ ██╔══╝ ██╔══██╗██╔══╝ ██╔══╝ ║ +║ ██║ ██║ ██║███████╗███████╗ ║ +║ ╚═╝ ╚═╝ ╚═╝╚══════╝╚══════╝ ║ +║ ║ +║ 🌟 100% FREE AI CODING ASSISTANT 🌟 ║ +║ ║ +║ Created by: Oussama Douhou ║ +║ Email: oussama.douhou@gmail.com ║ +║ ║ +╚══════════════════════════════════════════════════════════════╝ +EOF + echo -e "${NC}" +} + +# Status indicators +show_status() { + echo -e "${BLUE}📊 System Status:${NC}" + echo -e "${DIM}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" + + echo -n "đŸŗ Container: " + if $CONTAINER_CMD ps -q -f name="$CONTAINER_NAME" | grep -q .; then + echo -e "${GREEN}● Running${NC}" + else + echo -e "${RED}● Stopped${NC}" + fi + + echo -n "🌐 Server: " + if curl -s -f --max-time 2 "$SERVER_URL" > /dev/null 2>&1; then + echo -e "${GREEN}● Running${NC} (Port $SERVER_PORT)" + else + echo -e "${RED}● Stopped${NC}" + fi + + echo -n "🤖 OpenCode: " + if $CONTAINER_CMD ps -q -f name="$CONTAINER_NAME" | grep -q . && \ + $CONTAINER_CMD exec "$CONTAINER_NAME" pgrep -f "opencode serve" > /dev/null 2>&1; then + echo -e "${GREEN}● Active${NC} (oh-my-opencode loaded)" + else + echo -e "${RED}● Inactive${NC}" + fi + + # Server status + if curl -s -f --max-time 2 "$SERVER_URL" > /dev/null 2>&1; then + echo -e "🌐 Server: ${GREEN}● Running${NC} (Port $SERVER_PORT)" + else + echo -e "🌐 Server: ${RED}● Stopped${NC}" + fi + + # OpenCode status + if $CONTAINER_CMD ps -q -f name="$CONTAINER_NAME" | grep -q . && \ + $CONTAINER_CMD exec "$CONTAINER_NAME" pgrep -f "opencode serve" > /dev/null 2>&1; then + echo -e "🤖 OpenCode: ${GREEN}● Active${NC} (oh-my-opencode loaded)" + else + echo -e "🤖 OpenCode: ${RED}● Inactive${NC}" + fi + + echo +} + +# Main menu +show_main_menu() { + echo -e "${YELLOW}đŸŽ›ī¸ Main Menu:${NC}" + echo -e "${DIM}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" + echo -e "1) 🚀 Start Server - Launch OpenCode server" + echo -e "2) 🛑 Stop Server - Shutdown server and container" + echo -e "3) 🔄 Reload Configuration - Apply config changes" + echo -e "4) 🔗 Attach to Server - Connect to running server" + echo -e "5) 📝 Edit Configuration - Modify agent/model settings" + echo -e "6) 📊 View Logs - Check server and container logs" + echo -e "7) 🔍 Server Health Check - Test server connectivity" + echo -e "8) 📚 Documentation - View project documentation" + echo -e "9) âš™ī¸ System Information - Show system and version info" + echo -e "0) 👋 Exit - Quit the application" + echo +} + +# Start server +start_server() { + echo -e "${BLUE}🚀 Starting OpenCode Server...${NC}" + + if $CONTAINER_CMD ps -q -f name="$CONTAINER_NAME" | grep -q .; then + echo -e "${YELLOW}âš ī¸ Container is already running. Use reload if you made changes.${NC}" + read -p "Press Enter to continue..." + return + fi + + echo -e "đŸ—ī¸ Building and starting container..." + if $CONTAINER_CMD compose -f "$DOCKER_DIR/docker-compose.yml" up -d --build; then + echo -e "${GREEN}✅ Container started successfully!${NC}" + + echo -e "âŗ Waiting for server to initialize..." + for i in {1..10}; do + if curl -s -f --max-time 2 "$SERVER_URL" > /dev/null 2>&1; then + echo -e "${GREEN}🎉 Server is ready at $SERVER_URL${NC}" + echo + echo -e "${CYAN}📋 Quick Start Commands:${NC}" + echo -e " Attach locally: ${WHITE}opencode attach $SERVER_URL${NC}" + echo -e " Attach remotely: ${WHITE}opencode attach http://YOUR_IP:$SERVER_PORT${NC}" + break + fi + echo -n "." + sleep 1 + done + + if [ $i -eq 10 ]; then + echo -e "${YELLOW}âš ī¸ Server may still be starting. Check status in main menu.${NC}" + fi + else + echo -e "${RED}❌ Failed to start container. Check logs for details.${NC}" + fi + + echo + read -p "Press Enter to return to main menu..." +} + +# Stop server +stop_server() { + echo -e "${BLUE}🛑 Stopping OpenCode Server...${NC}" + + if ! $CONTAINER_CMD ps -q -f name="$CONTAINER_NAME" | grep -q .; then + echo -e "${YELLOW}âš ī¸ Container is not running.${NC}" + read -p "Press Enter to continue..." + return + fi + + if $CONTAINER_CMD compose -f "$DOCKER_DIR/docker-compose.yml" down; then + echo -e "${GREEN}✅ Server stopped successfully!${NC}" + else + echo -e "${RED}❌ Failed to stop server properly.${NC}" + fi + + echo + read -p "Press Enter to return to main menu..." +} + +# Reload configuration +reload_config() { + echo -e "${BLUE}🔄 Reloading Configuration...${NC}" + + if [ ! -f "$PROJECT_DIR/reload.sh" ]; then + echo -e "${RED}❌ Reload script not found at $PROJECT_DIR/reload.sh${NC}" + read -p "Press Enter to continue..." + return + fi + + echo -e "🔍 Analyzing changes..." + bash "$PROJECT_DIR/reload.sh" + + echo + read -p "Press Enter to return to main menu..." +} + +# Attach to server +attach_server() { + echo -e "${BLUE}🔗 Attaching to OpenCode Server...${NC}" + + if ! curl -s -f --max-time 2 "$SERVER_URL" > /dev/null 2>&1; then + echo -e "${RED}❌ Server is not running or not accessible.${NC}" + echo -e "${YELLOW}💡 Start the server first using option 1.${NC}" + read -p "Press Enter to continue..." + return + fi + + echo -e "${GREEN}✅ Server is accessible at $SERVER_URL${NC}" + echo + echo -e "${CYAN}Choose attachment method:${NC}" + echo -e "1) Local terminal attachment" + echo -e "2) Remote attachment (show command only)" + echo -e "3) Back to main menu" + echo + + read -p "Enter choice (1-3): " choice + + case $choice in + 1) + echo -e "${YELLOW}🔗 Attaching to server...${NC}" + echo -e "${DIM}(Press Ctrl+C to detach)${NC}" + sleep 2 + opencode attach "$SERVER_URL" + ;; + 2) + echo -e "${WHITE}📋 Remote attachment command:${NC}" + echo -e "${CYAN}opencode attach $SERVER_URL${NC}" + echo + echo -e "${YELLOW}💡 Replace 'localhost' with your server's IP address for remote access.${NC}" + ;; + 3) + return + ;; + *) + echo -e "${RED}❌ Invalid choice.${NC}" + ;; + esac + + echo + read -p "Press Enter to return to main menu..." +} + +# Edit configuration +edit_config() { + echo -e "${BLUE}📝 Configuration Editor${NC}" + echo -e "${DIM}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" + + echo -e "${CYAN}Available configuration files:${NC}" + echo -e "1) oh-my-opencode.json - Agent and model settings" + echo -e "2) opencode.jsonc - OpenCode core configuration" + echo -e "3) Back to main menu" + echo + + read -p "Enter choice (1-3): " choice + + case $choice in + 1) + if command -v nano &> /dev/null; then + nano "$CONFIG_DIR/oh-my-opencode.json" + elif command -v vim &> /dev/null; then + vim "$CONFIG_DIR/oh-my-opencode.json" + else + echo -e "${YELLOW}âš ī¸ No editor found. Please edit manually:${NC}" + echo -e "${WHITE}$CONFIG_DIR/oh-my-opencode.json${NC}" + fi + ;; + 2) + if command -v nano &> /dev/null; then + nano "$CONFIG_DIR/opencode.jsonc" + elif command -v vim &> /dev/null; then + vim "$CONFIG_DIR/opencode.jsonc" + else + echo -e "${YELLOW}âš ī¸ No editor found. Please edit manually:${NC}" + echo -e "${WHITE}$CONFIG_DIR/opencode.jsonc${NC}" + fi + ;; + 3) + return + ;; + *) + echo -e "${RED}❌ Invalid choice.${NC}" + ;; + esac + + echo + read -p "Press Enter to return to main menu..." +} + +# View logs +view_logs() { + echo -e "${BLUE}📊 Log Viewer${NC}" + echo -e "${DIM}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" + + echo -e "${CYAN}Choose log type:${NC}" + echo -e "1) Container logs - Docker container output" + echo -e "2) OpenCode logs - Server application logs" + echo -e "3) Build logs - Last build/reload output" + echo -e "4) Back to main menu" + echo + + read -p "Enter choice (1-4): " choice + + case $choice in + 1) + echo -e "${YELLOW}đŸŗ Container Logs (last 50 lines):${NC}" + echo -e "${DIM}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" + $CONTAINER_CMD logs --tail 50 "$CONTAINER_NAME" 2>/dev/null || echo -e "${RED}❌ Container not running or logs unavailable${NC}" + ;; + 2) + if $CONTAINER_CMD ps -q -f name="$CONTAINER_NAME" | grep -q .; then + echo -e "${YELLOW}🤖 OpenCode Server Logs:${NC}" + echo -e "${DIM}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" + $CONTAINER_CMD exec "$CONTAINER_NAME" journalctl -u opencode -n 50 --no-pager 2>/dev/null || \ + $CONTAINER_CMD exec "$CONTAINER_NAME" tail -50 /var/log/opencode.log 2>/dev/null || \ + echo -e "${YELLOW}â„šī¸ Logs not available through standard locations${NC}" + else + echo -e "${RED}❌ Container not running${NC}" + fi + ;; + 3) + echo -e "${YELLOW}đŸ—ī¸ Build/Reload Logs:${NC}" + echo -e "${DIM}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" + if [ -f "$PROJECT_DIR/.last_reload" ]; then + echo -e "${GREEN}Last reload: $(date -r "$PROJECT_DIR/.last_reload")${NC}" + else + echo -e "${YELLOW}No reload history found${NC}" + fi + ;; + 4) + return + ;; + *) + echo -e "${RED}❌ Invalid choice.${NC}" + ;; + esac + + echo + read -p "Press Enter to return to main menu..." +} + +# Health check +health_check() { + echo -e "${BLUE}🔍 Server Health Check${NC}" + echo -e "${DIM}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" + + echo -n "đŸŗ Container status: " + if $CONTAINER_CMD ps -q -f name="$CONTAINER_NAME" | grep -q .; then + echo -e "${GREEN}● Running${NC}" + else + echo -e "${RED}● Stopped${NC}" + fi + + echo -n "🌐 Server connectivity: " + if curl -s -f --max-time 5 "$SERVER_URL" > /dev/null 2>&1; then + echo -e "${GREEN}● Connected${NC}" + + response_time=$(curl -s -w "%{time_total}" -o /dev/null "$SERVER_URL" 2>/dev/null || echo "0") + if (( $(echo "$response_time > 0" | bc -l 2>/dev/null || echo "0") )); then + echo -e "âąī¸ Response time: ${GREEN}${response_time}s${NC}" + fi + else + echo -e "${RED}● Disconnected${NC}" + fi + + echo -n "🔌 oh-my-opencode plugin: " + if $CONTAINER_CMD ps -q -f name="$CONTAINER_NAME" | grep -q .; then + # Check if server is responding (main indicator) + if curl -s --max-time 2 "$SERVER_URL" > /dev/null 2>&1; then + # Check for plugin-specific indicators in logs + if $CONTAINER_CMD exec "$CONTAINER_NAME" sh -c "cat /var/log/opencode.log 2>/dev/null | grep -q 'oh-my-opencode'" 2>/dev/null; then + echo -e "${GREEN}● Loaded${NC}" + else + echo -e "${YELLOW}● Basic server only${NC}" + fi + else + echo -e "${RED}● Server not responding${NC}" + fi + else + echo -e "${YELLOW}● N/A (container stopped)${NC}" + fi + + if $CONTAINER_CMD ps -q -f name="$CONTAINER_NAME" | grep -q .; then + echo + echo -e "${CYAN}📊 Resource Usage:${NC}" + $CONTAINER_CMD stats --no-stream --format "table {{.Container}}\t{{.CPUPerc}}\t{{.MemUsage}}" "$CONTAINER_NAME" 2>/dev/null || echo -e "${YELLOW}Resource stats unavailable${NC}" + fi + + echo + read -p "Press Enter to return to main menu..." +} + +# Documentation +show_docs() { + echo -e "${BLUE}📚 Documentation${NC}" + echo -e "${DIM}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" + + if [ -f "$PROJECT_DIR/README.md" ]; then + echo -e "${CYAN}Opening README.md...${NC}" + if command -v less &> /dev/null; then + less "$PROJECT_DIR/README.md" + elif command -v more &> /dev/null; then + more "$PROJECT_DIR/README.md" + else + cat "$PROJECT_DIR/README.md" + fi + else + echo -e "${RED}❌ README.md not found${NC}" + fi + + echo + read -p "Press Enter to return to main menu..." +} + +# System information +show_system_info() { + echo -e "${BLUE}âš™ī¸ System Information${NC}" + echo -e "${DIM}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" + + echo -e "${CYAN}đŸ–Ĩī¸ Host System:${NC}" + echo -e " OS: $(uname -s) $(uname -r)" + echo -e " Shell: $SHELL" + echo -e " User: $(whoami)" + echo + + echo -e "${CYAN}đŸŗ Container Runtime:${NC}" + echo -e " Engine: $CONTAINER_CMD" + if command -v "$CONTAINER_CMD" &> /dev/null; then + echo -e " Version: $($CONTAINER_CMD --version)" + fi + echo + + echo -e "${CYAN}📁 Project Information:${NC}" + echo -e " Project: $PROJECT_DIR" + echo -e " Config: $CONFIG_DIR" + echo -e " Port: $SERVER_PORT" + echo + + echo -e "${CYAN}🤖 OpenCode Status:${NC}" + if command -v opencode &> /dev/null; then + opencode --version 2>/dev/null || echo -e " Version: Unknown" + else + echo -e " Status: ${RED}Not installed${NC}" + fi + echo + + echo -e "${CYAN}📊 Configuration Summary:${NC}" + if [ -f "$CONFIG_DIR/oh-my-opencode.json" ]; then + agent_count=$(jq '.agents | length' "$CONFIG_DIR/oh-my-opencode.json" 2>/dev/null || echo "Unknown") + echo -e " Agents: $agent_count configured" + fi + + if [ -f "$CONFIG_DIR/opencode.jsonc" ]; then + model_count=$(jq '.provider.opencode.models | length' "$CONFIG_DIR/opencode.jsonc" 2>/dev/null || echo "Unknown") + echo -e " Models: $model_count available" + fi + + echo + read -p "Press Enter to return to main menu..." +} + +# Main function +main() { + check_dependencies + + while true; do + show_header + show_status + show_main_menu + + read -p "Enter your choice (0-9): " choice + + case $choice in + 1) start_server ;; + 2) stop_server ;; + 3) reload_config ;; + 4) attach_server ;; + 5) edit_config ;; + 6) view_logs ;; + 7) health_check ;; + 8) show_docs ;; + 9) show_system_info ;; + 0) + echo -e "${GREEN}👋 Thank you for using Oh My OpenCode Free!${NC}" + echo -e "${CYAN}Created by Oussama Douhou${NC}" + echo -e "${DIM}oussama.douhou@gmail.com${NC}" + echo -e "${CYAN}Happy coding! 🚀${NC}" + exit 0 + ;; + *) + echo -e "${RED}❌ Invalid choice. Please enter 0-9.${NC}" + sleep 2 + ;; + esac + done +} + +# Run main function +main \ No newline at end of file