v1.2.0: opencode JSON mode, 180s Hermes timeout, client AbortController, SEO README, badge/CSS fixes, prompt clipboard fix

This commit is contained in:
modimihir07 2026-05-17 23:43:18 +05:30
parent 828a6ab38a
commit 2b3083308f
14 changed files with 489 additions and 186 deletions

4
.gitignore vendored
View File

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

View File

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

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

View File

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

View File

@ -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'),
};

View File

@ -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'); }
}
}

View File

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

View File

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

View File

@ -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');

View File

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

View File

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

View File

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

View File

@ -1,6 +0,0 @@
{
"entries": [],
"daily_totals": {},
"monthly_projection": 0,
"free_tier_alerts": []
}

154
server.py
View File

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