Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6b68c0a926 | ||
|
|
2ffc72f6f8 | ||
|
|
7c7f974e2e | ||
|
|
d5f75138dd | ||
|
|
336de62f7b |
59
.gitea/workflows/docker-publish.yaml
Normal file
59
.gitea/workflows/docker-publish.yaml
Normal 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 }}
|
||||||
402
GIT_STRATEGY.md
Normal file
402
GIT_STRATEGY.md
Normal file
@@ -0,0 +1,402 @@
|
|||||||
|
# Git Strategy and Workflow - Oh My OpenCode Free Fork
|
||||||
|
|
||||||
|
This document outlines the git workflow and branching strategy for the oh-my-opencode-free fork, ensuring clean development, easy upstream updates, and professional collaboration.
|
||||||
|
|
||||||
|
## 🏗️ Branch Structure
|
||||||
|
|
||||||
|
### Main Branches
|
||||||
|
|
||||||
|
| Branch | Purpose | Protection | Upstream Tracking |
|
||||||
|
|--------|---------|------------|-------------------|
|
||||||
|
| **`dev`** | Main development branch | ✅ Protected | `origin/dev` |
|
||||||
|
| **`main`** | Stable releases (future) | ✅ Protected | - |
|
||||||
|
|
||||||
|
### Release Branch (Selective Features)
|
||||||
|
|
||||||
|
| Branch | Purpose | Protection |
|
||||||
|
|--------|---------|------------|
|
||||||
|
| **`release`** | Production-ready with selected features only | ✅ Protected |
|
||||||
|
|
||||||
|
The `release` branch contains **only approved features** cherry-picked from `dev`. This allows:
|
||||||
|
- Excluding experimental or unstable features
|
||||||
|
- Building Docker images with specific feature sets
|
||||||
|
- Clean separation between development and production
|
||||||
|
|
||||||
|
### Feature Branches
|
||||||
|
|
||||||
|
| Branch Type | Naming Convention | Example |
|
||||||
|
|-------------|------------------|---------|
|
||||||
|
| **Features** | `feature/<feature-name>` | `feature/todo-codebase-compaction` |
|
||||||
|
| **Bug Fixes** | `fix/<issue-description>` | `fix/compaction-threshold-calculation` |
|
||||||
|
| **Refactoring** | `refactor/<component>` | `refactor/hook-injection-logic` |
|
||||||
|
| **Experiments** | `experiment/<idea>` | `experiment/alternative-compaction-prompt` |
|
||||||
|
|
||||||
|
## 🎯 Release Branch Workflow (Selective Features)
|
||||||
|
|
||||||
|
The `release` branch is used by the `oh-my-opencode-free` Docker build. Only merge features you want in production.
|
||||||
|
|
||||||
|
### Branch Flow Diagram
|
||||||
|
|
||||||
|
```
|
||||||
|
upstream (origin/dev) ──────────────────────────────► upstream updates
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
dev ─────┬──────────────────────────────────────────► all features
|
||||||
|
│
|
||||||
|
├── feature/todo-compaction ✓ merge to release
|
||||||
|
├── feature/parallel-agents ✓ merge to release
|
||||||
|
└── feature/experimental ✗ skip (not ready)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
release ─────────────────────────────────────────────► selected features only
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Docker build (oh-my-opencode-free)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Adding Features to Release
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Ensure release branch exists and is current
|
||||||
|
git checkout release
|
||||||
|
git pull gitea release
|
||||||
|
|
||||||
|
# Merge a specific feature from dev
|
||||||
|
git merge feature/todo-compaction --no-ff -m "release: add todo-compaction feature"
|
||||||
|
|
||||||
|
# Or cherry-pick specific commits
|
||||||
|
git cherry-pick <commit-hash>
|
||||||
|
|
||||||
|
# Test the build
|
||||||
|
bun install && bun run build && bun run typecheck
|
||||||
|
|
||||||
|
# Push to Gitea (triggers Docker rebuild if CI configured)
|
||||||
|
git push gitea release
|
||||||
|
```
|
||||||
|
|
||||||
|
### Removing Features from Release
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Find the merge commit
|
||||||
|
git log --oneline --merges release
|
||||||
|
|
||||||
|
# Revert the merge
|
||||||
|
git revert -m 1 <merge-commit-hash>
|
||||||
|
|
||||||
|
# Push
|
||||||
|
git push gitea release
|
||||||
|
```
|
||||||
|
|
||||||
|
### Syncing Release with Upstream
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Update dev from upstream first
|
||||||
|
git checkout dev
|
||||||
|
git fetch origin
|
||||||
|
git merge origin/dev
|
||||||
|
|
||||||
|
# Then selectively update release
|
||||||
|
git checkout release
|
||||||
|
|
||||||
|
# Option 1: Rebase release onto new dev (keeps feature selection)
|
||||||
|
git rebase dev
|
||||||
|
|
||||||
|
# Option 2: Reset and re-merge features (clean slate)
|
||||||
|
git reset --hard dev~5 # Go back to before your features
|
||||||
|
git merge feature/todo-compaction --no-ff
|
||||||
|
git merge feature/parallel-agents --no-ff
|
||||||
|
# Skip features you don't want
|
||||||
|
|
||||||
|
git push gitea release --force-with-lease
|
||||||
|
```
|
||||||
|
|
||||||
|
### Listing Features in Release vs Dev
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Show what's in release but not in dev (your additions)
|
||||||
|
git log --oneline release ^dev
|
||||||
|
|
||||||
|
# Show what's in dev but not in release (excluded features)
|
||||||
|
git log --oneline dev ^release
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Development Workflow
|
||||||
|
|
||||||
|
### 1. Starting a New Feature
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Ensure you're on latest dev
|
||||||
|
git checkout dev
|
||||||
|
git pull origin dev
|
||||||
|
|
||||||
|
# Create feature branch
|
||||||
|
git checkout -b feature/your-feature-name
|
||||||
|
|
||||||
|
# Develop and commit
|
||||||
|
git add .
|
||||||
|
git commit -m "feat: Add your feature description"
|
||||||
|
|
||||||
|
# Push feature branch (optional, for collaboration)
|
||||||
|
git push -u origin feature/your-feature-name
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Feature Development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Regular development cycle
|
||||||
|
git add .
|
||||||
|
git commit -m "feat: Implement core logic"
|
||||||
|
|
||||||
|
# Test your changes
|
||||||
|
bun run build && bun run typecheck
|
||||||
|
|
||||||
|
# Make more commits as needed
|
||||||
|
git commit -m "fix: Handle edge case"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Merging to Dev
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Switch to dev and ensure it's up to date
|
||||||
|
git checkout dev
|
||||||
|
git pull origin dev
|
||||||
|
|
||||||
|
# Merge feature branch
|
||||||
|
git merge feature/your-feature-name
|
||||||
|
|
||||||
|
# If merge conflicts, resolve them:
|
||||||
|
# 1. Edit conflicted files
|
||||||
|
# 2. git add <resolved-files>
|
||||||
|
# 3. git commit
|
||||||
|
|
||||||
|
# Push merged changes
|
||||||
|
git push origin dev
|
||||||
|
|
||||||
|
# Clean up feature branch
|
||||||
|
git branch -d feature/your-feature-name
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Handling Upstream Updates
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Fetch latest upstream changes
|
||||||
|
git fetch origin
|
||||||
|
|
||||||
|
# Check what changed
|
||||||
|
git log --oneline origin/dev ^dev
|
||||||
|
|
||||||
|
# Option 1: Rebase (clean history)
|
||||||
|
git rebase origin/dev
|
||||||
|
|
||||||
|
# Option 2: Merge (preserve history)
|
||||||
|
git merge origin/dev
|
||||||
|
|
||||||
|
# Resolve any conflicts and push
|
||||||
|
git push origin dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📋 Commit Message Convention
|
||||||
|
|
||||||
|
Follow conventional commits for consistency:
|
||||||
|
|
||||||
|
```
|
||||||
|
type(scope): description
|
||||||
|
|
||||||
|
[optional body]
|
||||||
|
|
||||||
|
[optional footer]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Types
|
||||||
|
- **`feat`**: New features
|
||||||
|
- **`fix`**: Bug fixes
|
||||||
|
- **`docs`**: Documentation
|
||||||
|
- **`style`**: Code style changes
|
||||||
|
- **`refactor`**: Code refactoring
|
||||||
|
- **`test`**: Testing
|
||||||
|
- **`chore`**: Maintenance
|
||||||
|
|
||||||
|
### Examples
|
||||||
|
```
|
||||||
|
feat: Add custom todo+codebase compaction hook
|
||||||
|
fix: Resolve compaction threshold calculation bug
|
||||||
|
docs: Update git strategy documentation
|
||||||
|
refactor: Simplify hook injection logic
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔄 Update Strategy for Upstream Changes
|
||||||
|
|
||||||
|
### Scenario: Upstream Releases New Features
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Backup current state
|
||||||
|
git branch backup-$(date +%Y%m%d)
|
||||||
|
|
||||||
|
# Option A: Rebase (Recommended)
|
||||||
|
git fetch origin
|
||||||
|
git rebase origin/dev
|
||||||
|
# Resolve conflicts if any
|
||||||
|
git rebase --continue
|
||||||
|
|
||||||
|
# Option B: Merge
|
||||||
|
git fetch origin
|
||||||
|
git merge origin/dev
|
||||||
|
|
||||||
|
# Test that custom features still work
|
||||||
|
bun install && bun run build && bun run typecheck
|
||||||
|
|
||||||
|
# Push updated dev
|
||||||
|
git push origin dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### Conflict Resolution Guidelines
|
||||||
|
|
||||||
|
1. **Prioritize Custom Features**: Keep your custom logic when conflicts occur
|
||||||
|
2. **Test Thoroughly**: Ensure custom compaction still works after merges
|
||||||
|
3. **Document Changes**: Update this doc if workflow changes
|
||||||
|
4. **Backup First**: Always create backups before major operations
|
||||||
|
|
||||||
|
## 🛡️ Quality Assurance
|
||||||
|
|
||||||
|
### Pre-Merge Checklist
|
||||||
|
|
||||||
|
- [ ] **Build passes**: `bun run build`
|
||||||
|
- [ ] **Types check**: `bun run typecheck`
|
||||||
|
- [ ] **Tests pass**: `bun run test` (if applicable)
|
||||||
|
- [ ] **Custom features work**: Test compaction, agents, etc.
|
||||||
|
- [ ] **No console errors**: Clean build output
|
||||||
|
- [ ] **Documentation updated**: README, this doc, etc.
|
||||||
|
|
||||||
|
### Post-Merge Verification
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Full verification script
|
||||||
|
bun install
|
||||||
|
bun run build
|
||||||
|
bun run typecheck
|
||||||
|
|
||||||
|
# Test custom features
|
||||||
|
ls src/hooks/todo-codebase-compaction/
|
||||||
|
grep "custom_compaction" src/config/schema.ts
|
||||||
|
|
||||||
|
echo "✅ All checks passed!"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🤝 Collaboration Guidelines
|
||||||
|
|
||||||
|
### For Team Members
|
||||||
|
|
||||||
|
1. **Never commit directly to `dev`** - Always use feature branches
|
||||||
|
2. **Keep feature branches focused** - One feature per branch
|
||||||
|
3. **Regular rebasing** - Keep up with upstream changes
|
||||||
|
4. **Clear commit messages** - Follow conventional commits
|
||||||
|
5. **Test before merging** - Ensure no regressions
|
||||||
|
|
||||||
|
### Code Review Process
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Before merging, get review
|
||||||
|
gh pr create --base dev --head feature/your-feature
|
||||||
|
|
||||||
|
# Or manual review
|
||||||
|
git diff dev..feature/your-feature
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🏷️ Tagging Strategy
|
||||||
|
|
||||||
|
### Version Tags (Future)
|
||||||
|
```
|
||||||
|
v1.0.0 # First stable release
|
||||||
|
v1.1.0 # Feature release
|
||||||
|
v1.1.1 # Bug fix release
|
||||||
|
```
|
||||||
|
|
||||||
|
### Feature Tags
|
||||||
|
```
|
||||||
|
feature/compaction-v1 # Major feature milestone
|
||||||
|
experiment/alternative-prompts # Experimental features
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚨 Emergency Procedures
|
||||||
|
|
||||||
|
### Lost Commits
|
||||||
|
```bash
|
||||||
|
# Find lost commits
|
||||||
|
git reflog
|
||||||
|
|
||||||
|
# Restore from reflog
|
||||||
|
git checkout <commit-hash>
|
||||||
|
git branch recovery-branch
|
||||||
|
```
|
||||||
|
|
||||||
|
### Upstream Conflicts
|
||||||
|
```bash
|
||||||
|
# Abort and try different strategy
|
||||||
|
git rebase --abort
|
||||||
|
git merge origin/dev # Try merge instead
|
||||||
|
|
||||||
|
# Or create fresh feature branch
|
||||||
|
git checkout -b feature/fresh-start origin/dev
|
||||||
|
# Re-implement changes
|
||||||
|
```
|
||||||
|
|
||||||
|
### Repository Corruption
|
||||||
|
```bash
|
||||||
|
# Fresh clone and reapply changes
|
||||||
|
cd ..
|
||||||
|
rm -rf oh-my-opencode-free-fork
|
||||||
|
git clone <your-gitea-url> oh-my-opencode-free-fork
|
||||||
|
cd oh-my-opencode-free-fork
|
||||||
|
# Reapply custom changes from backups
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📊 Metrics and Monitoring
|
||||||
|
|
||||||
|
### Branch Health
|
||||||
|
- **Max feature branch age**: 2 weeks
|
||||||
|
- **Max dev branch divergence**: 5 commits
|
||||||
|
- **Required reviews**: 1 reviewer for merges
|
||||||
|
|
||||||
|
### Quality Metrics
|
||||||
|
- **Build success rate**: >95%
|
||||||
|
- **Test coverage**: Track and improve
|
||||||
|
- **Merge conflict rate**: <10%
|
||||||
|
|
||||||
|
## 🎯 Best Practices
|
||||||
|
|
||||||
|
### General
|
||||||
|
- **Small, focused commits** - Easier to review and revert
|
||||||
|
- **Regular pushes** - Don't accumulate large changes
|
||||||
|
- **Clear naming** - Descriptive branch and commit names
|
||||||
|
- **Documentation** - Keep this and README updated
|
||||||
|
|
||||||
|
### Custom Features
|
||||||
|
- **Isolate custom code** - Keep it separate from upstream changes
|
||||||
|
- **Test compatibility** - Ensure custom features work with upstream updates
|
||||||
|
- **Document overrides** - Note where you deviate from upstream
|
||||||
|
- **Plan for conflicts** - Have strategies for resolving upstream conflicts
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
- **Fast-forward merges** - Prefer rebase for clean history
|
||||||
|
- **Shallow clones** - For CI/CD if needed
|
||||||
|
- **Branch cleanup** - Delete merged branches regularly
|
||||||
|
|
||||||
|
## 📚 Resources
|
||||||
|
|
||||||
|
- [Git Flow](https://nvie.com/posts/a-successful-git-branching-model/)
|
||||||
|
- [Conventional Commits](https://conventionalcommits.org/)
|
||||||
|
- [Git Rebase vs Merge](https://www.atlassian.com/git/tutorials/merging-vs-rebasing)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 Change Log
|
||||||
|
|
||||||
|
- **2026-01-08**: Initial git strategy documentation
|
||||||
|
- **2026-01-08**: Added custom compaction workflow
|
||||||
|
- **2026-01-08**: Established feature branch workflow
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*This strategy ensures clean, maintainable development while preserving your custom oh-my-opencode features.*
|
||||||
@@ -77,7 +77,9 @@
|
|||||||
"edit-error-recovery",
|
"edit-error-recovery",
|
||||||
"prometheus-md-only",
|
"prometheus-md-only",
|
||||||
"start-work",
|
"start-work",
|
||||||
"sisyphus-orchestrator"
|
"sisyphus-orchestrator",
|
||||||
|
"todo-codebase-compaction",
|
||||||
|
"usage-logging"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
94
docker/Dockerfile.leader
Normal file
94
docker/Dockerfile.leader
Normal 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
34
docker/docker-compose.yml
Normal 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
|
||||||
104
docker/shared-config/oh-my-opencode.json
Normal file
104
docker/shared-config/oh-my-opencode.json
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
52
docker/shared-config/opencode.json
Normal file
52
docker/shared-config/opencode.json
Normal 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"] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
118
docker/shared-config/opencode.jsonc
Normal file
118
docker/shared-config/opencode.jsonc
Normal 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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1
docker/shared-config/test-persistence.txt
Normal file
1
docker/shared-config/test-persistence.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
TEST: This change should persist rebuilds
|
||||||
79
reload.sh
Executable file
79
reload.sh
Executable 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."
|
||||||
@@ -82,6 +82,8 @@ export const HookNameSchema = z.enum([
|
|||||||
"prometheus-md-only",
|
"prometheus-md-only",
|
||||||
"start-work",
|
"start-work",
|
||||||
"sisyphus-orchestrator",
|
"sisyphus-orchestrator",
|
||||||
|
"todo-codebase-compaction",
|
||||||
|
"usage-logging",
|
||||||
])
|
])
|
||||||
|
|
||||||
export const BuiltinCommandNameSchema = z.enum([
|
export const BuiltinCommandNameSchema = z.enum([
|
||||||
|
|||||||
@@ -29,3 +29,5 @@ export { createPrometheusMdOnlyHook } from "./prometheus-md-only";
|
|||||||
export { createTaskResumeInfoHook } from "./task-resume-info";
|
export { createTaskResumeInfoHook } from "./task-resume-info";
|
||||||
export { createStartWorkHook } from "./start-work";
|
export { createStartWorkHook } from "./start-work";
|
||||||
export { createSisyphusOrchestratorHook } from "./sisyphus-orchestrator";
|
export { createSisyphusOrchestratorHook } from "./sisyphus-orchestrator";
|
||||||
|
export { createTodoCodebaseCompactionInjector, createCustomCompactionHook } from "./todo-codebase-compaction";
|
||||||
|
export { createUsageLoggingHook } from "./usage-logging";
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { SummarizeContext } from "../preemptive-compaction"
|
import type { SummarizeContext, PreemptiveCompactionOptions } from "../preemptive-compaction"
|
||||||
import { injectHookMessage } from "../../features/hook-message-injector"
|
import { injectHookMessage } from "../../features/hook-message-injector"
|
||||||
import { log } from "../../shared/logger"
|
import { log } from "../../shared/logger"
|
||||||
import { createPreemptiveCompactionHook } from "../preemptive-compaction"
|
import { createPreemptiveCompactionHook } from "../preemptive-compaction"
|
||||||
|
|||||||
229
src/hooks/usage-logging/index.ts
Normal file
229
src/hooks/usage-logging/index.ts
Normal file
@@ -0,0 +1,229 @@
|
|||||||
|
interface LogEvent {
|
||||||
|
timestamp: string;
|
||||||
|
stack_name: string;
|
||||||
|
session_id: string;
|
||||||
|
event_type: string;
|
||||||
|
data: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UsageLoggingOptions {
|
||||||
|
ingestUrl?: string;
|
||||||
|
stackName?: string;
|
||||||
|
batchSize?: number;
|
||||||
|
flushIntervalMs?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 getWordCount(content: string): number {
|
||||||
|
return content.split(/\s+/).filter(Boolean).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createUsageLoggingHook(options: UsageLoggingOptions = {}) {
|
||||||
|
const {
|
||||||
|
ingestUrl = DEFAULT_INGEST_URL,
|
||||||
|
stackName = DEFAULT_STACK_NAME,
|
||||||
|
batchSize = 10,
|
||||||
|
flushIntervalMs = 5000,
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
const enabled = process.env.USAGE_LOGGING_ENABLED !== 'false';
|
||||||
|
if (!enabled) {
|
||||||
|
return { event: async () => {} };
|
||||||
|
}
|
||||||
|
|
||||||
|
const eventBuffer: LogEvent[] = [];
|
||||||
|
let flushTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
const sessionStats = new Map<string, {
|
||||||
|
startTime: number;
|
||||||
|
messageCount: number;
|
||||||
|
toolUseCount: number;
|
||||||
|
tokensIn: number;
|
||||||
|
tokensOut: number;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
async function flushEvents(): Promise<void> {
|
||||||
|
if (eventBuffer.length === 0) return;
|
||||||
|
|
||||||
|
const eventsToSend = [...eventBuffer];
|
||||||
|
eventBuffer.length = 0;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${ingestUrl}/batch`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(eventsToSend),
|
||||||
|
signal: AbortSignal.timeout(5000)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
console.error(`[UsageLogging] Failed to send events: ${response.status}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[UsageLogging] Error sending events:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function queueEvent(sessionId: string, eventType: string, data: Record<string, unknown>): void {
|
||||||
|
eventBuffer.push({
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
stack_name: stackName,
|
||||||
|
session_id: sessionId,
|
||||||
|
event_type: eventType,
|
||||||
|
data
|
||||||
|
});
|
||||||
|
|
||||||
|
if (eventBuffer.length >= batchSize) {
|
||||||
|
flushEvents();
|
||||||
|
} else if (!flushTimer) {
|
||||||
|
flushTimer = setTimeout(() => {
|
||||||
|
flushTimer = null;
|
||||||
|
flushEvents();
|
||||||
|
}, flushIntervalMs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getOrCreateSessionStats(sessionId: string) {
|
||||||
|
if (!sessionStats.has(sessionId)) {
|
||||||
|
sessionStats.set(sessionId, {
|
||||||
|
startTime: Date.now(),
|
||||||
|
messageCount: 0,
|
||||||
|
toolUseCount: 0,
|
||||||
|
tokensIn: 0,
|
||||||
|
tokensOut: 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return sessionStats.get(sessionId)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
event: async (input: { event: { type: string; properties?: Record<string, unknown> } }) => {
|
||||||
|
const { event } = input;
|
||||||
|
const props = event.properties || {};
|
||||||
|
|
||||||
|
try {
|
||||||
|
switch (event.type) {
|
||||||
|
case 'session.created': {
|
||||||
|
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(),
|
||||||
|
model: info.model,
|
||||||
|
agent: info.agent
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'session.deleted': {
|
||||||
|
const info = props.info as { id?: string } | undefined;
|
||||||
|
if (info?.id) {
|
||||||
|
const stats = sessionStats.get(info.id);
|
||||||
|
if (stats) {
|
||||||
|
queueEvent(info.id, 'session_end', {
|
||||||
|
duration_ms: Date.now() - stats.startTime,
|
||||||
|
total_messages: stats.messageCount,
|
||||||
|
total_tool_uses: stats.toolUseCount,
|
||||||
|
total_tokens_in: stats.tokensIn,
|
||||||
|
total_tokens_out: stats.tokensOut
|
||||||
|
});
|
||||||
|
sessionStats.delete(info.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'message.created': {
|
||||||
|
const sessionID = props.sessionID as string | undefined;
|
||||||
|
const message = props.message as { role?: string; content?: unknown } | undefined;
|
||||||
|
if (sessionID && message) {
|
||||||
|
const stats = getOrCreateSessionStats(sessionID);
|
||||||
|
stats.messageCount++;
|
||||||
|
|
||||||
|
const content = typeof message.content === 'string'
|
||||||
|
? message.content
|
||||||
|
: JSON.stringify(message.content || '');
|
||||||
|
|
||||||
|
queueEvent(sessionID, 'message', {
|
||||||
|
role: message.role || 'unknown',
|
||||||
|
content: content,
|
||||||
|
content_length: content.length,
|
||||||
|
word_count: getWordCount(content),
|
||||||
|
message_number: stats.messageCount
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'tool.started': {
|
||||||
|
const sessionID = props.sessionID as 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()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'tool.completed': {
|
||||||
|
const sessionID = props.sessionID as string | 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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'session.error': {
|
||||||
|
const sessionID = props.sessionID as string | undefined;
|
||||||
|
const error = props.error as { message?: string; code?: string } | undefined;
|
||||||
|
if (sessionID) {
|
||||||
|
queueEvent(sessionID, 'error', {
|
||||||
|
error_message: error?.message || 'Unknown error',
|
||||||
|
error_code: error?.code
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'tokens.used': {
|
||||||
|
const sessionID = props.sessionID as string | 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,
|
||||||
|
total_tokens_out: stats.tokensOut
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[UsageLogging] Error processing event:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
14
src/index.ts
14
src/index.ts
@@ -31,6 +31,7 @@ import {
|
|||||||
createStartWorkHook,
|
createStartWorkHook,
|
||||||
createSisyphusOrchestratorHook,
|
createSisyphusOrchestratorHook,
|
||||||
createPrometheusMdOnlyHook,
|
createPrometheusMdOnlyHook,
|
||||||
|
createUsageLoggingHook,
|
||||||
} from "./hooks";
|
} from "./hooks";
|
||||||
import {
|
import {
|
||||||
contextCollector,
|
contextCollector,
|
||||||
@@ -146,10 +147,13 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
|
|||||||
const compactionContextInjector = isHookEnabled("compaction-context-injector")
|
const compactionContextInjector = isHookEnabled("compaction-context-injector")
|
||||||
? createCompactionContextInjector()
|
? createCompactionContextInjector()
|
||||||
: undefined;
|
: undefined;
|
||||||
|
const todoCodebaseCompactionInjector = isHookEnabled("todo-codebase-compaction")
|
||||||
|
? createTodoCodebaseCompactionInjector()
|
||||||
|
: undefined;
|
||||||
const preemptiveCompaction = isHookEnabled("preemptive-compaction")
|
const preemptiveCompaction = isHookEnabled("preemptive-compaction")
|
||||||
? createPreemptiveCompactionHook(ctx, {
|
? createPreemptiveCompactionHook(ctx, {
|
||||||
experimental: pluginConfig.experimental,
|
experimental: pluginConfig.experimental,
|
||||||
onBeforeSummarize: compactionContextInjector,
|
onBeforeSummarize: todoCodebaseCompactionInjector ?? compactionContextInjector,
|
||||||
getModelLimit: (providerID, modelID) =>
|
getModelLimit: (providerID, modelID) =>
|
||||||
getModelLimit(modelCacheState, providerID, modelID),
|
getModelLimit(modelCacheState, providerID, modelID),
|
||||||
})
|
})
|
||||||
@@ -209,6 +213,13 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
|
|||||||
? createPrometheusMdOnlyHook(ctx)
|
? createPrometheusMdOnlyHook(ctx)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
const usageLogging = isHookEnabled("usage-logging")
|
||||||
|
? createUsageLoggingHook({
|
||||||
|
stackName: process.env.STACK_NAME,
|
||||||
|
ingestUrl: process.env.LOG_INGEST_URL,
|
||||||
|
})
|
||||||
|
: null;
|
||||||
|
|
||||||
const taskResumeInfo = createTaskResumeInfoHook();
|
const taskResumeInfo = createTaskResumeInfoHook();
|
||||||
|
|
||||||
const backgroundManager = new BackgroundManager(ctx);
|
const backgroundManager = new BackgroundManager(ctx);
|
||||||
@@ -408,6 +419,7 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
|
|||||||
await interactiveBashSession?.event(input);
|
await interactiveBashSession?.event(input);
|
||||||
await ralphLoop?.event(input);
|
await ralphLoop?.event(input);
|
||||||
await sisyphusOrchestrator?.handler(input);
|
await sisyphusOrchestrator?.handler(input);
|
||||||
|
await usageLogging?.event(input);
|
||||||
|
|
||||||
const { event } = input;
|
const { event } = input;
|
||||||
const props = event.properties as Record<string, unknown> | undefined;
|
const props = event.properties as Record<string, unknown> | undefined;
|
||||||
|
|||||||
46
start.sh
Executable file
46
start.sh
Executable 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
547
tui.sh
Executable 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
|
||||||
Reference in New Issue
Block a user