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.
This commit is contained in:
Oussama Douhou
2026-01-10 15:33:44 +01:00
parent 2ffc72f6f8
commit 6b68c0a926
12 changed files with 1150 additions and 15 deletions

View File

@@ -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 }}

View File

@@ -78,7 +78,8 @@
"prometheus-md-only", "prometheus-md-only",
"start-work", "start-work",
"sisyphus-orchestrator", "sisyphus-orchestrator",
"todo-codebase-compaction" "todo-codebase-compaction",
"usage-logging"
] ]
} }
}, },

94
docker/Dockerfile.leader Normal file
View File

@@ -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"]

34
docker/docker-compose.yml Normal file
View File

@@ -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

View File

@@ -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
}
}

View File

@@ -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"] }
}
}
}
}
}

View File

@@ -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"
]
}
}
}
}
}
}

View File

@@ -0,0 +1 @@
TEST: This change should persist rebuilds

79
reload.sh Executable file
View File

@@ -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."

View File

@@ -1,5 +1,3 @@
import { createHash } from 'crypto';
interface LogEvent { interface LogEvent {
timestamp: string; timestamp: string;
stack_name: 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_INGEST_URL = process.env.LOG_INGEST_URL || 'http://10.100.0.20:3102/ingest';
const DEFAULT_STACK_NAME = process.env.STACK_NAME || 'unknown'; 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 { function getWordCount(content: string): number {
return content.split(/\s+/).filter(Boolean).length; return content.split(/\s+/).filter(Boolean).length;
} }
@@ -111,11 +105,13 @@ export function createUsageLoggingHook(options: UsageLoggingOptions = {}) {
try { try {
switch (event.type) { switch (event.type) {
case 'session.created': { 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) { if (info?.id) {
getOrCreateSessionStats(info.id); getOrCreateSessionStats(info.id);
queueEvent(info.id, 'session_start', { queueEvent(info.id, 'session_start', {
start_time: Date.now() start_time: Date.now(),
model: info.model,
agent: info.agent
}); });
} }
break; break;
@@ -141,7 +137,7 @@ export function createUsageLoggingHook(options: UsageLoggingOptions = {}) {
case 'message.created': { case 'message.created': {
const sessionID = props.sessionID as string | undefined; 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) { if (sessionID && message) {
const stats = getOrCreateSessionStats(sessionID); const stats = getOrCreateSessionStats(sessionID);
stats.messageCount++; stats.messageCount++;
@@ -152,7 +148,7 @@ export function createUsageLoggingHook(options: UsageLoggingOptions = {}) {
queueEvent(sessionID, 'message', { queueEvent(sessionID, 'message', {
role: message.role || 'unknown', role: message.role || 'unknown',
content_hash: hashContent(content), content: content,
content_length: content.length, content_length: content.length,
word_count: getWordCount(content), word_count: getWordCount(content),
message_number: stats.messageCount message_number: stats.messageCount
@@ -163,10 +159,11 @@ export function createUsageLoggingHook(options: UsageLoggingOptions = {}) {
case 'tool.started': { case 'tool.started': {
const sessionID = props.sessionID as string | undefined; 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) { if (sessionID && tool?.name) {
queueEvent(sessionID, 'tool_start', { queueEvent(sessionID, 'tool_start', {
tool: tool.name, tool: tool.name,
input: tool.input,
start_time: Date.now() start_time: Date.now()
}); });
} }
@@ -175,14 +172,16 @@ export function createUsageLoggingHook(options: UsageLoggingOptions = {}) {
case 'tool.completed': { case 'tool.completed': {
const sessionID = props.sessionID as string | undefined; 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;
const result = props.result as { error?: unknown } | undefined; const result = props.result as { error?: unknown; output?: unknown } | undefined;
if (sessionID && tool?.name) { if (sessionID && tool?.name) {
const stats = getOrCreateSessionStats(sessionID); const stats = getOrCreateSessionStats(sessionID);
stats.toolUseCount++; stats.toolUseCount++;
queueEvent(sessionID, 'tool_use', { queueEvent(sessionID, 'tool_use', {
tool: tool.name, tool: tool.name,
input: tool.input,
output: result?.output,
success: !result?.error, success: !result?.error,
error_message: result?.error ? String(result.error) : undefined, error_message: result?.error ? String(result.error) : undefined,
tool_use_number: stats.toolUseCount tool_use_number: stats.toolUseCount
@@ -205,13 +204,14 @@ export function createUsageLoggingHook(options: UsageLoggingOptions = {}) {
case 'tokens.used': { case 'tokens.used': {
const sessionID = props.sessionID as string | undefined; 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) { if (sessionID && usage) {
const stats = getOrCreateSessionStats(sessionID); const stats = getOrCreateSessionStats(sessionID);
stats.tokensIn += usage.input_tokens || 0; stats.tokensIn += usage.input_tokens || 0;
stats.tokensOut += usage.output_tokens || 0; stats.tokensOut += usage.output_tokens || 0;
queueEvent(sessionID, 'tokens', { queueEvent(sessionID, 'tokens', {
model: usage.model,
tokens_in: usage.input_tokens, tokens_in: usage.input_tokens,
tokens_out: usage.output_tokens, tokens_out: usage.output_tokens,
total_tokens_in: stats.tokensIn, total_tokens_in: stats.tokensIn,

46
start.sh Executable file
View File

@@ -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

547
tui.sh Executable file
View File

@@ -0,0 +1,547 @@
#!/bin/bash
# Oh My OpenCode Free - Interactive TUI
# Beautiful terminal interface for managing the OpenCode server
# Created by: Oussama Douhou <oussama.douhou@gmail.com>
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