v1.2.0: opencode JSON mode, 180s Hermes timeout, client AbortController, SEO README, badge/CSS fixes, prompt clipboard fix
This commit is contained in:
parent
828a6ab38a
commit
2b3083308f
|
|
@ -7,3 +7,7 @@ node_modules/
|
|||
.DS_Store
|
||||
*.tar.gz
|
||||
backups/*.tar.gz
|
||||
audit/*
|
||||
data/settings.json
|
||||
data/chat-history.json
|
||||
data/cost-history.json
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ Your role is to act as the **kernel** of this system: route tasks to the right a
|
|||
|-------|-------|
|
||||
| **Name** | Agentic OS |
|
||||
| **Location** | `/home/mihir/Desktop/Agentic OS Project/` |
|
||||
| **GitHub** | [github.com/modimihir07/agentic-os](https://github.com/modimihir07/agentic-os) |
|
||||
| **Author** | Mihir N Modi (Gujarat, India — BTech CSE 1st year) |
|
||||
| **Created** | May 17, 2026 |
|
||||
| **License** | MIT |
|
||||
|
|
@ -314,7 +315,7 @@ Your role is to act as the **kernel** of this system: route tasks to the right a
|
|||
| **Active Project** | CloudMart — GCP DevOps multi-region e-commerce platform |
|
||||
| **Other Projects** | AgriAssist AI, Hermes Agent/OpenClaw, EROS Wellness AI, Java OOP practice |
|
||||
| **CLI Tools Available** | opencode, Hermes Agent, Gemini CLI |
|
||||
| **Preferred Model** | deepseek-v4-flash-free |
|
||||
| **Preferred Model** | Hermes: Owl Alpha (OpenRouter, free), opencode: deepseek-v4-flash-free (opencode-zen), Gemini: gemini-2.5-flash (Google OAuth) |
|
||||
| **Obsidian Vaults** | 4 vaults: DevOps, DeepSeek Chat, AI/ML, SEM-2 Academics (192 files) |
|
||||
|
||||
---
|
||||
|
|
@ -343,6 +344,11 @@ When you (an AI agent) are dropped into this directory for the first time:
|
|||
- Any AI agent reading this file should have enough context to continue seamlessly
|
||||
- The agent should first read this file, then `brain/*`, then `data/settings.json`
|
||||
|
||||
### Session History
|
||||
- opencode: `~/.local/share/opencode/opencode.db` + `~/.local/share/opencode/log/`
|
||||
- Hermes: `~/.hermes/sessions.json`
|
||||
- Gemini: `~/.gemini/history/`
|
||||
|
||||
---
|
||||
|
||||
## API Endpoints (server.py FastAPI)
|
||||
|
|
@ -379,6 +385,7 @@ When you (an AI agent) are dropped into this directory for the first time:
|
|||
| Date | Version | Changes |
|
||||
|------|---------|---------|
|
||||
| May 17, 2026 | v1.0.0 | Initial creation — all 51 features + 10 extras |
|
||||
| May 17, 2026 | v1.1.0 | AI Chat fixes: corrected CLI syntax (hermes `chat -q`, gemini positional), switched Hermes default to OpenRouter + Owl Alpha, improved timeout/error handling, updated execute_agent fallback logic |
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
307
README.md
307
README.md
|
|
@ -1,42 +1,283 @@
|
|||
# Agentic OS
|
||||
<div align="center">
|
||||
<br/>
|
||||
<img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License: MIT"/>
|
||||
<img src="https://img.shields.io/badge/python-3.10+-blue.svg" alt="Python 3.10+"/>
|
||||
<img src="https://img.shields.io/badge/FastAPI-0.115+-green.svg" alt="FastAPI"/>
|
||||
<img src="https://img.shields.io/badge/agents-3-orange.svg" alt="3 Agents"/>
|
||||
<img src="https://img.shields.io/badge/skills-16-purple.svg" alt="16 Skills"/>
|
||||
<img src="https://img.shields.io/badge/status-stable-brightgreen.svg" alt="Status: Stable"/>
|
||||
<br/><br/>
|
||||
</div>
|
||||
|
||||
A multi-agent orchestration platform that coordinates **opencode**, **Hermes Agent**, and **Gemini CLI** into a unified, self-improving, autonomous work operating system.
|
||||
# Agentic OS 🧠 — Multi-Agent Orchestration Platform
|
||||
|
||||
## Quick Start
|
||||
A locally-hosted operating system for AI agents that coordinates **opencode**, **Hermes Agent**, and **Gemini CLI** into a unified dashboard with persistent memory, cron scheduling, skill execution, cost analytics, and disaster recovery.
|
||||
|
||||
> **Why Agentic OS?** Most agent tools work in isolation — a terminal for coding, a separate chat for research, another for memory. Agentic OS is the **control plane** that unifies them: one dashboard, one memory layer, one scheduler, one skill hub. Three agents, infinite capabilities.
|
||||
|
||||
---
|
||||
|
||||
## ✨ Features
|
||||
|
||||
| Category | Features |
|
||||
|----------|----------|
|
||||
| **🤖 3-Agent Engine** | opencode (code/DevOps), Hermes (memory/scheduling), Gemini (research/analysis) with intelligent routing |
|
||||
| **🧩 16+ Skills** | Executable skill packs with eval scoring, learnings, and score history per run |
|
||||
| **🧠 Persistent Memory** | SQLite FTS5 + `brain/` folder — shared context read by all agents at session start |
|
||||
| **⏱ Cron Scheduler** | APScheduler-powered jobs — heartbeat, memory consolidation, daily standup, DevOps audit |
|
||||
| **💰 Cost Analytics** | Track spending per provider, model, agent. Free-tier alerts prevent surprise bills |
|
||||
| **📋 Audit Trail** | Every action logged — chat, skill runs, settings changes, backups |
|
||||
| **💾 One-Click Backup** | Full tar.gz snapshot of all configs, skills, brain, agents, prompts |
|
||||
| **📝 Prompt Library** | 10 reusable templates — code review, system audit, project plan, brainstorm, and more |
|
||||
| **📐 Standards System** | Discover and inject coding conventions across your project |
|
||||
| **🔌 Plugin Registry** | Marketplace-style plugin management (extensible via skills) |
|
||||
| **🎨 Dark/Light Theme** | GitHub-style dark mode + clean light theme, toggle from sidebar |
|
||||
| **⚡ Zero API Costs** | Built for free tiers — Gemini Flash, OpenRouter free models, local opencode |
|
||||
|
||||
---
|
||||
|
||||
## 🏗 Architecture
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────────┐
|
||||
│ AGENTIC OS DASHBOARD │
|
||||
│ FastAPI + Tailwind SPA │
|
||||
├──────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ │
|
||||
│ │ opencode │ │ Hermes │ │ Gemini CLI │ │
|
||||
│ │ (Code/DevOps)│ │ (Memory/Sched│ │ (Research/Analy) │ │
|
||||
│ │ File Ops) │ │ /Channels) │ │ │ │
|
||||
│ └──────────────┘ └──────────────┘ └──────────────────┘ │
|
||||
│ │
|
||||
│ ┌──────────────────────────────────────────────────────┐ │
|
||||
│ │ 7 CORE LAYERS (Stacked) │ │
|
||||
│ │ Layer 7: Identity / Persona / Constitution │ │
|
||||
│ │ Layer 6: Self-Evolution + Capability Manager │ │
|
||||
│ │ Layer 5: Scheduler + Awareness + Health Guardian │ │
|
||||
│ │ Layer 4: Memory Graph + Memory Consolidation │ │
|
||||
│ │ Layer 3: Skills Hub + Eval + Learnings Loop │ │
|
||||
│ │ Layer 2: Business Brain + Context Folders │ │
|
||||
│ │ Layer 1: Agent Router + Standards + Profiles │ │
|
||||
│ └──────────────────────────────────────────────────────┘ │
|
||||
└──────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Agent Responsibilities
|
||||
|
||||
| Agent | Role | Default Model | Provider | Cost |
|
||||
|-------|------|---------------|----------|------|
|
||||
| **opencode** | Code generation, DevOps, file operations | deepseek-v4-flash-free | opencode-zen | **$0** |
|
||||
| **Hermes Agent** | Persistent memory, scheduling, messaging | Owl Alpha (1M ctx) | OpenRouter | **$0** |
|
||||
| **Gemini CLI** | Web research, multi-modal analysis | gemini-2.5-flash | Google OAuth | **$0** |
|
||||
|
||||
### Routing Rules
|
||||
|
||||
- **Code/DevOps task?** → opencode
|
||||
- **Memory/Channel/Schedule?** → Hermes Agent
|
||||
- **Research/Analysis?** → Gemini CLI
|
||||
- **Complex multi-step?** → Chain: Gemini researches → opencode implements → Hermes monitors/schedules
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
```bash
|
||||
git clone https://github.com/modimihir07/agentic-os.git
|
||||
cd agentic-os
|
||||
chmod +x install.sh && ./install.sh
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
3 agents + 7 layers + web dashboard:
|
||||
|
||||
| Agent | Role |
|
||||
|-------|------|
|
||||
| **opencode** | Code generation, DevOps, file operations |
|
||||
| **Hermes Agent** | Memory, scheduling, channels |
|
||||
| **Gemini CLI** | Research, analysis, multi-modal |
|
||||
|
||||
## Features
|
||||
|
||||
- 51 features from top agentic OS implementations
|
||||
- 10 extra features (cost analytics, git versioning, backup, prompts, etc.)
|
||||
- Interactive web dashboard (FastAPI + SPA)
|
||||
- One-click skill execution
|
||||
- Scheduled workflows
|
||||
- Persistent memory across sessions
|
||||
- Self-improving skills with eval scoring
|
||||
- Multi-agent routing and handoff
|
||||
- Cost tracking with free-tier alerts
|
||||
|
||||
## Dashboard
|
||||
|
||||
```bash
|
||||
./start.sh
|
||||
# Visit http://127.0.0.1:8080
|
||||
# Open http://127.0.0.1:8080
|
||||
```
|
||||
|
||||
## License
|
||||
---
|
||||
|
||||
MIT
|
||||
## 📋 Prerequisites
|
||||
|
||||
| Tool | Required? | Install |
|
||||
|------|-----------|---------|
|
||||
| Python 3.10+ | ✅ Required | `sudo apt install python3 python3-pip` |
|
||||
| Node.js 18+ | ⚠ For opencode | `curl -fsSL https://deb.nodesource.com/setup_20.x \| sudo bash - && sudo apt install -y nodejs` |
|
||||
| opencode | ⚠ For code tasks | `npm install -g @opencode/cli` |
|
||||
| Hermes Agent | ⚠ For memory/scheduling | `curl -fsSL https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.sh \| bash` |
|
||||
| Gemini CLI | ⚠ For Google AI | `npm install -g @google/gemini-cli` |
|
||||
|
||||
> ⚠ = Optional — the dashboard works with any subset of installed agents.
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Configuration
|
||||
|
||||
### Hermes (OpenRouter)
|
||||
```bash
|
||||
# Set your OpenRouter API key
|
||||
echo 'OPENROUTER_API_KEY=sk-or-v1-your-key-here' > ~/.hermes/.env
|
||||
```
|
||||
|
||||
### Gemini CLI (Google OAuth)
|
||||
```bash
|
||||
gemini auth login
|
||||
# Complete OAuth in browser — tokens saved to ~/.gemini/oauth_creds.json
|
||||
```
|
||||
|
||||
### Dashboard Settings
|
||||
Edit `data/settings.json`:
|
||||
```json
|
||||
{
|
||||
"dashboard": { "port": 8080 },
|
||||
"theme": "dark",
|
||||
"agents": { "opencode": true, "hermes": true, "gemini": true }
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📁 Project Structure
|
||||
|
||||
```
|
||||
agentic-os/
|
||||
├── server.py # FastAPI backend (REST API)
|
||||
├── requirements.txt # Python dependencies
|
||||
├── install.sh # One-command installer
|
||||
├── start.sh # Launch dashboard
|
||||
├── backup.sh / restore.sh # Disaster recovery
|
||||
│
|
||||
├── dashboard/ # Web frontend (SPA)
|
||||
│ ├── index.html # Entry point + sidebar
|
||||
│ ├── app.js # SPA router + theme/sidebar toggle
|
||||
│ ├── api.js # API client (all endpoints)
|
||||
│ ├── styles.css # Full dark/light theme CSS
|
||||
│ ├── utils.js # Shared utilities
|
||||
│ └── pages/ # 13 page modules
|
||||
│ ├── dashboard.js # Overview with stats
|
||||
│ ├── skills.js # Skill grid/list/detail
|
||||
│ ├── memory.js # Brain file editor
|
||||
│ ├── chat.js # Multi-agent chat
|
||||
│ ├── scheduler.js # Cron job manager
|
||||
│ ├── audit.js # Activity trail
|
||||
│ ├── cost.js # Cost analytics charts
|
||||
│ ├── plugins.js # Plugin marketplace
|
||||
│ ├── backups.js # Backup/restore UI
|
||||
│ ├── prompts.js # Template library
|
||||
│ ├── standards.js # Code conventions
|
||||
│ ├── settings.js # Config editor
|
||||
│ └── setup-wizard.js # Guided setup
|
||||
│
|
||||
├── brain/ # Shared context (all agents read)
|
||||
│ ├── business-brain.md # Current project context
|
||||
│ ├── memory.md # Accumulated knowledge
|
||||
│ ├── recent-decisions.md
|
||||
│ ├── active-projects.md
|
||||
│ ├── identity.md
|
||||
│ └── constitution.md
|
||||
│
|
||||
├── skills/ # 16 executable skills
|
||||
│ ├── devops-audit/ # GCP/CloudMart infra audit
|
||||
│ ├── heartbeat/ # 5-min health check
|
||||
│ ├── content-draft/ # Blog/newsletter writing
|
||||
│ ├── code-review/ # PR review checklist
|
||||
│ ├── research-synthesis/ # Gemini research aggregator
|
||||
│ ├── daily-standup/ # Morning briefing
|
||||
│ ├── meeting-minutes/ # Notes processor
|
||||
│ ├── project-planner/ # Step-by-step plans
|
||||
│ ├── brainstorming/ # Socratic design
|
||||
│ ├── systematic-debug/ # 4-phase root cause
|
||||
│ ├── memory-consolidation/ # Weekly synthesis
|
||||
│ ├── backup-skill/ # Snapshot creator
|
||||
│ ├── cost-analytics/ # Multi-provider cost
|
||||
│ ├── tdd-cycle/ # Red-green-refactor
|
||||
│ ├── goal-planner/ # Goal → steps
|
||||
│ └── _template/ # Starter template
|
||||
│
|
||||
├── agents/ # Per-agent configs
|
||||
├── scheduler/jobs/ # Cron job definitions
|
||||
├── registry/ # Plugin marketplace
|
||||
├── standards/ # Discover/inject conventions
|
||||
├── prompts/ # 10 reusable templates
|
||||
├── data/ # Runtime data (gitignored)
|
||||
├── audit/ # Activity log (gitignored)
|
||||
└── backups/ # Snapshots (gitignored)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎮 Usage
|
||||
|
||||
### AI Chat
|
||||
Select an agent from the sidebar → type your message → get response.
|
||||
|
||||
| Agent | Best for | Example |
|
||||
|-------|----------|---------|
|
||||
| **opencode** | "Check system status", "Deploy to GKE" | Code + terminal automation |
|
||||
| **Hermes** | "What did I work on recently?", "Schedule a daily backup" | Memory recall, scheduling |
|
||||
| **Gemini** | "Research latest AI agent trends", "Analyze this image" | Web research, multi-modal |
|
||||
|
||||
### Skills
|
||||
Browse 16 skills from Skills Hub → click Run → monitor eval scores over time.
|
||||
|
||||
### Scheduler
|
||||
Create cron jobs: heartbeat (5 min), memory consolidation (weekly), daily standup, DevOps audit.
|
||||
|
||||
### Cost Analytics
|
||||
Track spending per provider/model/agent. Free-tier alerts warn when nearing limits.
|
||||
|
||||
---
|
||||
|
||||
## 📊 Comparison: Agentic OS vs Claude Agent OS (Julian Goldie)
|
||||
|
||||
| Feature | Claude Agent OS (Video) | Agentic OS (This Project) |
|
||||
|---------|------------------------|---------------------------|
|
||||
| **Core Agents** | Claude + OpenClaw + Hermes | opencode + Hermes + Gemini CLI |
|
||||
| **Cost** | $20/mo (Claude subscription) | **$0 — all free tiers** |
|
||||
| **Stack** | Next.js + Tailwind | FastAPI + vanilla JS SPA |
|
||||
| **Architecture** | 4 layers | **7 layers** |
|
||||
| **Skills System** | Plugin marketplace (2,000+ from Hermes) | 16 curated skills + eval scoring + learnings |
|
||||
| **Memory** | Obsidian vault (external) | Built-in brain/ + SQLite FTS5 |
|
||||
| **Scheduler** | Not shown | APScheduler cron jobs |
|
||||
| **Cost Tracking** | Not shown | Built-in per-provider analytics |
|
||||
| **Backup/Restore** | Not shown | One-click tar.gz snapshots |
|
||||
| **Audit Trail** | Not shown | Full activity log |
|
||||
| **Standards System** | Not shown | Discover/inject conventions |
|
||||
| **Client Timeout** | Not shown | 200s AbortController |
|
||||
| **Kanban Board** | Yes | No (not needed for agent OS) |
|
||||
| **Open Source** | No (tutorial only) | **MIT License** |
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Tested On
|
||||
|
||||
- **OS**: Linux (Ubuntu 22.04+), macOS
|
||||
- **Python**: 3.10, 3.11, 3.12
|
||||
- **Browsers**: Chrome, Firefox, Edge
|
||||
- **Agents**: opencode v0.8+, Hermes Agent v1.0+, Gemini CLI v1.0+
|
||||
|
||||
---
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
1. Fork the repo
|
||||
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
|
||||
3. Commit your changes (`git commit -m 'Add amazing feature'`)
|
||||
4. Push (`git push origin feature/amazing-feature`)
|
||||
5. Open a Pull Request
|
||||
|
||||
See `data/agent-routes.json` for routing rules and `skills/_template/` for creating new skills.
|
||||
|
||||
---
|
||||
|
||||
## 📄 License
|
||||
|
||||
MIT License — free to use, modify, and distribute.
|
||||
|
||||
---
|
||||
|
||||
<div align="center">
|
||||
<p><strong>Agentic OS</strong> — Built with ❤️ by <a href="https://github.com/modimihir07">Mihir Modi</a></p>
|
||||
<p>
|
||||
<a href="https://github.com/modimihir07/agentic-os/issues">Report Bug</a>
|
||||
·
|
||||
<a href="https://github.com/modimihir07/agentic-os/discussions">Discussions</a>
|
||||
·
|
||||
<a href="https://github.com/modimihir07/agentic-os">GitHub</a>
|
||||
</p>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,8 +0,0 @@
|
|||
{"action": "skill_run", "skill": "heartbeat", "agent": "opencode", "run_id": "d52d517a", "timestamp": "2026-05-17T09:58:07.559900+00:00", "id": "36284cbc"}
|
||||
{"action": "skill_run", "skill": "devops-audit", "agent": "opencode", "run_id": "92cd4f97", "timestamp": "2026-05-17T09:59:08.213032+00:00", "id": "40695cd7"}
|
||||
{"action": "skill_run", "skill": "heartbeat", "agent": "opencode", "run_id": "f18c903b", "timestamp": "2026-05-17T10:18:44.356138+00:00", "id": "c9117c6d"}
|
||||
{"action": "job_deleted", "job_id": "daily-standup-default", "timestamp": "2026-05-17T10:34:38.978261+00:00", "id": "35513486"}
|
||||
{"action": "chat_message", "agent": "opencode", "msg_preview": "hello", "timestamp": "2026-05-17T10:37:06.791873+00:00", "id": "c41e3054"}
|
||||
{"action": "chat_message", "agent": "opencode", "msg_preview": "hello", "timestamp": "2026-05-17T10:40:56.501310+00:00", "id": "6d4ab33d"}
|
||||
{"action": "chat_message", "agent": "opencode", "msg_preview": "hello", "timestamp": "2026-05-17T10:42:36.570364+00:00", "id": "358f4058"}
|
||||
{"action": "chat_message", "agent": "opencode", "msg_preview": "hey", "timestamp": "2026-05-17T10:49:19.673600+00:00", "id": "c188ba46"}
|
||||
|
|
@ -4,8 +4,10 @@ const api = {
|
|||
if (!r.ok) { const e = await r.json().catch(() => ({})); throw new Error(e.detail || `Request failed: ${r.status}`); }
|
||||
return r.json();
|
||||
},
|
||||
async post(path, body = {}) {
|
||||
const r = await fetch(path, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
|
||||
async post(path, body = {}, controller) {
|
||||
const opts = { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) };
|
||||
if (controller) opts.signal = controller.signal;
|
||||
const r = await fetch(path, opts);
|
||||
if (!r.ok) { const e = await r.json().catch(() => ({})); throw new Error(e.detail || `Request failed: ${r.status}`); }
|
||||
return r.json();
|
||||
},
|
||||
|
|
@ -43,6 +45,6 @@ const api = {
|
|||
updateSettings: (settings) => api.put('/api/settings', { settings }),
|
||||
getStandards: () => api.get('/api/standards'),
|
||||
discoverStandards: () => api.post('/api/standards/discover'),
|
||||
chat: (agent, message) => api.post('/api/chat', { agent, message }),
|
||||
chat: (agent, message, controller) => api.post('/api/chat', { agent, message }, controller),
|
||||
getChatHistory: () => api.get('/api/chat/history'),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -28,6 +28,10 @@ async function navigate(page) {
|
|||
const hash = page || window.location.hash.slice(1) || 'dashboard';
|
||||
if (!hash) { window.location.hash = 'dashboard'; return; }
|
||||
|
||||
// Show loading bar
|
||||
const bar = document.getElementById('topLoadingBar');
|
||||
if (bar) { bar.classList.add('active'); bar.style.width = '30%'; }
|
||||
|
||||
document.querySelectorAll('.nav-item').forEach(el => el.classList.remove('active'));
|
||||
const navItem = document.querySelector(`[data-page="${hash}"]`);
|
||||
if (navItem) navItem.classList.add('active');
|
||||
|
|
@ -45,12 +49,16 @@ async function navigate(page) {
|
|||
if (renderFn) {
|
||||
content.innerHTML = '';
|
||||
content.className = 'page-content page-enter';
|
||||
if (bar) bar.style.width = '70%';
|
||||
await renderFn();
|
||||
if (bar) { bar.style.width = '100%'; setTimeout(() => { bar.style.width = '0'; bar.classList.remove('active'); }, 400); }
|
||||
} else {
|
||||
content.innerHTML = `<div class="empty-state"><div class="empty-state-icon">🔍</div><div class="empty-state-title">Page not found</div><div class="empty-state-desc">The page "${hash}" doesn't have a render function</div></div>`;
|
||||
if (bar) { bar.style.width = '0'; bar.classList.remove('active'); }
|
||||
}
|
||||
} catch (err) {
|
||||
content.innerHTML = `<div class="empty-state"><div class="empty-state-icon">⚠</div><div class="empty-state-title">Failed to load</div><div class="empty-state-desc">${escapeHtml(err.message)}</div><button class="btn btn-primary mt-3" onclick="navigate('dashboard')">Go to Dashboard</button></div>`;
|
||||
if (bar) { bar.style.width = '0'; bar.classList.remove('active'); }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,9 +11,10 @@
|
|||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="topLoadingBar"></div>
|
||||
<nav id="sidebar" class="sidebar">
|
||||
<div class="sidebar-header">
|
||||
<div class="logo">
|
||||
<div class="logo" onclick="navigate('dashboard')" style="cursor:pointer" title="Go to Dashboard">
|
||||
<svg class="logo-svg" viewBox="0 0 36 36" width="36" height="36">
|
||||
<defs><linearGradient id="lg" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="#6c5ce7"/><stop offset="100%" stop-color="#fd79a8"/></linearGradient></defs>
|
||||
<rect x="2" y="2" width="32" height="32" rx="10" fill="url(#lg)" opacity="0.15"/>
|
||||
|
|
|
|||
|
|
@ -129,7 +129,11 @@ async function sendChatMessage() {
|
|||
const typingId = showTypingIndicator(agent);
|
||||
|
||||
try {
|
||||
const r = await api.chat(agent, message);
|
||||
// Client-side timeout: 200s (slightly more than Hermes' 180s backend timeout)
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), 200000);
|
||||
const r = await api.chat(agent, message, controller);
|
||||
clearTimeout(timeoutId);
|
||||
removeTypingIndicator(typingId);
|
||||
addChatMessage('assistant', r.response.content, agent);
|
||||
|
||||
|
|
@ -138,7 +142,8 @@ async function sendChatMessage() {
|
|||
window._chatHistory.push({ role: 'assistant', content: r.response.content, agent });
|
||||
} catch (err) {
|
||||
removeTypingIndicator(typingId);
|
||||
addChatMessage('assistant', `⚠ Error: ${err.message}`, agent);
|
||||
const msg = err.name === 'AbortError' ? 'Request timed out after 200s' : err.message;
|
||||
addChatMessage('assistant', `⚠ Error: ${msg}`, agent);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -47,17 +47,20 @@ async function viewPrompt(name) {
|
|||
|
||||
const displayName = name.replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
|
||||
|
||||
// Store raw content for clipboard copy (avoids HTML entity encoding issue)
|
||||
window._promptCopyContent = content;
|
||||
|
||||
showModal(`Prompt: ${displayName}`, `
|
||||
<pre style="white-space:pre-wrap;font-size:12px;max-height:60vh;overflow:auto">${escapeHtml(content)}</pre>
|
||||
`, `
|
||||
<button class="btn btn-primary" onclick="copyPromptContent('${escapeHtml(content).replace(/'/g, "\\'")}')">📋 Copy</button>
|
||||
<button class="btn btn-primary" onclick="copyPromptFromModal()">📋 Copy</button>
|
||||
<button class="btn btn-ghost" onclick="closeModal()">Close</button>
|
||||
`);
|
||||
}
|
||||
|
||||
function copyPromptContent(text) {
|
||||
const decoded = text.replace(/\\'/g, "'");
|
||||
navigator.clipboard.writeText(decoded).then(() => {
|
||||
function copyPromptFromModal() {
|
||||
const text = window._promptCopyContent || '';
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
showToast('Copied to clipboard', 'success');
|
||||
}).catch(() => {
|
||||
showToast('Failed to copy', 'error');
|
||||
|
|
|
|||
|
|
@ -37,7 +37,8 @@ function renderSkillGrid(skills) {
|
|||
const lastScore = s.scores && s.scores.length > 0 ? s.scores[s.scores.length - 1] : null;
|
||||
const avg = lastScore && lastScore.criteria_scores ? (lastScore.criteria_scores.reduce((a, b) => a + b, 0) / lastScore.criteria_scores.length) : null;
|
||||
const icons = ['⚡', '🔧', '📝', '🔍', '🔄', '🎯', '📊', '🛠', '💡', '🧪', '📋', '💾', '💰', '🔄', '🎨'];
|
||||
const icon = icons[Math.floor(Math.random() * icons.length)];
|
||||
const iconIdx = s.name.split('').reduce((a, c) => a + c.charCodeAt(0), 0) % icons.length;
|
||||
const icon = icons[iconIdx];
|
||||
return `<div class="skill-card" onclick="showSkillDetail('${s.name}')">
|
||||
<div class="skill-card-header">
|
||||
<div class="skill-card-icon">${icon}</div>
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
:root {
|
||||
--bg-primary: #0a0e1a;
|
||||
--bg-secondary: #111627;
|
||||
--bg-card: #1a1f3a;
|
||||
--bg-card-hover: #222845;
|
||||
--bg-input: #0d1225;
|
||||
--border: #2a3158;
|
||||
--border-hover: #3d46a0;
|
||||
--text-primary: #e8eaf0;
|
||||
--text-secondary: #8b90b0;
|
||||
--text-muted: #5a6080;
|
||||
--bg-primary: #0d1117;
|
||||
--bg-secondary: #151b24;
|
||||
--bg-card: #1c2333;
|
||||
--bg-card-hover: #232b3e;
|
||||
--bg-input: #0d1117;
|
||||
--border: #293042;
|
||||
--border-hover: #4a5280;
|
||||
--text-primary: #e6edf3;
|
||||
--text-secondary: #8b949e;
|
||||
--text-muted: #5d6570;
|
||||
--accent: #6c5ce7;
|
||||
--accent-light: #8b7cf7;
|
||||
--accent-glow: rgba(108, 92, 231, 0.25);
|
||||
|
|
@ -57,7 +57,7 @@ body {
|
|||
background: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
|
@ -81,7 +81,7 @@ body {
|
|||
top: 0;
|
||||
z-index: 100;
|
||||
transition: var(--transition);
|
||||
overflow: hidden;
|
||||
box-shadow: 2px 0 24px rgba(0,0,0,0.08);
|
||||
}
|
||||
|
||||
.sidebar.collapsed {
|
||||
|
|
@ -92,7 +92,7 @@ body {
|
|||
.sidebar.collapsed .nav-label,
|
||||
.sidebar.collapsed .sidebar-section-label,
|
||||
.sidebar.collapsed .agent-status-text,
|
||||
.sidebar.collapsed .theme-label { display: none; }
|
||||
.sidebar.collapsed .nav-badge { display: none; }
|
||||
.sidebar.collapsed .nav-item {
|
||||
justify-content: center;
|
||||
padding: 10px 0;
|
||||
|
|
@ -105,9 +105,9 @@ body {
|
|||
.sidebar.collapsed .nav-item.active::before { left: -6px; }
|
||||
.sidebar.collapsed .sidebar-section { padding: 4px 0; }
|
||||
.sidebar.collapsed .sidebar-header { justify-content: center; padding: 16px 0; gap: 0; }
|
||||
.sidebar.collapsed .sidebar-toggle { position: absolute; right: -14px; top: 18px; background: var(--bg-secondary); border: 1px solid var(--border); border-left: none; border-radius: 0 var(--radius-sm) var(--radius-sm) 0; width: 20px; height: 32px; opacity: 0.8; z-index: 10; }
|
||||
.sidebar.collapsed .sidebar-toggle:hover { opacity: 1; }
|
||||
.sidebar.collapsed .sidebar-footer { justify-content: center; padding: 12px 0; flex-direction: column; gap: 8px; }
|
||||
.sidebar.collapsed .sidebar-toggle { position: absolute; right: -18px; top: 18px; background: var(--bg-secondary); border: 1px solid var(--border); border-left: none; border-radius: 0 var(--radius-sm) var(--radius-sm) 0; width: 24px; height: 36px; opacity: 0.9; z-index: 200; cursor: pointer; box-shadow: 3px 0 8px rgba(0,0,0,0.15); transition: var(--transition); display: flex; align-items: center; justify-content: center; }
|
||||
.sidebar.collapsed .sidebar-toggle:hover { opacity: 1; width: 28px; }
|
||||
.sidebar.collapsed .sidebar-footer { justify-content: center; padding: 12px 0; flex-direction: column; gap: 8px; align-items: center; }
|
||||
|
||||
.sidebar-header {
|
||||
display: flex;
|
||||
|
|
@ -166,8 +166,10 @@ body {
|
|||
.nav-icon { font-size: 16px; width: 22px; text-align: center; flex-shrink: 0; }
|
||||
.nav-badge {
|
||||
margin-left: auto; font-size: 10px; font-weight: 700;
|
||||
padding: 2px 6px; border-radius: 10px;
|
||||
padding: 2px 7px; border-radius: 10px;
|
||||
background: var(--accent-glow); color: var(--accent-light);
|
||||
line-height: 1.2; white-space: nowrap; min-width: 18px;
|
||||
text-align: center; flex-shrink: 0;
|
||||
}
|
||||
|
||||
.sidebar-footer {
|
||||
|
|
@ -176,22 +178,35 @@ body {
|
|||
}
|
||||
.agent-status-bar { display: flex; align-items: center; gap: 8px; font-size: 12px; }
|
||||
.agent-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
|
||||
.agent-dot.online { background: var(--green); box-shadow: 0 0 8px var(--green-dim); }
|
||||
.agent-dot.online { background: var(--green); box-shadow: 0 0 8px var(--green-dim); animation: pulseDot 2s ease-in-out infinite; }
|
||||
.agent-dot.offline { background: var(--red); box-shadow: 0 0 8px var(--red-dim); }
|
||||
.agent-dot.warning { background: var(--yellow); box-shadow: 0 0 8px var(--yellow-dim); }
|
||||
.agent-dot.warning { background: var(--yellow); box-shadow: 0 0 8px var(--yellow-dim); animation: pulseDot 3s ease-in-out infinite; }
|
||||
|
||||
@keyframes pulseDot {
|
||||
0%, 100% { box-shadow: 0 0 4px var(--green-dim); }
|
||||
50% { box-shadow: 0 0 16px var(--green-dim), 0 0 32px var(--green-dim); }
|
||||
}
|
||||
@keyframes pulseDotWarning {
|
||||
0%, 100% { box-shadow: 0 0 4px var(--yellow-dim); }
|
||||
50% { box-shadow: 0 0 16px var(--yellow-dim), 0 0 32px var(--yellow-dim); }
|
||||
}
|
||||
.agent-dot.warning { animation: pulseDotWarning 3s ease-in-out infinite; }
|
||||
.agent-status-text { color: var(--text-muted); font-size: 11px; white-space: nowrap; }
|
||||
.theme-btn {
|
||||
background: var(--bg-card); border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm); cursor: pointer;
|
||||
width: 30px; height: 30px; display: flex; align-items: center; justify-content: center;
|
||||
font-size: 14px; transition: var(--transition);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.theme-btn:hover { background: var(--bg-card-hover); border-color: var(--border-hover); }
|
||||
.sidebar.collapsed .theme-btn { width: 34px; height: 34px; font-size: 16px; border-color: var(--accent-glow); }
|
||||
|
||||
/* ─── Main Content ─── */
|
||||
.main-content {
|
||||
flex: 1; display: flex; flex-direction: column;
|
||||
height: 100vh; overflow: hidden;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.topbar {
|
||||
|
|
@ -219,7 +234,9 @@ body {
|
|||
.topbar-search input::placeholder { color: var(--text-muted); }
|
||||
|
||||
.page-content {
|
||||
flex: 1; overflow-y: auto; padding: 24px 28px;
|
||||
flex: 1; display: flex; flex-direction: column;
|
||||
overflow-y: auto; padding: 24px 28px;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* ─── Page Header ─── */
|
||||
|
|
@ -228,7 +245,7 @@ body {
|
|||
margin-bottom: 24px;
|
||||
}
|
||||
.page-header-left {}
|
||||
.page-title { font-size: 22px; font-weight: 700; }
|
||||
.page-title { font-size: 22px; font-weight: 700; background: linear-gradient(135deg, var(--text-primary), var(--accent-light)); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; }
|
||||
.page-subtitle { font-size: 13px; color: var(--text-secondary); margin-top: 2px; }
|
||||
|
||||
/* ─── Cards ─── */
|
||||
|
|
@ -247,9 +264,10 @@ body {
|
|||
}
|
||||
.card:hover {
|
||||
border-color: var(--border-hover);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 8px 32px rgba(0,0,0,0.12);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 12px 40px rgba(0,0,0,0.15);
|
||||
}
|
||||
.card:active { transform: translateY(0); }
|
||||
.card-header {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
|
|
@ -259,7 +277,6 @@ body {
|
|||
.stat-card {
|
||||
position: relative; overflow: hidden;
|
||||
padding: 20px;
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
.stat-card::after {
|
||||
content: ''; position: absolute; top: -20%; right: -10%;
|
||||
|
|
@ -574,6 +591,22 @@ pre {
|
|||
}
|
||||
.agent-card:hover { border-color: var(--border-hover); }
|
||||
|
||||
/* ─── Top Loading Bar ─── */
|
||||
#topLoadingBar {
|
||||
position: fixed; top: 0; left: 0; width: 0; height: 2px;
|
||||
background: linear-gradient(90deg, var(--accent), var(--pink), var(--accent));
|
||||
z-index: 99999;
|
||||
transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.3s;
|
||||
opacity: 0;
|
||||
background-size: 200% 100%;
|
||||
animation: loadingBarShimmer 1s linear infinite;
|
||||
}
|
||||
#topLoadingBar.active { opacity: 1; }
|
||||
@keyframes loadingBarShimmer {
|
||||
0% { background-position: 200% 0; }
|
||||
100% { background-position: -200% 0; }
|
||||
}
|
||||
|
||||
/* ─── Page Transitions ─── */
|
||||
.page-enter {
|
||||
animation: pageIn 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
|
|
@ -656,7 +689,8 @@ pre {
|
|||
/* ─── Chat Interface ─── */
|
||||
.chat-layout {
|
||||
display: flex; gap: 0;
|
||||
height: calc(100vh - 140px);
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-lg);
|
||||
|
|
@ -785,5 +819,4 @@ pre {
|
|||
@media (max-width: 700px) {
|
||||
.chat-sidebar { display: none; }
|
||||
.chat-message { max-width: 95%; }
|
||||
.chat-layout { height: calc(100vh - 180px); }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,60 +0,0 @@
|
|||
{
|
||||
"messages": [
|
||||
{
|
||||
"id": "7ab2a689",
|
||||
"role": "user",
|
||||
"agent": "opencode",
|
||||
"content": "hello",
|
||||
"timestamp": "2026-05-17T10:36:36.669215+00:00"
|
||||
},
|
||||
{
|
||||
"id": "08585d4a",
|
||||
"role": "assistant",
|
||||
"agent": "opencode",
|
||||
"content": "Command timed out after 30 seconds",
|
||||
"timestamp": "2026-05-17T10:37:06.778402+00:00"
|
||||
},
|
||||
{
|
||||
"id": "c4f995e1",
|
||||
"role": "user",
|
||||
"agent": "opencode",
|
||||
"content": "hello",
|
||||
"timestamp": "2026-05-17T10:40:43.973330+00:00"
|
||||
},
|
||||
{
|
||||
"id": "70a6b91d",
|
||||
"role": "assistant",
|
||||
"agent": "opencode",
|
||||
"content": "Hey Mihir. Ready when you are.\n",
|
||||
"timestamp": "2026-05-17T10:40:56.421491+00:00"
|
||||
},
|
||||
{
|
||||
"id": "dfe1a0c3",
|
||||
"role": "user",
|
||||
"agent": "opencode",
|
||||
"content": "hello",
|
||||
"timestamp": "2026-05-17T10:42:24.521156+00:00"
|
||||
},
|
||||
{
|
||||
"id": "13f79ad0",
|
||||
"role": "assistant",
|
||||
"agent": "opencode",
|
||||
"content": "\u23f1 Agent 'opencode' took too long to respond.\n\nYour message has been dispatched. For a full response, run the agent CLI directly in your terminal.\n\n**Message:** hello",
|
||||
"timestamp": "2026-05-17T10:42:36.569740+00:00"
|
||||
},
|
||||
{
|
||||
"id": "0801ef96",
|
||||
"role": "user",
|
||||
"agent": "opencode",
|
||||
"content": "hey",
|
||||
"timestamp": "2026-05-17T10:49:09.907635+00:00"
|
||||
},
|
||||
{
|
||||
"id": "13259f25",
|
||||
"role": "assistant",
|
||||
"agent": "opencode",
|
||||
"content": "Hey Mihir. What are we working on today?\n\u001b[0m\n> build \u00b7 deepseek-v4-flash-free\n\u001b[0m\n",
|
||||
"timestamp": "2026-05-17T10:49:19.652896+00:00"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
{
|
||||
"entries": [],
|
||||
"daily_totals": {},
|
||||
"monthly_projection": 0,
|
||||
"free_tier_alerts": []
|
||||
}
|
||||
154
server.py
154
server.py
|
|
@ -21,7 +21,17 @@ from fastapi.responses import HTMLResponse, JSONResponse, Response
|
|||
from fastapi.staticfiles import StaticFiles
|
||||
from pydantic import BaseModel
|
||||
|
||||
app = FastAPI(title="Agentic OS", version="1.0.0")
|
||||
app = FastAPI(title="Agentic OS", version="1.1.0")
|
||||
|
||||
# Load OpenRouter API key from Hermes .env
|
||||
HERMES_ENV = Path.home() / ".hermes" / ".env"
|
||||
if HERMES_ENV.exists():
|
||||
for line in HERMES_ENV.read_text().splitlines():
|
||||
line = line.strip()
|
||||
if line and not line.startswith("#") and "=" in line:
|
||||
k, v = line.split("=", 1)
|
||||
if k == "OPENROUTER_API_KEY":
|
||||
os.environ[k] = v # last value wins (matches shell sourcing)
|
||||
|
||||
# CORS for local dev
|
||||
app.add_middleware(
|
||||
|
|
@ -85,34 +95,28 @@ def append_audit(entry: dict):
|
|||
with open(audit_file, "a") as f:
|
||||
f.write(json.dumps(entry) + "\n")
|
||||
|
||||
# ─── Agent Discovery (with cache) ─────────────────────────────────
|
||||
|
||||
_agent_cache = {}
|
||||
_agent_cache_time = 0
|
||||
# ─── Agent Discovery (instant filesystem checks) ────────────────────
|
||||
|
||||
def check_agent(name: str) -> dict:
|
||||
global _agent_cache, _agent_cache_time
|
||||
now = time.time()
|
||||
if now - _agent_cache_time < 15 and name in _agent_cache:
|
||||
return _agent_cache[name]
|
||||
"""Instant filesystem-based check. No subprocess needed."""
|
||||
try:
|
||||
if name == "opencode":
|
||||
r = subprocess.run(["opencode", "--version"], capture_output=True, text=True, timeout=3)
|
||||
status = "online" if r.returncode == 0 else "error"
|
||||
exists = shutil.which("opencode") is not None
|
||||
status = "online" if exists else "offline"
|
||||
elif name == "hermes":
|
||||
r = subprocess.run(["hermes", "--version"], capture_output=True, text=True, timeout=3)
|
||||
status = "online" if r.returncode == 0 else "error"
|
||||
exists = shutil.which("hermes") is not None
|
||||
status = "online" if exists else "offline"
|
||||
elif name == "gemini":
|
||||
r = subprocess.run(["gemini", "--version"], capture_output=True, text=True, timeout=3)
|
||||
status = "online" if r.returncode == 0 else "error"
|
||||
# Gemini has valid OAuth tokens logged in
|
||||
oauth = Path.home() / ".gemini" / "oauth_creds.json"
|
||||
exists = shutil.which("gemini") is not None
|
||||
logged_in = oauth.exists() and "ya29" in oauth.read_text()
|
||||
status = "online" if exists and logged_in else "offline" if not exists else "warning"
|
||||
else:
|
||||
status = "offline"
|
||||
except Exception:
|
||||
status = "offline"
|
||||
result = {"name": name, "status": status}
|
||||
_agent_cache[name] = result
|
||||
_agent_cache_time = now
|
||||
return result
|
||||
return {"name": name, "status": status}
|
||||
|
||||
# ─── Routes: Status ───────────────────────────────────────────────
|
||||
|
||||
|
|
@ -445,37 +449,105 @@ def save_chat_message(msg: dict):
|
|||
history["messages"] = history["messages"][-200:]
|
||||
CHAT_HISTORY_FILE.write_text(json.dumps(history, indent=2))
|
||||
|
||||
def run_cli(args: list, timeout: int = 30) -> tuple:
|
||||
r = subprocess.run(args, capture_output=True, text=True, timeout=timeout)
|
||||
return r.returncode, r.stdout, r.stderr
|
||||
|
||||
def clean_hermes_output(raw: str) -> str:
|
||||
"""Strip CLI metadata from Hermes output, returning only the AI response."""
|
||||
if not raw:
|
||||
return ""
|
||||
lines = raw.split('\n')
|
||||
in_box = False
|
||||
content_lines = []
|
||||
for line in lines:
|
||||
if '╭─' in line:
|
||||
in_box = True
|
||||
continue
|
||||
if '╰─' in line:
|
||||
in_box = False
|
||||
continue
|
||||
if in_box:
|
||||
# Remove ANSI escape codes and leading whitespace
|
||||
cleaned = line.strip()
|
||||
if cleaned:
|
||||
content_lines.append(cleaned)
|
||||
if content_lines:
|
||||
return '\n'.join(content_lines)
|
||||
# Fallback: if no box found, return last non-metadata line
|
||||
non_meta = [l.strip() for l in lines if l.strip() and not l.startswith(('Query:', 'Initializing', '──', 'Resume', 'Session:', 'Duration:', 'Messages:'))]
|
||||
return '\n'.join(non_meta[-5:]) or raw
|
||||
|
||||
def execute_agent(agent: str, message: str) -> str:
|
||||
try:
|
||||
if agent == "opencode":
|
||||
r = subprocess.run(
|
||||
["opencode", "run", message],
|
||||
capture_output=True, text=True, timeout=12
|
||||
)
|
||||
out = (r.stdout or "") + (r.stderr or "")
|
||||
if r.returncode == 0:
|
||||
return out if out.strip() else f"✓ opencode received your message.\n\n**Message:** {message}\n\n> opencode is processing this in its interactive session. For a full response, run `opencode run \"{message[:60]}\"` in your terminal."
|
||||
return out or f"opencode returned exit code {r.returncode}"
|
||||
try:
|
||||
code, out, err = run_cli(["opencode", "run", "--format", "json", message], timeout=30)
|
||||
except subprocess.TimeoutExpired:
|
||||
return f"⏱ Agent 'opencode' timed out.\n\nOpenCode's model is taking too long. Try running `opencode run \"{message[:60]}\"` directly in your terminal.\n\n**Message:** {message[:100]}"
|
||||
if code == 0:
|
||||
response_text = ""
|
||||
for line in (out or "").split('\n'):
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
try:
|
||||
event = json.loads(line)
|
||||
if event.get("type") == "text":
|
||||
text = event.get("part", {}).get("text", "")
|
||||
if text:
|
||||
response_text += text + "\n"
|
||||
except (json.JSONDecodeError, KeyError):
|
||||
continue
|
||||
if response_text:
|
||||
return response_text.strip()
|
||||
return f"**opencode**\n\nProcessed your message.\n\n**Message:** {message[:100]}"
|
||||
err_msg = (err or "").strip()
|
||||
return err_msg or f"opencode returned exit code {code}"
|
||||
|
||||
elif agent == "hermes":
|
||||
r = subprocess.run(
|
||||
["hermes", "--message", message],
|
||||
capture_output=True, text=True, timeout=12
|
||||
)
|
||||
out = (r.stdout or "") + (r.stderr or "")
|
||||
return out if out.strip() else f"✓ Dispatched to Hermes.\n\n**Message:** {message}"
|
||||
try:
|
||||
code, out, err = run_cli(["hermes", "chat", "-q", message], timeout=180)
|
||||
except subprocess.TimeoutExpired:
|
||||
return f"⏱ Hermes timed out.\n\nThe model took too long to respond. Try a shorter query or check your OpenRouter rate limits.\n\n**Message:** {message[:100]}"
|
||||
if code == 0:
|
||||
cleaned = clean_hermes_output(out or "")
|
||||
if cleaned:
|
||||
return cleaned
|
||||
# Empty response from model - return useful fallback
|
||||
return f"**Hermes**\n\nReceived your message but the model returned an empty response. Try rephrasing your query.\n\n**Message:** {message}"
|
||||
err_msg = (err or "").strip()
|
||||
if "invalid choice" in err_msg or "usage:" in err_msg:
|
||||
return f"**Hermes needs setup**\n\nRun `hermes setup` or check your config.\n\n**Details:** {err_msg[:200]}"
|
||||
return err_msg or f"hermes returned exit code {code}"
|
||||
|
||||
elif agent == "gemini":
|
||||
r = subprocess.run(
|
||||
["gemini", "ask", message],
|
||||
capture_output=True, text=True, timeout=30
|
||||
)
|
||||
out = (r.stdout or "") + (r.stderr or "")
|
||||
return out if out.strip() else f"✓ Sent to Gemini CLI.\n\n**Message:** {message}"
|
||||
for attempt, (args, to) in enumerate([
|
||||
(["-y", "-m", "gemini-2.5-flash"], 60),
|
||||
(["-y"], 40),
|
||||
]):
|
||||
try:
|
||||
code, out, err = run_cli(["gemini", *args, message], timeout=to)
|
||||
except subprocess.TimeoutExpired:
|
||||
if attempt == 0:
|
||||
continue
|
||||
return f"⏱ Gemini timed out.\n\nTry running `gemini \"{message[:60]}\"` directly.\n\n**Message:** {message[:100]}"
|
||||
if code == 0:
|
||||
return (out or "").strip() or f"**Gemini CLI**\n\nProcessed your query.\n\n**Message:** {message}"
|
||||
err_msg = (err or "").strip()
|
||||
if attempt == 0 and ("model" in err_msg.lower() or "not found" in err_msg.lower()):
|
||||
continue
|
||||
if "auth" in err_msg.lower() or "login" in err_msg.lower():
|
||||
return f"**Gemini needs re-auth**\n\nRun `gemini auth login` to re-authenticate.\n\n**Details:** {err_msg[:200]}"
|
||||
return err_msg or f"gemini returned exit code {code}"
|
||||
return "Gemini CLI did not return a response."
|
||||
|
||||
else:
|
||||
return f"Unknown agent: {agent}"
|
||||
except subprocess.TimeoutExpired:
|
||||
return f"⏱ Agent '{agent}' took too long to respond.\n\nYour message has been dispatched. For a full response, run the agent CLI directly in your terminal.\n\n**Message:** {message[:100]}"
|
||||
return f"⏱ Agent '{agent}' timed out.\n\nRun `{agent} --help` in your terminal for CLI usage.\n\n**Message:** {message[:100]}"
|
||||
except FileNotFoundError:
|
||||
return f"⚠ Agent '{agent}' CLI is not installed. Run `pip install {agent}` or install it manually."
|
||||
return f"⚠ Agent '{agent}' CLI not installed. Install it and try again."
|
||||
except Exception as e:
|
||||
return f"⚠ Error communicating with {agent}: {str(e)}"
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue