Wire up dynamic agent registry, integrations, agent-time tracking, UI modernization
- server.py: dynamic agent execution engine (cli/http/mcp), live agent registry (/api/agents register/unregister), integrations API, agent stats & skill scoring, kanban task delete, plugin uninstall, agent-time tracking + /test page - dashboard: modernized agent-health, plugins marketplace, smart-router, styles; added agent-time monitor page + terminal page - scheduler.py rewrite; start.sh / start-agentic-os.sh improvements - skills: added firebase-*, notion-knowledge-capture, xcode-project-setup, audit/test plugins; updated learnings across skills - brain/ docs + data registry files (agent-registry, integrations, router-keywords) - .gitignore: exclude runtime artifacts (pid, graphify-out, logs) Verified live on :8081 - all new endpoints return 200 and register/unregister persists to data/agent-registry.json.
This commit is contained in:
parent
701df3e58e
commit
96d8281b9b
|
|
@ -15,3 +15,8 @@ audit/*
|
|||
data/settings.json
|
||||
data/chat-history.json
|
||||
data/cost-history.json
|
||||
.agentic-os.pid
|
||||
graphify-out/
|
||||
brain/graphify-out/
|
||||
skills/*/graphify-out/
|
||||
*.log
|
||||
|
|
|
|||
|
|
@ -317,7 +317,7 @@ Your role is to act as the **kernel** of this system: route tasks to the right a
|
|||
| Detail | Info |
|
||||
|--------|------|
|
||||
| **Budget** | Strictly free tiers (GCP Free, GitHub Student Pack, Colab, Kaggle) |
|
||||
| **Active Project** | CloudMart — GCP DevOps multi-region e-commerce platform |
|
||||
|| **Active Projects** | Kitchen-Inventory (catering kitchen management on antigravity), Pendleton-comms-live (walkie talkie for catering events on Railway) |
|
||||
| **CLI Tools Available** | opencode, Hermes Agent, Gemini CLI |
|
||||
| **Preferred Model** | Hermes: Owl Alpha (OpenRouter, free), opencode: deepseek-v4-flash-free (opencode-zen), Gemini: gemini-2.5-flash (Google OAuth) |
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,72 @@
|
|||
# Changelog
|
||||
|
||||
All notable changes to Agentic OS will be documented in this file.
|
||||
|
||||
## [v1.3.0] - 2026-06-28
|
||||
|
||||
### Added
|
||||
- **Web Terminal** — Full bash shell in browser via xterm.js + WebSocket PTY (port 8082)
|
||||
- 256-color support, terminal resizing, 10k line scrollback
|
||||
- Reconnect and clear buttons
|
||||
- Dark theme matching dashboard
|
||||
- **Dynamic Agent Registry** — Register/remove custom agents via dashboard or API
|
||||
- `GET /api/agents` — list all agents
|
||||
- `POST /api/agents/register` — add custom agent (CLI, HTTP, or MCP type)
|
||||
- `DELETE /api/agents/:name` — remove custom agent
|
||||
- Agent config stored in `data/agent-registry.json`
|
||||
- Custom agents auto-appear in status, health, router
|
||||
- **Agent Health Dashboard** — Complete rewrite
|
||||
- Card grid view with status indicators
|
||||
- Registry table with type/source/actions
|
||||
- "Add Agent" modal with form (name, binary, run args, health check, router keywords)
|
||||
- Remove button for custom agents
|
||||
- **Smart Router Extensibility** — Custom agents can register routing keywords via `data/router-keywords.json`
|
||||
|
||||
### Changed
|
||||
- **Hermes Model** — Switched from `openrouter/owl-alpha` to free model chain:
|
||||
1. `qwen/qwen3-next-80b-a3b-instruct:free` (primary)
|
||||
2. `openai/gpt-oss-120b:free` (fallback 1)
|
||||
3. `google/gemma-4-31b-it:free` (fallback 2)
|
||||
4. `nvidia/nemotron-3-super-120b-a12b:free` (fallback 3)
|
||||
- **Agent Discovery** — All endpoints now use dynamic registry instead of hardcoded `["opencode", "hermes", "gemini"]`
|
||||
- `/api/status`, `/api/agents/health`, `/api/chat`, `/api/router/route`, `/api/agents/:name/stats`
|
||||
- **Base URL** — Hermes now points to `https://openrouter.ai/api/v1` directly
|
||||
|
||||
### Fixed
|
||||
- Audit log malformed timestamp entry removed
|
||||
- Test artifacts cleaned from kanban, goals, cost history, brain
|
||||
|
||||
## [v1.2.0] - 2026-06-26
|
||||
|
||||
### Added
|
||||
- Agent profiles (`agents/{opencode,hermes,gemini}/`)
|
||||
- APScheduler integration (`scheduler/scheduler.py`)
|
||||
- Plugin install from git repo or template (`POST /api/plugins/install`)
|
||||
- Connected apps / integrations system
|
||||
|
||||
## [v1.1.0] - 2026-06-05
|
||||
|
||||
### Added
|
||||
- Kanban board (6-column, 13 API endpoints)
|
||||
- Goals system with auto-sync to `brain/active-projects.md`
|
||||
- Journal with daily entries and full-text search
|
||||
- Agent health monitoring (3 endpoints)
|
||||
- Smart Router with keyword-based task routing
|
||||
- Learning Analytics (skill eval scores, trends)
|
||||
- Session Replay (browse opencode logs)
|
||||
- Cost Analytics (token usage tracking)
|
||||
- Backup/restore system (tar.gz snapshots)
|
||||
|
||||
## [v1.0.0] - 2026-05-17
|
||||
|
||||
### Added
|
||||
- Initial release
|
||||
- FastAPI backend (58+ endpoints)
|
||||
- Vanilla JS SPA dashboard (21 pages)
|
||||
- 3 built-in agents: OpenCode, Hermes, Gemini CLI
|
||||
- 16 skills with eval scoring
|
||||
- Brain/memory system (markdown files)
|
||||
- Scheduler (APScheduler + Hermes cron)
|
||||
- Plugin registry
|
||||
- Standards system
|
||||
- Prompt templates
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
# Operational Notes
|
||||
|
||||
## Server Debugging
|
||||
- Logs: `tail -f /tmp/agentic-os.log`
|
||||
- If browser shows blank dashboard, check for a single failing API endpoint crashing the SPA
|
||||
- Malformed JSON in `audit.log` can crash `/api/audit` — the server skips bad lines but check if it's persistent
|
||||
|
||||
## WSL → Windows Browser
|
||||
- Server-side 500 errors can look like connection failures from Windows browser
|
||||
- WSL2 loopback and Windows loopback are separate interfaces — use `127.0.0.1` from WSL, not `localhost` from Windows
|
||||
|
||||
## Hermes Config
|
||||
- Config: `~/.hermes/config.yaml`
|
||||
- API keys: `~/.hermes/.env`
|
||||
- Current model: `qwen/qwen3-next-80b-a3b-instruct:free` (OpenRouter)
|
||||
- Fallbacks: gpt-oss-120b:free → gemma-4-31b-it:free → nemotron-3-super-120b:free
|
||||
- Change model: `hermes config set model.default <model-id>`
|
||||
|
||||
## Agentic OS Docs
|
||||
- Full docs: `docs/README.md`
|
||||
- Changelog: `CHANGELOG.md`
|
||||
- After API changes: `python3 scripts/update-api-docs.py`
|
||||
- Skill: `software-development/agentic-os-docs`
|
||||
|
||||
## Brain Guardian Cron
|
||||
- Job ID: `68221afb4d45`
|
||||
- Runs every 2h via Hermes cron
|
||||
- Outputs: brain/health-report.md, brain/skill-usage.md, brain/github-activity.md
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
## User
|
||||
- Name: User
|
||||
- Tech Stack: opencode, Hermes Agent, Gemini CLI, deepseek-v4-flash-free
|
||||
- Projects: CloudMart (GCP DevOps), Agentic OS
|
||||
- Projects: Kitchen-Inventory (catering kitchen management on antigravity), Pendleton-comms-live (walkie talkie on Railway), Agentic OS
|
||||
- Budget: Free tiers only
|
||||
|
||||
## Preferences
|
||||
|
|
|
|||
|
|
@ -6,13 +6,25 @@
|
|||
- Building complete agent orchestration platform
|
||||
- 8 phases planned
|
||||
|
||||
## CloudMart
|
||||
- Status: Active (ongoing)
|
||||
- GCP DevOps multi-region e-commerce platform
|
||||
- GKE Autopilot, Cloud SQL, Cloud CDN, Istio, Next.js
|
||||
## Kitchen-Inventory (Active)
|
||||
- Status: In development — catering kitchen management system
|
||||
- Deployed on antigravity (server/host)
|
||||
- Key features: invoice scanner (AI vision), barcode scanner, shelf life tracker, vendor invoice parsing
|
||||
- Pipeline: Invoice image → pre-process → Gemini vision (primary) → GPT-5.5 (backup) → vendor detection → Claude (text→JSON) → math audit → Firebase write
|
||||
- Vendor invoices: Sysco, Vern's, US Foods (each with custom prompts)
|
||||
- Shelf scans: photo → Gemini → JSON array of items
|
||||
- Code under review: geminiService.ts has 18 issues (4 critical) — wrong shelf prompt in GPT fallback, analyzeImage ignores prompt, no shelf retries, bad model names, bare catches
|
||||
|
||||
- [Waste log](goal:215a1f6a) — I have a couple of files that work as an applicate for a catering kitchen waste
|
||||
## Pendelton-comms-live (Active)
|
||||
- Status: In development — real-time voice communication app for catering events
|
||||
- Purpose: Walkie talkie for employees spread out at big catering events
|
||||
- GitHub: https://github.com/Linecheck-store/Pendelton-comms-live (private)
|
||||
- Last worked on: 2026-02-28
|
||||
- Tech: Node.js + Express + Socket.IO signaling, React + TypeScript frontend
|
||||
- Audio: WebRTC peer-to-peer, STUN/TURN configured
|
||||
- Deployed on Railway
|
||||
- Known issues: audio cutting out on mobile Safari, reconnection logic needs improvement
|
||||
|
||||
- [Pendleton-comms-live](goal:ac785f05) — I need to finish my pendleton walkie talkie app before whiskey fest so my team c
|
||||
- [Ship Agentic OS v1](goal:c24b3d85) — Complete audit and fix all issues
|
||||
|
||||
- [AUdit](goal:a0f84134) — Run a dependency audit
|
||||
- [Push everything in the Kanban board down the line](goal:2c4b22ac) — It involves different skills to get different things done.
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ Agentic OS is a multi-agent orchestration platform that coordinates opencode, He
|
|||
## Key Relationships
|
||||
- User: Developer — AI/ML and DevOps enthusiast
|
||||
- Tool stack: opencode, Hermes Agent, Gemini CLI, deepseek-v4-flash-free
|
||||
- Active projects: CloudMart (GCP DevOps), Agentic OS
|
||||
- Active projects: Kitchen-Inventory (on antigravity), Pendleton-comms-live (on Railway)
|
||||
|
||||
## Standing Decisions
|
||||
- All memory is stored in markdown for cross-agent compatibility
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
# GitHub Activity Log
|
||||
|
||||
> Last checked: 2026-07-25 09:44
|
||||
|
||||
## Project: Agentic OS
|
||||
- Repo: ~/agentic-os (github.com/modimihir07/agentic-os)
|
||||
- Last commit: d9b1b0d "Update Hermes MEMORY.md with v0.2.0 features" (2026-06-05)
|
||||
- Unpushed: 0 commits
|
||||
- Recent (48h): none — last commit 50 days ago
|
||||
- Uncommitted local changes: AGENTS.md, agents/hermes/USER.md (modified, not committed)
|
||||
|
||||
## Project: Kitchen-Inventory
|
||||
- Repo: not cloned locally (deployed on antigravity server/host)
|
||||
- No git activity data available from this machine
|
||||
|
||||
## Project: Pendelton-comms-live
|
||||
- Repo: https://github.com/Linecheck-store/Pendelton-comms-live (private)
|
||||
- Not cloned locally — no git activity data available
|
||||
- Last worked on: 2026-02-28 (per active-projects.md)
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
# Brain Health Report
|
||||
|
||||
> Generated: 2026-07-25 09:45
|
||||
|
||||
## Status: needs attention (activity stale ~8 days)
|
||||
|
||||
### Memory Files
|
||||
- memory.md: 208 words (OK, limit 600)
|
||||
- active-projects.md: 3 projects (2 need status refresh)
|
||||
- log.md: 23 lines (OK)
|
||||
- recent-decisions.md: 1 entry, 68 words (OK)
|
||||
|
||||
### Projects
|
||||
- Agentic OS: Building Phase 1 — last commit 2026-06-05 (50 days ago); uncommitted changes to AGENTS.md and agents/hermes/USER.md sitting in working tree
|
||||
- Kitchen-Inventory: In development — repo not cloned locally; geminiService.ts still flagged with 18 issues (4 critical) awaiting fixes
|
||||
- Pendelton-comms-live: STALE — last worked on 2026-02-28 (~5 months); repo not cloned locally
|
||||
|
||||
### GitHub
|
||||
- Agentic OS: 0 commits in 48h, 0 unpushed, 2 modified files uncommitted
|
||||
- Kitchen-Inventory / Pendelton-comms-live: repos not cloned locally — no data
|
||||
|
||||
### Skills
|
||||
- 2 skills with new learnings since 2026-06-28 (memory-consolidation, firebase-hosting-basics — both 2026-07-17)
|
||||
- 0 skills with eval data (all 18 score-history.json files empty)
|
||||
- Last skill_run: memory-consolidation 2026-07-17 (one run timed out)
|
||||
- Recurring problem: agent timeouts on skill runs (opencode 06-28, hermes 07-17)
|
||||
|
||||
### Alerts
|
||||
1. All 3 projects show no activity in the past week — flag for review.
|
||||
2. Uncommitted changes in ~/agentic-os (AGENTS.md, agents/hermes/USER.md) — commit or discard.
|
||||
3. Eval scoring system unused — score-history.json files never populated.
|
||||
4. Kitchen-Inventory critical code-review findings (geminiService.ts) still open.
|
||||
|
|
@ -0,0 +1 @@
|
|||
Journal entry content verification test
|
||||
|
|
@ -0,0 +1 @@
|
|||
I got to figure out how to get the KanBan Board working right.
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
# Wiki Log
|
||||
|
||||
> Chronological record of all wiki actions. Append-only.
|
||||
> Format: `## [YYYY-MM-DD] action | subject`
|
||||
> Actions: ingest, update, query, lint, create, archive, delete
|
||||
> When this file exceeds 500 entries, rotate: rename to log-YYYY.md, start fresh.
|
||||
|
||||
## [2026-06-26] create | Wiki initialized
|
||||
- Domain: Personal knowledge base (general-purpose second brain)
|
||||
- Structure created with SCHEMA.md, index.md, log.md
|
||||
- Directories: raw/{articles,papers,transcripts,assets}, entities, concepts, comparisons, queries
|
||||
|
||||
## [2026-06-26] ingest | Second Brain User Guide
|
||||
- Source: user-provided document (full user guide)
|
||||
- Created: concepts/second-brain-user-guide.md, concepts/gtd.md
|
||||
- Updated: index.md (2 new entries, total: 3)
|
||||
- Key topics: inbox workflow, page types, knowledge graph, journaling, people/project/idea/reference tracking, import/export, backup
|
||||
|
||||
## [2026-06-28] update | Project memory cleanup
|
||||
- Removed CloudMart reference (not an active project)
|
||||
- Added Kitchen-Inventory: catering kitchen management system (on antigravity) — invoice scanner, barcode scanner, shelf life tracker
|
||||
- Added Pendleton-comms-live: walkie talkie app for catering events (on Railway) — WebRTC, Socket.IO, React/TS
|
||||
- Updated: active-projects.md, memory.md, business-brain.md, AGENTS.md
|
||||
|
|
@ -4,6 +4,7 @@
|
|||
- 3 agents configured: opencode, Hermes Agent, Gemini CLI
|
||||
- Dashboard runs on FastAPI (Python) — localhost only
|
||||
- All skills follow _template/ convention
|
||||
- Memory consolidation: 2026-07-17 (run — all files clean, stale truncated run log from 2026-06-29 cleaned)
|
||||
|
||||
## User
|
||||
- Name: User
|
||||
|
|
@ -11,9 +12,19 @@
|
|||
|
||||
## Active Work
|
||||
- Building Agentic OS from scratch (May 17, 2026)
|
||||
- CloudMart GCP DevOps project ongoing
|
||||
- Kitchen-Inventory — catering kitchen management system (on antigravity)
|
||||
- Pendleton-comms-live — walkie talkie app for catering events (on Railway)
|
||||
|
||||
## Technology Preferences
|
||||
- Preferred model: deepseek-v4-flash-free
|
||||
- Free tiers: GCP Free, GitHub Student Dev Pack, Colab, Kaggle
|
||||
- Knowledge management via markdown vaults
|
||||
|
||||
## Consolidated Insights (2026-06-29)
|
||||
- **opencode** times out on non-code tasks (brainstorming, tdd-cycle, backup-skill, code-review, goal-planner, test-plugin) — route these to hermes
|
||||
- **hermes** can also time out on complex multi-step tasks — keep queries short
|
||||
- **daily-standup** is the most-run skill; hermes executes faster than opencode for it
|
||||
- **Score tracking**: all 18 skills have empty score-history.json — no eval data yet
|
||||
- **All learnings files** are concise (under 200 words each), no compression needed
|
||||
- **No contradictions** found across skill learnings
|
||||
- **recent-decisions.md**: 3 entries archived (all from 2026-05-17), 0 recent — no archiving needed
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
# Recent Decisions
|
||||
|
||||
## 2026-05-17
|
||||
(No decisions in the last 30 days)
|
||||
|
||||
## Archived (older than 30 days)
|
||||
|
||||
### 2026-05-17
|
||||
- Created Agentic OS project structure
|
||||
- 3-agent architecture: opencode (code) + Hermes (memory/channels) + Gemini CLI (research)
|
||||
- Web dashboard only (no CLI dashboard) — FastAPI + vanilla JS SPA
|
||||
|
|
|
|||
|
|
@ -0,0 +1,38 @@
|
|||
# Skill Usage & Effectiveness
|
||||
|
||||
> Last updated: 2026-07-25
|
||||
|
||||
## Audit Log Summary (164 total entries)
|
||||
| Action | Count |
|
||||
|--------|-------|
|
||||
| kanban_task_updated | 31 |
|
||||
| skill_run | 22 |
|
||||
| chat_message | 15 |
|
||||
| brain_update | 11 |
|
||||
| journal_saved | 10 |
|
||||
| settings_updated | 9 |
|
||||
| kanban_task_created | 8 |
|
||||
| agent_registered | 7 |
|
||||
| task_routed | 5 |
|
||||
| standards_discovery_run | 5 |
|
||||
|
||||
## Recent Skill Runs
|
||||
| Skill | Agent | Last Run | Result |
|
||||
|-------|-------|----------|--------|
|
||||
| memory-consolidation | hermes | 2026-07-17 | 1 completed, 1 timeout |
|
||||
| firebase-hosting-basics | opencode | 2026-06-28 | timeout |
|
||||
|
||||
## Eval Scores
|
||||
- All 18 score-history.json files are empty — no eval data has accumulated yet.
|
||||
|
||||
## New Learnings Since 2026-06-28
|
||||
- memory-consolidation/learnings.md (updated 2026-07-17)
|
||||
- firebase-hosting-basics/learnings.md (updated 2026-07-17)
|
||||
- goal-planner, code-review, test-plugin (updated 2026-06-29)
|
||||
|
||||
## Skills Per Project
|
||||
| Project | Skills Used |
|
||||
|---------|-------------|
|
||||
| Agentic OS | memory-consolidation, brain-guardian (Hermes cron), daily-standup, heartbeat |
|
||||
| Kitchen-Inventory | code-review, firebase-hosting-basics |
|
||||
| Pendelton-comms-live | (none recorded) |
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
# Agentic-OS · Agent Time Monitor
|
||||
|
||||
A localhost dashboard that tracks **total time the AI agents spent on the
|
||||
agentic-os project**, plus an embedded graphify knowledge-graph view of the
|
||||
codebase, with one-click sync of the total to **Mnemoverse** persistent memory.
|
||||
|
||||
## What it measures
|
||||
The project's logs (`audit/audit.log`, `data/chat-history.json`,
|
||||
`data/cost-history.json`) record *events* (a chat, a skill run, a write)
|
||||
but not explicit session durations. Total agent time is therefore **estimated**
|
||||
with a session-gap model:
|
||||
|
||||
- Each logged event is a timestamped "touch" by an agent.
|
||||
- A *session* for an agent = a run of touches where each touch is within
|
||||
`GAP` seconds (default 30 min) of the previous one.
|
||||
- Session duration = (last touch − first touch) + `TAIL` (default 2 min, to
|
||||
account for work after the last logged event).
|
||||
- `total_seconds` = sum of all an agent's session durations.
|
||||
|
||||
This is clearly labeled as an ESTIMATE in the UI. Tune `GAP`/`TAIL` via the
|
||||
`AGENT_TIME_GAP` / `AGENT_TIME_TAIL` env vars.
|
||||
|
||||
## Run it
|
||||
cd ~/agentic-os
|
||||
./dashboard/start-monitor.sh # default port 8765
|
||||
# or: python3 dashboard/serve_monitor.py --port 8765
|
||||
|
||||
Then open:
|
||||
http://localhost:8765/dashboard/pages/agent-time-monitor.html
|
||||
|
||||
(Visiting http://localhost:8765/ redirects to the dashboard.)
|
||||
|
||||
## Endpoints (server: dashboard/serve_monitor.py)
|
||||
- `GET /` → redirect to dashboard
|
||||
- `GET /dashboard/.../agent-time-monitor.html` → the dashboard UI
|
||||
- `GET /data/agent-time.json` → computed report (totals + per-agent)
|
||||
- `GET /graphify-out/graph.html` → graphify code knowledge graph
|
||||
- `POST /recompute` → re-run the analyzer from logs
|
||||
- `POST /sync-mnemoverse` → push total to Mnemoverse (graceful)
|
||||
|
||||
## graphify integration
|
||||
The interactive graph is produced by graphify (code-only, no API key needed):
|
||||
|
||||
cd ~/agentic-os
|
||||
graphify . --code-only # writes graphify-out/{graph.json,graph.html,GRAPH_REPORT.md}
|
||||
|
||||
Rebuild any time the codebase changes; the dashboard iframe picks it up live.
|
||||
|
||||
## Mnemoverse integration
|
||||
Set your key to enable the "Sync total to Mnemoverse" button:
|
||||
|
||||
export MNEMOVERSE_API_KEY=mk_live_xxxxxxxx
|
||||
./dashboard/start-monitor.sh
|
||||
|
||||
The server calls `https://core.mnemoverse.com/api/v1/memory/write` with a
|
||||
single memory holding the total agent time, the agent count, and the method.
|
||||
Without a key (or offline) the button reports a clear, graceful message
|
||||
instead of failing.
|
||||
|
||||
## Files
|
||||
- `scripts/analyze_agent_time.py` — derives total/per-agent time from logs
|
||||
- `dashboard/serve_monitor.py` — localhost server (stdlib only)
|
||||
- `dashboard/pages/agent-time-monitor.html` — the dashboard UI
|
||||
- `data/agent-time.json` — last computed report (regenerated each run)
|
||||
- `graphify-out/` — graphify graph (built separately)
|
||||
|
|
@ -35,7 +35,10 @@ const api = {
|
|||
getCost: () => api.get('/api/cost'),
|
||||
recordCost: (data) => api.post('/api/cost/record', data),
|
||||
getPlugins: () => api.get('/api/plugins'),
|
||||
installPlugin: (name) => api.post('/api/plugins/install', { name }),
|
||||
installPlugin: (name, repo_url) => api.post('/api/plugins/install', { name, repo_url }),
|
||||
uninstallPlugin: (name) => api.del(`/api/plugins/${encodeURIComponent(name)}`),
|
||||
getIntegrations: () => api.get('/api/integrations'),
|
||||
addIntegration: (name, url, type) => api.post('/api/integrations', { name, url, type }),
|
||||
getBackups: () => api.get('/api/backups'),
|
||||
createBackup: () => api.post('/api/backup'),
|
||||
restoreBackup: (file) => api.post('/api/backup/restore', { file }),
|
||||
|
|
@ -85,4 +88,11 @@ const api = {
|
|||
// Session Replay
|
||||
listSessions: () => api.get('/api/sessions/list'),
|
||||
getSessionReplay: (id) => api.get(`/api/sessions/${encodeURIComponent(id)}/replay`),
|
||||
// Agent Time Monitor
|
||||
getAgentTime: (recompute = false) => api.get(`/api/agent-time${recompute ? '?recompute=true' : ''}`),
|
||||
recomputeAgentTime: () => api.post('/api/agent-time/recompute', {}),
|
||||
// Agent Registry
|
||||
getAgents: () => api.get('/api/agents'),
|
||||
registerAgent: (data) => api.post('/api/agents/register', data),
|
||||
unregisterAgent: (name) => api.del(`/api/agents/${encodeURIComponent(name)}`),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm@5.3.0/css/xterm.css">
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
|
|
@ -49,6 +50,7 @@
|
|||
<a href="#smart-router" class="nav-item" data-page="smart-router"><span class="nav-icon">🧭</span><span class="nav-label">Smart Router</span></a>
|
||||
<a href="#learning-analytics" class="nav-item" data-page="learning-analytics"><span class="nav-icon">📊</span><span class="nav-label">Learning Analytics</span></a>
|
||||
<a href="#session-replay" class="nav-item" data-page="session-replay"><span class="nav-icon">🔄</span><span class="nav-label">Session Replay</span></a>
|
||||
<a href="#agent-time" class="nav-item" data-page="agent-time"><span class="nav-icon">⏱</span><span class="nav-label">Agent Time</span></a>
|
||||
<div class="sidebar-section"><div class="sidebar-section-label">Management</div></div>
|
||||
<a href="#cost" class="nav-item" data-page="cost"><span class="nav-icon">💰</span><span class="nav-label">Cost Analytics</span></a>
|
||||
<a href="#plugins" class="nav-item" data-page="plugins"><span class="nav-icon">🔌</span><span class="nav-label">Plugins</span></a>
|
||||
|
|
|
|||
|
|
@ -1,115 +1,220 @@
|
|||
let agentHealthInterval = null;
|
||||
|
||||
async function renderAgentHealth() {
|
||||
const content = document.getElementById('pageContent');
|
||||
content.innerHTML = `
|
||||
<div class="page-header">
|
||||
<div class="page-header-left">
|
||||
<div class="page-title">Agent Health</div>
|
||||
<div class="page-subtitle">Real-time monitoring of all 3 agents</div>
|
||||
<h1 class="page-title">Agent Health</h1>
|
||||
<p class="page-subtitle">Monitor and manage all connected agents</p>
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<label class="switch" title="Auto-refresh every 5s">
|
||||
<input type="checkbox" id="healthAutoRefresh" checked onchange="toggleHealthAutoRefresh()">
|
||||
<span class="switch-slider"></span>
|
||||
</label>
|
||||
<span class="text-sm text-muted">Auto</span>
|
||||
<button class="btn btn-primary" onclick="refreshAgentHealth()">🔄 Refresh Now</button>
|
||||
<button class="btn btn-ghost" onclick="loadAgents()">↻ Refresh</button>
|
||||
<button class="btn btn-primary" onclick="showAddAgentModal()">+ Add Agent</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="agentHealthCards" class="grid grid-3" style="margin-bottom:20px">
|
||||
<div class="skeleton" style="height:180px"></div>
|
||||
<div class="skeleton" style="height:180px"></div>
|
||||
<div class="skeleton" style="height:180px"></div>
|
||||
<div id="agentCards" class="grid grid-3">
|
||||
<div class="card"><div class="skeleton" style="height:120px"></div></div>
|
||||
<div class="card"><div class="skeleton" style="height:120px"></div></div>
|
||||
<div class="card"><div class="skeleton" style="height:120px"></div></div>
|
||||
</div>
|
||||
<div class="section-title">Health Overview</div>
|
||||
<div class="card" id="healthOverviewCard">
|
||||
<div class="loading"><div class="loading-spinner"></div><span>Loading health data...</span></div>
|
||||
<div class="card mt-4">
|
||||
<div class="card-header"><span class="card-title">Agent Registry</span></div>
|
||||
<div id="agentTable"></div>
|
||||
</div>
|
||||
`;
|
||||
await refreshAgentHealth();
|
||||
if (document.getElementById('healthAutoRefresh')?.checked) {
|
||||
startHealthAutoRefresh();
|
||||
}
|
||||
await loadAgents();
|
||||
}
|
||||
|
||||
function startHealthAutoRefresh() {
|
||||
stopHealthAutoRefresh();
|
||||
agentHealthInterval = setInterval(refreshAgentHealth, 5000);
|
||||
}
|
||||
|
||||
function stopHealthAutoRefresh() {
|
||||
if (agentHealthInterval) {
|
||||
clearInterval(agentHealthInterval);
|
||||
agentHealthInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
function toggleHealthAutoRefresh() {
|
||||
if (document.getElementById('healthAutoRefresh')?.checked) {
|
||||
startHealthAutoRefresh();
|
||||
} else {
|
||||
stopHealthAutoRefresh();
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshAgentHealth() {
|
||||
async function loadAgents() {
|
||||
try {
|
||||
const data = await api.getAgentHealth();
|
||||
const agents = data.agents || [];
|
||||
const cards = document.getElementById('agentHealthCards');
|
||||
if (!cards) return;
|
||||
const agentIcons = { opencode: '🔧', hermes: '⚡', gemini: '🧠' };
|
||||
const agentColors = { opencode: 'purple', hermes: 'green', gemini: 'blue' };
|
||||
cards.innerHTML = agents.map(a => `
|
||||
<div class="agent-health-card">
|
||||
<div class="agent-health-avatar" style="background:var(--${agentColors[a.name] || 'accent'}-dim);color:var(--${agentColors[a.name] || 'accent'})">
|
||||
${agentIcons[a.name] || '🤖'}
|
||||
</div>
|
||||
<div class="agent-health-info">
|
||||
<div class="agent-health-name" style="text-transform:capitalize">${a.name}</div>
|
||||
<div class="agent-health-status">
|
||||
<span class="agent-dot ${a.status === 'online' ? 'online' : a.status === 'warning' ? 'warning' : 'offline'}"></span>
|
||||
<span style="text-transform:capitalize;color:var(--text-secondary)">${a.status}</span>
|
||||
const { agents } = await api.getAgents();
|
||||
const container = document.getElementById('agentCards');
|
||||
const table = document.getElementById('agentTable');
|
||||
|
||||
// Card view
|
||||
container.innerHTML = agents.map(a => {
|
||||
const sc = statusColor(a.status);
|
||||
return `
|
||||
<div class="card agent-card">
|
||||
<div class="agent-card-header">
|
||||
<div class="agent-dot ${a.status}" style="width:12px;height:12px"></div>
|
||||
<span style="font-weight:600;font-size:15px">${escapeHtml(a.display_name || a.name)}</span>
|
||||
${a.builtin ? '<span class="nav-badge" style="background:var(--accent)">built-in</span>' : '<span class="nav-badge" style="background:var(--green)">custom</span>'}
|
||||
</div>
|
||||
<div class="agent-health-stats">
|
||||
<div class="agent-health-stat">
|
||||
<div class="agent-health-stat-value" style="color:var(--green)">${a.status === 'online' ? '100' : '0'}%</div>
|
||||
<div class="agent-health-stat-label">Uptime</div>
|
||||
</div>
|
||||
<div class="agent-health-stat">
|
||||
<div class="agent-health-stat-value" style="color:var(--accent-light)">${a.status === 'online' ? '✓' : '✗'}</div>
|
||||
<div class="agent-health-stat-label">Reachable</div>
|
||||
</div>
|
||||
<div class="agent-health-stat">
|
||||
<div class="agent-health-stat-value text-sm" style="font-size:11px;color:var(--text-muted)">${new Date(data.updated).toLocaleTimeString()}</div>
|
||||
<div class="agent-health-stat-label">Last Check</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
const overview = document.getElementById('healthOverviewCard');
|
||||
if (overview) {
|
||||
const online = agents.filter(a => a.status === 'online').length;
|
||||
const total = agents.length;
|
||||
overview.innerHTML = `
|
||||
<div style="display:flex;align-items:center;justify-content:space-between">
|
||||
<div>
|
||||
<div style="font-size:14px;font-weight:600">System Status</div>
|
||||
<div style="font-size:12px;color:var(--text-secondary);margin-top:4px">
|
||||
${online}/${total} agents online · Last updated: ${new Date(data.updated).toLocaleTimeString()}
|
||||
</div>
|
||||
</div>
|
||||
<div class="status-indicator ${online === total ? 'online' : online > 0 ? 'warning' : 'offline'}">
|
||||
<span class="agent-dot ${online === total ? 'online' : online > 0 ? 'warning' : 'offline'}"></span>
|
||||
${online === total ? 'All Online' : online > 0 ? 'Partial' : 'Offline'}
|
||||
<div style="font-size:12px;color:var(--text-secondary);margin:8px 0">${escapeHtml(a.description || 'No description')}</div>
|
||||
<div style="display:flex;gap:8px;align-items:center">
|
||||
<span style="font-size:11px;color:${sc.text};text-transform:uppercase;font-weight:600">${a.status}</span>
|
||||
<span style="font-size:11px;color:var(--text-muted)">${a.type || 'cli'}</span>
|
||||
</div>
|
||||
${!a.builtin ? `<div style="margin-top:12px"><button class="btn btn-ghost btn-sm" onclick="removeAgent('${a.name}')">Remove</button></div>` : ''}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}).join('');
|
||||
|
||||
// Table view
|
||||
table.innerHTML = `
|
||||
<table class="agent-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Agent</th>
|
||||
<th>Type</th>
|
||||
<th>Status</th>
|
||||
<th>Source</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${agents.map(a => `
|
||||
<tr>
|
||||
<td>
|
||||
<div style="display:flex;align-items:center;gap:8px">
|
||||
<div class="agent-dot ${a.status}" style="width:8px;height:8px"></div>
|
||||
<span style="font-weight:500">${escapeHtml(a.display_name || a.name)}</span>
|
||||
</div>
|
||||
<div style="font-size:11px;color:var(--text-muted);margin-top:2px">${escapeHtml(a.description || '')}</div>
|
||||
</td>
|
||||
<td style="text-transform:uppercase;font-size:11px">${a.type || 'cli'}</td>
|
||||
<td><span style="color:${statusColor(a.status).text};font-size:11px;text-transform:uppercase;font-weight:600">${a.status}</span></td>
|
||||
<td>${a.builtin ? '<span style="color:var(--text-muted)">built-in</span>' : '<span style="color:var(--green)">custom</span>'}</td>
|
||||
<td>
|
||||
${!a.builtin ? `<button class="btn btn-ghost btn-sm" onclick="removeAgent('${a.name}')">Remove</button>` : '<span style="color:var(--text-muted);font-size:11px">—</span>'}
|
||||
</td>
|
||||
</tr>
|
||||
`).join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
`;
|
||||
} catch (err) {
|
||||
const cards = document.getElementById('agentHealthCards');
|
||||
if (cards) cards.innerHTML = `<div class="empty-state" style="grid-column:1/-1"><div class="empty-state-icon">⚠</div><div class="empty-state-title">Failed to load health data</div><div class="empty-state-desc">${escapeHtml(err.message)}</div></div>`;
|
||||
document.getElementById('agentCards').innerHTML = `<div class="card" style="grid-column:1/-1"><div class="empty-state"><div class="empty-state-icon">⚠</div><div class="empty-state-title">Error</div><div class="empty-state-desc">${escapeHtml(err.message)}</div></div></div>`;
|
||||
}
|
||||
}
|
||||
|
||||
function showAddAgentModal() {
|
||||
showModal('Register New Agent', `
|
||||
<div class="form-group">
|
||||
<label class="form-label">Agent Name *</label>
|
||||
<input id="agentName" class="form-input" placeholder="e.g. claude-code, codex, custom-llm">
|
||||
<div style="font-size:11px;color:var(--text-muted);margin-top:4px">Unique identifier (lowercase, no spaces)</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Display Name</label>
|
||||
<input id="agentDisplay" class="form-input" placeholder="e.g. Claude Code">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Description</label>
|
||||
<input id="agentDesc" class="form-input" placeholder="What this agent does">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Agent Type</label>
|
||||
<select id="agentType" class="form-select" onchange="updateAgentForm()">
|
||||
<option value="cli">CLI Binary (local command)</option>
|
||||
<option value="http">HTTP API (remote endpoint)</option>
|
||||
<option value="mcp">MCP Server (Model Context Protocol)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div id="cliFields">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Binary Name *</label>
|
||||
<input id="agentBinary" class="form-input" placeholder="e.g. claude, codex">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Run Arguments (one per line, use {message} for user input)</label>
|
||||
<textarea id="agentRunArgs" class="form-textarea" rows="3" placeholder="claude -p {message}"></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Health Check</label>
|
||||
<select id="agentCheckType" class="form-select">
|
||||
<option value="binary">Binary exists in PATH</option>
|
||||
<option value="custom">Custom command</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group" id="customCheckGroup" style="display:none">
|
||||
<label class="form-label">Custom Check Command</label>
|
||||
<input id="agentCheckCmd" class="form-input" placeholder="e.g. claude --version">
|
||||
</div>
|
||||
</div>
|
||||
<div id="httpFields" style="display:none">
|
||||
<div class="form-group">
|
||||
<label class="form-label">API URL *</label>
|
||||
<input id="agentApiUrl" class="form-input" placeholder="https://api.example.com/chat">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">HTTP Method</label>
|
||||
<select id="agentApiMethod" class="form-select">
|
||||
<option value="POST">POST</option>
|
||||
<option value="GET">GET</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Timeout (seconds)</label>
|
||||
<input id="agentTimeout" class="form-input" type="number" value="60">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Router Keywords (comma-separated)</label>
|
||||
<input id="agentKeywords" class="form-input" placeholder="code, debug, test, build">
|
||||
<div style="font-size:11px;color:var(--text-muted);margin-top:4px">Keywords for smart routing to this agent</div>
|
||||
</div>
|
||||
`, `
|
||||
<button class="btn btn-ghost" onclick="closeModal()">Cancel</button>
|
||||
<button class="btn btn-primary" onclick="registerAgent()">Register Agent</button>
|
||||
`);
|
||||
|
||||
// Show/hide custom check command field
|
||||
document.getElementById('agentCheckType').addEventListener('change', (e) => {
|
||||
document.getElementById('customCheckGroup').style.display = e.target.value === 'custom' ? 'block' : 'none';
|
||||
});
|
||||
}
|
||||
|
||||
function updateAgentForm() {
|
||||
const type = document.getElementById('agentType').value;
|
||||
document.getElementById('cliFields').style.display = type === 'cli' ? 'block' : 'none';
|
||||
document.getElementById('httpFields').style.display = type === 'http' ? 'block' : 'none';
|
||||
}
|
||||
|
||||
async function registerAgent() {
|
||||
const name = document.getElementById('agentName').value.trim();
|
||||
if (!name) { showToast('Agent name is required', 'error'); return; }
|
||||
|
||||
const type = document.getElementById('agentType').value;
|
||||
const data = {
|
||||
name: name,
|
||||
display_name: document.getElementById('agentDisplay').value.trim() || name,
|
||||
description: document.getElementById('agentDesc').value.trim(),
|
||||
type: type,
|
||||
binary: document.getElementById('agentBinary').value.trim() || name,
|
||||
check_type: document.getElementById('agentCheckType').value === 'custom' ? 'custom' : 'binary',
|
||||
check_command: document.getElementById('agentCheckCmd').value.trim(),
|
||||
timeout: parseInt(document.getElementById('agentTimeout').value) || 60,
|
||||
router_keywords: document.getElementById('agentKeywords').value.split(',').map(k => k.trim()).filter(Boolean),
|
||||
};
|
||||
|
||||
if (type === 'cli') {
|
||||
const runArgs = document.getElementById('agentRunArgs').value.trim();
|
||||
if (runArgs) {
|
||||
data.run_args = runArgs.split('\n').map(a => a.trim()).filter(Boolean);
|
||||
}
|
||||
} else if (type === 'http') {
|
||||
data.api_url = document.getElementById('agentApiUrl').value.trim();
|
||||
data.api_method = document.getElementById('agentApiMethod').value;
|
||||
}
|
||||
|
||||
try {
|
||||
await api.registerAgent(data);
|
||||
closeModal();
|
||||
showToast(`Agent '${name}' registered successfully`, 'success');
|
||||
loadAgents();
|
||||
} catch (err) {
|
||||
showToast(`Error: ${err.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function removeAgent(name) {
|
||||
if (!confirm(`Remove agent '${name}'?`)) return;
|
||||
try {
|
||||
await api.unregisterAgent(name);
|
||||
showToast(`Agent '${name}' removed`, 'success');
|
||||
loadAgents();
|
||||
} catch (err) {
|
||||
showToast(`Error: ${err.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,222 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Agentic-OS · Agent Time Monitor</title>
|
||||
<style>
|
||||
:root{
|
||||
--bg:#0b0f17; --panel:#121826; --panel2:#0f1420; --line:#1f2a3d;
|
||||
--txt:#e6edf6; --muted:#8a99b3; --accent:#5b8cff; --accent2:#37e0a6;
|
||||
--warn:#ffcf5c; --danger:#ff6b6b;
|
||||
--open:#5b8cff; --hermes:#37e0a6; --gemini:#ffcf5c; --sys:#9b8cff;
|
||||
}
|
||||
*{box-sizing:border-box}
|
||||
body{margin:0;background:var(--bg);color:var(--txt);
|
||||
font-family:"Inter",system-ui,-apple-system,Segoe UI,Roboto,sans-serif;}
|
||||
header{padding:18px 26px;border-bottom:1px solid var(--line);
|
||||
display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:12px}
|
||||
header h1{font-size:18px;margin:0;font-weight:650;letter-spacing:.2px}
|
||||
header .sub{color:var(--muted);font-size:13px;margin-top:2px}
|
||||
.badge{font-size:11px;color:var(--accent2);border:1px solid #1d3b30;
|
||||
background:#0e1c17;padding:3px 9px;border-radius:20px}
|
||||
main{padding:22px 26px;max-width:1280px;margin:0 auto}
|
||||
.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:16px;margin-bottom:22px}
|
||||
.card{background:var(--panel);border:1px solid var(--line);border-radius:14px;padding:18px}
|
||||
.card .label{color:var(--muted);font-size:12px;text-transform:uppercase;letter-spacing:.6px}
|
||||
.card .big{font-size:30px;font-weight:700;margin-top:6px;line-height:1.1}
|
||||
.card .meta{color:var(--muted);font-size:12px;margin-top:6px}
|
||||
.row{display:flex;gap:10px;flex-wrap:wrap;align-items:center;margin:14px 0 8px}
|
||||
button{cursor:pointer;border:1px solid var(--line);background:var(--panel2);
|
||||
color:var(--txt);padding:9px 14px;border-radius:10px;font-size:13px;font-weight:600}
|
||||
button.primary{background:var(--accent);border-color:var(--accent);color:#fff}
|
||||
button:disabled{opacity:.5;cursor:not-allowed}
|
||||
.status{font-size:13px;color:var(--muted)}
|
||||
.status.ok{color:var(--accent2)} .status.err{color:var(--danger)}
|
||||
table{width:100%;border-collapse:collapse;margin-top:6px}
|
||||
th,td{text-align:left;padding:9px 10px;border-bottom:1px solid var(--line);font-size:13px}
|
||||
th{color:var(--muted);font-weight:600;text-transform:uppercase;font-size:11px;letter-spacing:.5px}
|
||||
td.num{text-align:right;font-variant-numeric:tabular-nums}
|
||||
.bar{height:9px;border-radius:6px;background:linear-gradient(90deg,var(--accent),var(--accent2));
|
||||
width:0;transition:width 1s ease-out}
|
||||
.barwrap{background:#0c1320;border-radius:6px;overflow:hidden;min-width:90px}
|
||||
section{background:var(--panel);border:1px solid var(--line);border-radius:14px;
|
||||
padding:16px 18px;margin-bottom:22px}
|
||||
section h2{font-size:14px;margin:0 0 4px;font-weight:650}
|
||||
section .hint{color:var(--muted);font-size:12px;margin-bottom:10px}
|
||||
iframe{border:1px solid var(--line);border-radius:12px;width:100%;height:620px;background:#0a0d14}
|
||||
.pill{display:inline-block;font-size:11px;padding:2px 8px;border-radius:20px;
|
||||
border:1px solid var(--line);color:var(--muted);margin-right:6px}
|
||||
code{background:#0c1320;padding:1px 6px;border-radius:5px;font-size:12px}
|
||||
a{color:var(--accent)}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<div>
|
||||
<h1>Agentic-OS · Agent Time Monitor</h1>
|
||||
<div class="sub">Total AI-agent time spent on the project · derived from audit & chat logs</div>
|
||||
</div>
|
||||
<div class="badge" id="project-badge">project: agentic-os</div>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<section style="background:linear-gradient(135deg,#121826,#0e1626)">
|
||||
<div class="grid">
|
||||
<div class="card">
|
||||
<div class="label">Total Agent Time</div>
|
||||
<div class="big" id="total-time">—</div>
|
||||
<div class="meta" id="total-meta">loading…</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="label">Agents Active</div>
|
||||
<div class="big" id="agent-count">—</div>
|
||||
<div class="meta" id="agent-meta">distinct agents</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="label">Logged Events</div>
|
||||
<div class="big" id="event-count">—</div>
|
||||
<div class="meta" id="event-meta">audit + chat + cost</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="label">Coverage</div>
|
||||
<div class="big" id="coverage">—</div>
|
||||
<div class="meta" id="coverage-meta">first → last activity</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<button class="primary" id="refresh-btn">↻ Recompute</button>
|
||||
<button id="sync-btn">⇪ Sync total to Mnemoverse</button>
|
||||
<span class="status" id="status">Ready. Estimates are session-gap based.</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Time per Agent</h2>
|
||||
<div class="hint">Each bar is the estimated total time an agent was engaged with the project
|
||||
(sessions of activity within a 30-min gap window, plus a 2-min tail).</div>
|
||||
<table>
|
||||
<thead><tr><th>Agent</th><th>Total Time</th><th style="width:34%">Share</th><th class="num">Sessions</th><th class="num">Touches</th></tr></thead>
|
||||
<tbody id="agent-rows"><tr><td colspan="5" class="meta">loading…</td></tr></tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Codebase Knowledge Graph · graphify</h2>
|
||||
<div class="hint">Interactive graphify visualization of the <code>agentic-os</code> codebase
|
||||
(nodes = code symbols/modules, edges = relationships). Rebuild with
|
||||
<code>graphify . --code-only</code> inside the project.</div>
|
||||
<div class="row">
|
||||
<span class="pill" id="g-nodes">nodes: —</span>
|
||||
<span class="pill" id="g-edges">edges: —</span>
|
||||
<span class="pill" id="g-communities">communities: —</span>
|
||||
<a href="/graphify-out/graph.html" target="_blank">↗ open full graph in new tab</a>
|
||||
</div>
|
||||
<iframe src="/graphify-out/graph.html" title="graphify knowledge graph" loading="lazy"></iframe>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
const API = {
|
||||
async get(path){ const r = await fetch(path); return await r.json(); },
|
||||
};
|
||||
|
||||
function fmtHMS(s){
|
||||
s = Math.floor(s); const h=Math.floor(s/3600), m=Math.floor((s%3600)/60), sec=s%60;
|
||||
return h+"h "+m+"m "+sec+"s";
|
||||
}
|
||||
function fmtDuration(s){
|
||||
s=Math.floor(s); const h=Math.floor(s/3600), m=Math.floor((s%3600)/60);
|
||||
if(h>0) return h+"h "+m+"m"; return m+"m "+s%60+"s";
|
||||
}
|
||||
|
||||
async function load(){
|
||||
setStatus("Loading…");
|
||||
try{
|
||||
const data = await API.get("/data/agent-time.json");
|
||||
document.getElementById("total-time").textContent = data.total_human;
|
||||
document.getElementById("total-meta").textContent =
|
||||
"estimated · " + data.method;
|
||||
const agents = data.agents || {};
|
||||
const keys = Object.keys(agents);
|
||||
document.getElementById("agent-count").textContent = keys.length;
|
||||
document.getElementById("event-count").textContent = data.event_count;
|
||||
document.getElementById("coverage").textContent =
|
||||
(data.first_seen||"—").slice(0,10) + " → " + (data.last_seen||"—").slice(0,10);
|
||||
|
||||
// rows
|
||||
const max = Math.max(...keys.map(k=>agents[k].total_seconds), 1);
|
||||
const sorted = keys.sort((a,b)=>agents[b].total_seconds-agents[a].total_seconds);
|
||||
const tb = document.getElementById("agent-rows");
|
||||
tb.innerHTML = "";
|
||||
sorted.forEach((k,i)=>{
|
||||
const a = agents[k];
|
||||
const pct = (a.total_seconds/max*100).toFixed(1);
|
||||
const tr = document.createElement("tr");
|
||||
tr.innerHTML =
|
||||
`<td>${k}</td>`+
|
||||
`<td><b>${fmtDuration(a.total_seconds)}</b></td>`+
|
||||
`<td><div class="barwrap"><div class="bar" data-w="${pct}" style="background:${
|
||||
k==='system'?'var(--sys)':k==='hermes'?'var(--hermes)':k==='opencode'?'var(--open)':k==='gemini'?'var(--gemini)':'linear-gradient(90deg,var(--accent),var(--accent2))'
|
||||
}"></div></div></td>`+
|
||||
`<td class="num">${a.sessions}</td>`+
|
||||
`<td class="num">${a.touches}</td>`;
|
||||
tb.appendChild(tr);
|
||||
});
|
||||
// animate bars
|
||||
setTimeout(()=>document.querySelectorAll(".bar").forEach(b=>b.style.width=b.dataset.w+"%"),60);
|
||||
|
||||
// graph meta
|
||||
if(data.graph){
|
||||
document.getElementById("g-nodes").textContent = "nodes: "+(data.graph.nodes||"—");
|
||||
document.getElementById("g-edges").textContent = "edges: "+(data.graph.edges||"—");
|
||||
document.getElementById("g-communities").textContent = "communities: "+(data.graph.communities||"—");
|
||||
}
|
||||
setStatus("✓ Computed at "+(data.generated_at||"").slice(0,19).replace("T"," ")+" UTC", "ok");
|
||||
}catch(e){
|
||||
setStatus("Failed to load agent-time.json: "+e.message, "err");
|
||||
}
|
||||
}
|
||||
|
||||
async function recompute(){
|
||||
setStatus("Recomputing from logs…");
|
||||
document.getElementById("refresh-btn").disabled = true;
|
||||
try{
|
||||
const r = await fetch("recompute", {method:"POST"});
|
||||
const j = await r.json();
|
||||
if(!j.ok){ throw new Error(j.error||"recompute failed"); }
|
||||
setStatus("✓ Recomputed: "+j.total_human+" across "+j.agents+" agents", "ok");
|
||||
await load();
|
||||
}catch(e){
|
||||
setStatus("Recompute error: "+e.message, "err");
|
||||
}finally{
|
||||
document.getElementById("refresh-btn").disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function syncMnemoverse(){
|
||||
const btn = document.getElementById("sync-btn");
|
||||
btn.disabled = true; setStatus("Pushing total to Mnemoverse…");
|
||||
try{
|
||||
const r = await fetch("sync-mnemoverse", {method:"POST"});
|
||||
const j = await r.json();
|
||||
if(j.ok){
|
||||
setStatus("✓ Synced to Mnemoverse: "+j.message, "ok");
|
||||
}else{
|
||||
// graceful: show guidance (no key / offline)
|
||||
setStatus("⚠ Mnemoverse: "+(j.error||"not synced")+(j.hint?(" — "+j.hint):""), "err");
|
||||
}
|
||||
}catch(e){
|
||||
setStatus("Mnemoverse sync error: "+e.message, "err");
|
||||
}finally{ btn.disabled=false; }
|
||||
}
|
||||
|
||||
function setStatus(msg, cls){ const s=document.getElementById("status"); s.textContent=msg; s.className="status"+(cls?(" "+cls):""); }
|
||||
|
||||
document.getElementById("refresh-btn").addEventListener("click", recompute);
|
||||
document.getElementById("sync-btn").addEventListener("click", syncMnemoverse);
|
||||
load();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,176 @@
|
|||
// Agent Time Monitor page — total time AI agents spent on the project.
|
||||
// Data comes from the backend (/api/agent-time), which derives totals
|
||||
// from event logs via a session-gap estimator. No Mnemoverse widget.
|
||||
|
||||
function fmtDuration(seconds) {
|
||||
seconds = Math.floor(seconds || 0);
|
||||
const h = Math.floor(seconds / 3600);
|
||||
const m = Math.floor((seconds % 3600) / 60);
|
||||
const s = seconds % 60;
|
||||
if (h > 0) return `${h}h ${m}m`;
|
||||
if (m > 0) return `${m}m ${s}s`;
|
||||
return `${s}s`;
|
||||
}
|
||||
|
||||
function fmtHMS(seconds) {
|
||||
seconds = Math.floor(seconds || 0);
|
||||
const h = Math.floor(seconds / 3600);
|
||||
const m = Math.floor((seconds % 3600) / 60);
|
||||
const s = seconds % 60;
|
||||
return `${h}h ${m}m ${s}s`;
|
||||
}
|
||||
|
||||
// Agent accent colors, matching agent-health / known identities.
|
||||
function agentColor(agent) {
|
||||
const map = {
|
||||
system: 'var(--purple, #9b8cff)',
|
||||
hermes: 'var(--green, #37e0a6)',
|
||||
opencode: 'var(--accent, #6c5ce7)',
|
||||
gemini: 'var(--yellow, #ffcf5c)',
|
||||
codex: '#5b8cff',
|
||||
jarvis: '#ff79c6',
|
||||
kilocode: '#74b9ff',
|
||||
test_claude: '#a29bfe',
|
||||
test: '#7f8fa6',
|
||||
};
|
||||
return map[agent] || 'linear-gradient(90deg, var(--accent), var(--green))';
|
||||
}
|
||||
|
||||
async function renderAgentTime() {
|
||||
const content = document.getElementById('pageContent');
|
||||
content.innerHTML = `
|
||||
<div class="page-header">
|
||||
<div class="page-header-left">
|
||||
<h1 class="page-title">Agent Time</h1>
|
||||
<p class="page-subtitle">Total time AI agents have spent on this project</p>
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-ghost" onclick="loadAgentTime(false)">↻ Refresh</button>
|
||||
<button class="btn btn-primary" onclick="recomputeAgentTime()">⚡ Recompute</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="agentTimeBody">
|
||||
<div class="grid grid-4">
|
||||
<div class="card"><div class="skeleton" style="height:90px"></div></div>
|
||||
<div class="card"><div class="skeleton" style="height:90px"></div></div>
|
||||
<div class="card"><div class="skeleton" style="height:90px"></div></div>
|
||||
<div class="card"><div class="skeleton" style="height:90px"></div></div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
await loadAgentTime(false);
|
||||
}
|
||||
|
||||
async function loadAgentTime(recompute) {
|
||||
const body = document.getElementById('agentTimeBody');
|
||||
try {
|
||||
const data = await api.getAgentTime(recompute);
|
||||
const agents = data.agents || {};
|
||||
const keys = Object.keys(agents);
|
||||
const max = Math.max(1, ...keys.map(k => agents[k].total_seconds));
|
||||
const sorted = keys.slice().sort((a, b) => agents[b].total_seconds - agents[a].total_seconds);
|
||||
|
||||
const g = data.graph || {};
|
||||
const graphCard = g.nodes != null ? `
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-title">Codebase Graph (graphify)</span></div>
|
||||
<div style="display:flex;gap:14px;flex-wrap:wrap;margin-top:8px">
|
||||
<div><div style="font-size:22px;font-weight:700">${g.nodes}</div><div style="font-size:11px;color:var(--text-muted);text-transform:uppercase">nodes</div></div>
|
||||
<div><div style="font-size:22px;font-weight:700">${g.edges}</div><div style="font-size:11px;color:var(--text-muted);text-transform:uppercase">edges</div></div>
|
||||
<div><div style="font-size:22px;font-weight:700">${g.communities != null ? g.communities : '—'}</div><div style="font-size:11px;color:var(--text-muted);text-transform:uppercase">communities</div></div>
|
||||
</div>
|
||||
<a class="btn btn-ghost btn-sm mt-3" href="/dashboard/graphify-out/graph.html" target="_blank">↗ Open interactive graph</a>
|
||||
</div>` : '';
|
||||
|
||||
body.innerHTML = `
|
||||
<div class="grid grid-4">
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-title">Total Agent Time</span></div>
|
||||
<div style="font-size:30px;font-weight:800;margin-top:6px">${escapeHtml(data.total_human || '0h 0m 0s')}</div>
|
||||
<div style="font-size:11px;color:var(--text-muted);margin-top:6px">estimated · ${escapeHtml((data.method || '').replace('session-gap estimate ', ''))}</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-title">Active Agents</span></div>
|
||||
<div style="font-size:30px;font-weight:800;margin-top:6px">${keys.length}</div>
|
||||
<div style="font-size:11px;color:var(--text-muted);margin-top:6px">distinct agents</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-title">Logged Events</span></div>
|
||||
<div style="font-size:30px;font-weight:800;margin-top:6px">${data.event_count || 0}</div>
|
||||
<div style="font-size:11px;color:var(--text-muted);margin-top:6px">audit + chat + cost</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-title">Coverage</span></div>
|
||||
<div style="font-size:18px;font-weight:700;margin-top:8px">${(data.first_seen || '—').slice(0, 10)}</div>
|
||||
<div style="font-size:13px;color:var(--text-muted)">→ ${(data.last_seen || '—').slice(0, 10)}</div>
|
||||
<div style="font-size:11px;color:var(--text-muted);margin-top:4px">first → last activity</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mt-4">
|
||||
<div class="card-header"><span class="card-title">Time per Agent</span>
|
||||
<span style="font-size:11px;color:var(--text-muted)">sessions of activity (30-min gap) + 2-min tail</span></div>
|
||||
<div class="table-wrap">
|
||||
<table class="agent-table">
|
||||
<thead><tr><th>Agent</th><th>Total Time</th><th>Share</th><th class="num">Sessions</th><th class="num">Touches</th></tr></thead>
|
||||
<tbody>
|
||||
${sorted.map(k => {
|
||||
const a = agents[k];
|
||||
const pct = ((a.total_seconds / max) * 100).toFixed(1);
|
||||
return `
|
||||
<tr>
|
||||
<td><span style="font-weight:600">${escapeHtml(k)}</span></td>
|
||||
<td><b>${fmtDuration(a.total_seconds)}</b></td>
|
||||
<td style="min-width:140px">
|
||||
<div style="height:8px;border-radius:6px;background:var(--bg-elevated,#0c1320);overflow:hidden">
|
||||
<div style="height:100%;width:0;background:${agentColor(k)};border-radius:6px;transition:width .8s ease-out" data-w="${pct}%"></div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="num">${a.sessions}</td>
|
||||
<td class="num">${a.touches}</td>
|
||||
</tr>`;
|
||||
}).join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${graphCard ? `<div class="card mt-4">${graphCard}</div>` : ''}
|
||||
|
||||
<div style="font-size:11px;color:var(--text-muted);margin-top:14px">
|
||||
Computed ${escapeHtml((data.generated_at || '').replace('T', ' ').slice(0, 19))} UTC.
|
||||
Totals are <b>estimates</b> — the project logs events, not durations; see the method in the report.
|
||||
</div>
|
||||
`;
|
||||
// animate bars
|
||||
setTimeout(() => {
|
||||
document.querySelectorAll('#agentTimeBody [data-w]').forEach(el => { el.style.width = el.dataset.w; });
|
||||
}, 60);
|
||||
} catch (err) {
|
||||
body.innerHTML = `
|
||||
<div class="card">
|
||||
<div class="empty-state">
|
||||
<div class="empty-state-icon">⚠</div>
|
||||
<div class="empty-state-title">Could not load agent-time report</div>
|
||||
<div class="empty-state-desc">${escapeHtml(err.message)}</div>
|
||||
<button class="btn btn-primary mt-3" onclick="recomputeAgentTime()">Recompute now</button>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
async function recomputeAgentTime() {
|
||||
const body = document.getElementById('agentTimeBody');
|
||||
showToast('Recomputing from logs…', 'info');
|
||||
try {
|
||||
const res = await api.recomputeAgentTime();
|
||||
if (res.ok) {
|
||||
showToast(`Recomputed: ${res.total_human} across ${res.agents} agents`, 'success');
|
||||
await loadAgentTime(false);
|
||||
} else {
|
||||
showToast('Recompute failed', 'error');
|
||||
}
|
||||
} catch (err) {
|
||||
showToast(`Recompute error: ${err.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
|
@ -77,7 +77,8 @@ async function saveJournalEntry() {
|
|||
if (wc) wc.textContent = words;
|
||||
} catch (err) {
|
||||
const status = document.getElementById('journalSaveStatus');
|
||||
if (status) status.textContent = 'Save failed';
|
||||
if (status) status.textContent = 'Save failed: ' + err.message;
|
||||
console.error('Journal save error:', err);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,68 +4,178 @@ async function renderPlugins() {
|
|||
<div class="page-header">
|
||||
<div class="page-header-left">
|
||||
<h1 class="page-title">Plugin Registry</h1>
|
||||
<p class="page-subtitle">Manage installed plugins and extensions</p>
|
||||
<p class="page-subtitle">Install skills from GitHub or create your own</p>
|
||||
</div>
|
||||
<button class="btn btn-primary" onclick="showInstallPlugin()">+ Install</button>
|
||||
<button class="btn btn-primary" onclick="showInstallPlugin()">+ Install from GitHub</button>
|
||||
</div>
|
||||
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-bottom:24px">
|
||||
<div class="card">
|
||||
<div class="card-title" style="margin-bottom:12px">Install a Plugin</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">GitHub Repo URL</label>
|
||||
<input id="pluginRepoInput" class="form-input" placeholder="https://github.com/you/your-skill-repo">
|
||||
<div class="form-hint">Paste a GitHub URL to clone a skill from a repo</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Plugin Name (optional)</label>
|
||||
<input id="pluginNameInput" class="form-input" placeholder="auto-detect from repo URL">
|
||||
</div>
|
||||
<button class="btn btn-primary" onclick="installPluginFromRepo()">Install</button>
|
||||
<div id="installStatus" style="margin-top:8px;font-size:12px"></div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-title" style="margin-bottom:12px">Connect External App</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">App Name</label>
|
||||
<input id="integrationNameInput" class="form-input" placeholder="e.g., Slack, Zapier, n8n">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Webhook / URL</label>
|
||||
<input id="integrationUrlInput" class="form-input" placeholder="https://hooks.slack.com/...">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Type</label>
|
||||
<select id="integrationType" class="form-input">
|
||||
<option value="webhook">Webhook</option>
|
||||
<option value="api">REST API</option>
|
||||
<option value="zapier">Zapier</option>
|
||||
<option value="n8n">n8n</option>
|
||||
</select>
|
||||
</div>
|
||||
<button class="btn btn-primary" onclick="addIntegration()">Connect</button>
|
||||
<div id="integrationStatus" style="margin-top:8px;font-size:12px"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-bottom:16px">
|
||||
<div class="card-title" style="margin-bottom:12px">Connected Apps</div>
|
||||
<div id="integrationsList"></div>
|
||||
</div>
|
||||
|
||||
<div id="pluginList"><div class="loading"><div class="loading-spinner"></div></div></div>
|
||||
`;
|
||||
|
||||
try {
|
||||
const data = await api.getPlugins();
|
||||
const plugins = data.plugins || [];
|
||||
const container = document.getElementById('pluginList');
|
||||
const [pluginsData, integrationsData] = await Promise.all([
|
||||
api.getPlugins(),
|
||||
api.getIntegrations(),
|
||||
]);
|
||||
|
||||
if (plugins.length === 0) {
|
||||
container.innerHTML = '<div class="empty-state"><div class="empty-state-icon">🔌</div><div class="empty-state-title">No plugins installed</div><div class="empty-state-desc">Install plugins from the registry or create your own</div></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="table-wrapper">
|
||||
<table>
|
||||
<thead><tr><th>Plugin</th><th>Version</th><th>Type</th><th>Installed</th></tr></thead>
|
||||
<tbody>
|
||||
${plugins.map(p => `
|
||||
<tr>
|
||||
<td><strong>${p.name}</strong></td>
|
||||
<td><code>${p.version || '1.0.0'}</code></td>
|
||||
<td><span class="badge badge-info">${p.type || 'skill'}</span></td>
|
||||
<td style="font-size:12px;color:var(--text-muted)">${formatDate(p.installed)}</td>
|
||||
</tr>
|
||||
`).join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div style="font-size:12px;color:var(--text-muted);text-align:right;margin-top:8px">${plugins.length} plugin${plugins.length !== 1 ? 's' : ''}</div>
|
||||
`;
|
||||
renderPluginTable(pluginsData.plugins || []);
|
||||
renderIntegrations(integrationsData.integrations || []);
|
||||
} catch (err) {
|
||||
document.getElementById('pluginList').innerHTML = `<div class="empty-state"><div class="empty-state-icon">⚠</div><div class="empty-state-title">${escapeHtml(err.message)}</div></div>`;
|
||||
}
|
||||
}
|
||||
|
||||
function showInstallPlugin() {
|
||||
showModal('Install Plugin', `
|
||||
<div class="form-group">
|
||||
<label class="form-label">Plugin Name</label>
|
||||
<input id="pluginNameInput" class="form-input" placeholder="e.g., my-custom-skill">
|
||||
<div class="form-hint">Enter the name of the plugin to install from the registry</div>
|
||||
function renderPluginTable(plugins) {
|
||||
const container = document.getElementById('pluginList');
|
||||
if (plugins.length === 0) {
|
||||
container.innerHTML = '<div class="empty-state"><div class="empty-state-icon">🔌</div><div class="empty-state-title">No plugins installed</div><div class="empty-state-desc">Install plugins from GitHub or create your own</div></div>';
|
||||
return;
|
||||
}
|
||||
container.innerHTML = `
|
||||
<div class="card">
|
||||
<div class="card-title">Installed Plugins (${plugins.length})</div>
|
||||
<div class="table-wrapper">
|
||||
<table>
|
||||
<thead><tr><th>Plugin</th><th>Version</th><th>Type</th><th>Source</th><th>Installed</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
${plugins.map(p => `
|
||||
<tr>
|
||||
<td><strong>${escapeHtml(p.name)}</strong></td>
|
||||
<td><code>${p.version || '1.0.0'}</code></td>
|
||||
<td><span class="badge badge-info">${p.type || 'skill'}</span></td>
|
||||
<td style="font-size:11px;max-width:200px;overflow:hidden;text-overflow:ellipsis" title="${escapeHtml(p.source || '')}">${escapeHtml(p.source || 'built-in')}</td>
|
||||
<td style="font-size:12px;color:var(--text-muted)">${formatDate(p.installed)}</td>
|
||||
<td>${p.source ? `<button class="btn btn-ghost" style="padding:4px 8px;font-size:11px" onclick="uninstallPlugin('${escapeHtml(p.name)}')">Remove</button>` : '—'}</td>
|
||||
</tr>
|
||||
`).join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
`, `
|
||||
<button class="btn btn-ghost" onclick="closeModal()">Cancel</button>
|
||||
<button class="btn btn-primary" onclick="installPlugin()">Install</button>
|
||||
`);
|
||||
`;
|
||||
}
|
||||
|
||||
async function installPlugin() {
|
||||
function renderIntegrations(integrations) {
|
||||
const container = document.getElementById('integrationsList');
|
||||
if (integrations.length === 0) {
|
||||
container.innerHTML = '<div style="font-size:12px;color:var(--text-muted)">No external apps connected yet</div>';
|
||||
return;
|
||||
}
|
||||
container.innerHTML = `<div style="display:flex;flex-direction:column;gap:8px">
|
||||
${integrations.map(i => `
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;padding:8px 12px;background:var(--bg-secondary);border-radius:6px">
|
||||
<div>
|
||||
<strong style="font-size:13px">${escapeHtml(i.name)}</strong>
|
||||
<span class="badge badge-info" style="margin-left:8px">${escapeHtml(i.type)}</span>
|
||||
<div style="font-size:11px;color:var(--text-muted);margin-top:2px">${escapeHtml(i.url)}</div>
|
||||
</div>
|
||||
<span style="font-size:11px;color:var(--accent)">● connected</span>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function showInstallPlugin() {
|
||||
document.getElementById('pluginRepoInput').focus();
|
||||
}
|
||||
|
||||
async function installPluginFromRepo() {
|
||||
const repoUrl = document.getElementById('pluginRepoInput').value.trim();
|
||||
const name = document.getElementById('pluginNameInput').value.trim();
|
||||
if (!name) { showToast('Plugin name required', 'warning'); return; }
|
||||
const statusEl = document.getElementById('installStatus');
|
||||
|
||||
if (!repoUrl) {
|
||||
statusEl.innerHTML = '<span style="color:#ef4444">Please enter a GitHub repo URL</span>';
|
||||
return;
|
||||
}
|
||||
|
||||
statusEl.innerHTML = '<span style="color:var(--accent)">Installing...</span>';
|
||||
try {
|
||||
const r = await api.installPlugin(name);
|
||||
closeModal();
|
||||
showToast(r.status === 'already_installed' ? 'Already installed' : `"${name}" installed`, r.status === 'already_installed' ? 'info' : 'success');
|
||||
const r = await api.installPlugin(name || repoUrl, repoUrl);
|
||||
statusEl.innerHTML = `<span style="color:#22c55e">✓ ${escapeHtml(r.plugin)} installed</span>`;
|
||||
document.getElementById('pluginRepoInput').value = '';
|
||||
document.getElementById('pluginNameInput').value = '';
|
||||
setTimeout(() => renderPlugins(), 2000);
|
||||
} catch (err) {
|
||||
statusEl.innerHTML = `<span style="color:#ef4444">Error: ${escapeHtml(err.message)}</span>`;
|
||||
}
|
||||
}
|
||||
|
||||
async function uninstallPlugin(name) {
|
||||
if (!confirm(`Remove "${name}"?`)) return;
|
||||
try {
|
||||
await api.uninstallPlugin(name);
|
||||
showToast(`${name} removed`, 'success');
|
||||
renderPlugins();
|
||||
} catch (err) {
|
||||
showToast(`Error: ${err.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function addIntegration() {
|
||||
const name = document.getElementById('integrationNameInput').value.trim();
|
||||
const url = document.getElementById('integrationUrlInput').value.trim();
|
||||
const type = document.getElementById('integrationType').value;
|
||||
const statusEl = document.getElementById('integrationStatus');
|
||||
|
||||
if (!name || !url) {
|
||||
statusEl.innerHTML = '<span style="color:#ef4444">Name and URL required</span>';
|
||||
return;
|
||||
}
|
||||
|
||||
statusEl.innerHTML = '<span style="color:var(--accent)">Connecting...</span>';
|
||||
try {
|
||||
const r = await api.addIntegration(name, url, type);
|
||||
statusEl.innerHTML = `<span style="color:#22c55e">✓ ${escapeHtml(r.name)} connected</span>`;
|
||||
document.getElementById('integrationNameInput').value = '';
|
||||
document.getElementById('integrationUrlInput').value = '';
|
||||
setTimeout(() => renderPlugins(), 2000);
|
||||
} catch (err) {
|
||||
statusEl.innerHTML = `<span style="color:#ef4444">Error: ${escapeHtml(err.message)}</span>`;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,16 @@
|
|||
// Per-agent display metadata (icons + role blurb). Built-ins + known custom agents.
|
||||
const ROUTER_AGENT_META = {
|
||||
opencode: { icon: '🔧', blurb: 'Code & DevOps' },
|
||||
hermes: { icon: '⚡', blurb: 'Memory & Scheduling' },
|
||||
gemini: { icon: '🧠', blurb: 'Research & Analysis' },
|
||||
jarvis: { icon: '🤖', blurb: 'Local-first AI / Deep Research' },
|
||||
kilocode: { icon: '💻', blurb: 'AI Coding Assistant' },
|
||||
codex: { icon: '🧩', blurb: 'Code Generation' },
|
||||
};
|
||||
function routerAgentMeta(name) {
|
||||
return ROUTER_AGENT_META[name] || { icon: '🤖', blurb: 'Custom agent' };
|
||||
}
|
||||
|
||||
async function renderSmartRouter() {
|
||||
const content = document.getElementById('pageContent');
|
||||
content.innerHTML = `
|
||||
|
|
@ -20,9 +33,7 @@ async function renderSmartRouter() {
|
|||
<label class="form-label">Route to Agent</label>
|
||||
<select class="form-select" id="routerAgentSelect">
|
||||
<option value="auto">🤖 Auto (AI suggests)</option>
|
||||
<option value="opencode">🔧 opencode (Code/DevOps)</option>
|
||||
<option value="hermes">⚡ Hermes (Memory/Scheduling)</option>
|
||||
<option value="gemini">🧠 Gemini CLI (Research/Analysis)</option>
|
||||
<!-- agent options injected by loadRouterAgents() -->
|
||||
</select>
|
||||
</div>
|
||||
<button class="btn btn-primary" onclick="suggestRouter()" style="margin-bottom:16px">🤖 Suggest Agent</button>
|
||||
|
|
@ -32,14 +43,51 @@ async function renderSmartRouter() {
|
|||
<div id="routerResult"></div>
|
||||
<div class="section-title" style="margin-top:20px">Routing Rules</div>
|
||||
<div class="card">
|
||||
<table>
|
||||
<table id="routerRulesTable">
|
||||
<tr><th>Agent</th><th>Best For</th><th>Keywords</th></tr>
|
||||
<tr><td><strong>🔧 opencode</strong></td><td>Code, DevOps, infra, git, file operations</td><td class="text-muted text-sm">code, deploy, git, terraform, docker, test, build, script</td></tr>
|
||||
<tr><td><strong>⚡ Hermes</strong></td><td>Memory, scheduling, messaging, skills</td><td class="text-muted text-sm">memory, schedule, cron, reminder, brain, plugin, backup</td></tr>
|
||||
<tr><td><strong>🧠 Gemini CLI</strong></td><td>Research, analysis, study, document, review</td><td class="text-muted text-sm">research, analyze, search, explain, study, learn, report</td></tr>
|
||||
<!-- rows injected by loadRouterAgents() -->
|
||||
</table>
|
||||
</div>
|
||||
`;
|
||||
await loadRouterAgents();
|
||||
}
|
||||
|
||||
// Pull the live agent list from the server so the dropdown + rules table
|
||||
// always reflect every registered agent (built-in AND custom).
|
||||
async function loadRouterAgents() {
|
||||
let agents = [];
|
||||
try {
|
||||
const data = await api.getAgents();
|
||||
agents = (data.agents || []).map(a => a.name);
|
||||
} catch (e) {
|
||||
// Fallback to known built-ins if the API is unreachable.
|
||||
agents = ['opencode', 'hermes', 'gemini'];
|
||||
}
|
||||
|
||||
const select = document.getElementById('routerAgentSelect');
|
||||
const table = document.getElementById('routerRulesTable');
|
||||
if (!select || !table) return;
|
||||
|
||||
// Populate dropdown (preserve the Auto option already in the DOM).
|
||||
agents.forEach(name => {
|
||||
const meta = routerAgentMeta(name);
|
||||
const opt = document.createElement('option');
|
||||
opt.value = name;
|
||||
opt.textContent = `${meta.icon} ${name} (${meta.blurb})`;
|
||||
select.appendChild(opt);
|
||||
});
|
||||
|
||||
// Populate routing-rules table.
|
||||
const rows = agents.map(name => {
|
||||
const meta = routerAgentMeta(name);
|
||||
const kw = (window._routerKeywords && window._routerKeywords[name]) || [];
|
||||
return `<tr>
|
||||
<td><strong>${meta.icon} ${name}</strong></td>
|
||||
<td>${meta.blurb}</td>
|
||||
<td class="text-muted text-sm">${kw.join(', ') || '—'}</td>
|
||||
</tr>`;
|
||||
}).join('');
|
||||
table.innerHTML = `<tr><th>Agent</th><th>Best For</th><th>Keywords</th></tr>${rows}`;
|
||||
}
|
||||
|
||||
async function suggestRouter() {
|
||||
|
|
@ -49,8 +97,13 @@ async function suggestRouter() {
|
|||
if (btn) { btn.disabled = true; btn.textContent = '⏳ Thinking...'; }
|
||||
try {
|
||||
const data = await api.suggestRouter(task);
|
||||
// Cache keyword map from scores for the rules table refresh.
|
||||
window._routerKeywords = data.scores ? Object.keys(data.scores).reduce((m, a) => m, {}) : {};
|
||||
const result = document.getElementById('routerResult');
|
||||
const agentIcons = { opencode: '🔧', hermes: '⚡', gemini: '🧠' };
|
||||
const agentIcons = {
|
||||
opencode: '🔧', hermes: '⚡', gemini: '🧠',
|
||||
jarvis: '🤖', kilocode: '💻', codex: '🧩'
|
||||
};
|
||||
const confidenceColors = { high: 'var(--green)', medium: 'var(--yellow)', low: 'var(--text-muted)' };
|
||||
result.innerHTML = `
|
||||
<div class="card" style="border-color:${confidenceColors[data.confidence] || 'var(--border)'};margin-bottom:12px">
|
||||
|
|
|
|||
|
|
@ -1,120 +1,150 @@
|
|||
let _termInstance = null;
|
||||
let _termSocket = null;
|
||||
|
||||
function loadXterm() {
|
||||
if (window.Terminal && window.FitAddon) return Promise.resolve();
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!document.querySelector('link[data-xterm-css]')) {
|
||||
const link = document.createElement('link');
|
||||
link.rel = 'stylesheet';
|
||||
link.href = 'https://cdn.jsdelivr.net/npm/xterm@5.3.0/css/xterm.css';
|
||||
link.setAttribute('data-xterm-css', '1');
|
||||
document.head.appendChild(link);
|
||||
}
|
||||
const script = document.createElement('script');
|
||||
script.src = 'https://cdn.jsdelivr.net/npm/xterm@5.3.0/lib/xterm.js';
|
||||
script.onload = () => {
|
||||
const fitScript = document.createElement('script');
|
||||
fitScript.src = 'https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.8.0/lib/xterm-addon-fit.js';
|
||||
fitScript.onload = () => resolve();
|
||||
fitScript.onerror = () => reject(new Error('Failed to load xterm-addon-fit'));
|
||||
document.body.appendChild(fitScript);
|
||||
};
|
||||
script.onerror = () => reject(new Error('Failed to load xterm.js'));
|
||||
document.body.appendChild(script);
|
||||
});
|
||||
}
|
||||
|
||||
function setTerminalStatus(kind, text) {
|
||||
const el = document.getElementById('terminalStatus');
|
||||
if (!el) return;
|
||||
el.textContent = text;
|
||||
el.className = `badge badge-${kind === 'online' ? 'success' : kind === 'offline' ? 'danger' : 'warning'}`;
|
||||
}
|
||||
|
||||
function closeTerminalSession() {
|
||||
if (window._terminalResizeHandler) {
|
||||
window.removeEventListener('resize', window._terminalResizeHandler);
|
||||
window._terminalResizeHandler = null;
|
||||
}
|
||||
if (_termSocket) {
|
||||
try { _termSocket.close(); } catch {}
|
||||
_termSocket = null;
|
||||
}
|
||||
if (_termInstance) {
|
||||
try { _termInstance.dispose(); } catch {}
|
||||
_termInstance = null;
|
||||
}
|
||||
}
|
||||
|
||||
async function renderTerminal() {
|
||||
const content = document.getElementById('pageContent');
|
||||
content.innerHTML = `
|
||||
<div class="page-header">
|
||||
<div class="page-header-left">
|
||||
<h1 class="page-title">Terminal</h1>
|
||||
<p class="page-subtitle">A real, interactive shell running on this machine</p>
|
||||
<p class="page-subtitle">Full bash shell in the browser — WSL environment</p>
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<span id="terminalStatus" class="badge badge-warning">Connecting…</span>
|
||||
<button class="btn btn-ghost" onclick="terminalReconnect()">↻ Reconnect</button>
|
||||
<button class="btn btn-ghost" onclick="terminalClear()">✕ Clear</button>
|
||||
<span id="terminalStatus" class="nav-badge" style="background:var(--yellow)">Connecting...</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="terminal-panel">
|
||||
<div id="xtermContainer" class="terminal-xterm-container"></div>
|
||||
<div class="terminal-container">
|
||||
<div id="terminal"></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
closeTerminalSession();
|
||||
|
||||
// Load xterm.js from CDN (with fallback)
|
||||
try {
|
||||
await loadXterm();
|
||||
await loadScript('https://cdn.jsdelivr.net/npm/xterm@5.3.0/lib/xterm.min.js');
|
||||
await loadScript('https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.8.0/lib/xterm-addon-fit.js');
|
||||
} catch (err) {
|
||||
document.getElementById('xtermContainer').innerHTML =
|
||||
`<div class="empty-state"><div class="empty-state-icon">⚠</div><div class="empty-state-title">Failed to load terminal library</div><div class="empty-state-desc">${escapeHtml(err.message || String(err))}</div></div>`;
|
||||
return;
|
||||
// Fallback to unpkg
|
||||
await loadScript('https://unpkg.com/xterm@5.3.0/lib/xterm.min.js');
|
||||
await loadScript('https://unpkg.com/xterm-addon-fit@0.8.0/lib/xterm-addon-fit.js');
|
||||
}
|
||||
|
||||
const term = new window.Terminal({
|
||||
// Verify Terminal and FitAddon are available
|
||||
if (typeof Terminal === 'undefined') {
|
||||
throw new Error('xterm.js failed to load from CDN');
|
||||
}
|
||||
|
||||
// Initialize terminal
|
||||
const term = new Terminal({
|
||||
cursorBlink: true,
|
||||
fontFamily: "'JetBrains Mono', 'Fira Code', 'SF Mono', monospace",
|
||||
fontSize: 13,
|
||||
theme: { background: '#0a0e14', foreground: '#c9d1d9' },
|
||||
fontSize: 14,
|
||||
fontFamily: "'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'Courier New', monospace",
|
||||
theme: {
|
||||
background: '#0d1117',
|
||||
foreground: '#c9d1d9',
|
||||
cursor: '#58a6ff',
|
||||
selection: '#264f78',
|
||||
black: '#0d1117',
|
||||
red: '#ff7b72',
|
||||
green: '#3fb950',
|
||||
yellow: '#d29922',
|
||||
blue: '#58a6ff',
|
||||
magenta: '#bc8cff',
|
||||
cyan: '#39d2c0',
|
||||
white: '#c9d1d9',
|
||||
},
|
||||
scrollback: 50000,
|
||||
allowProposedApi: true,
|
||||
convertEol: true,
|
||||
});
|
||||
const fitAddon = new window.FitAddon.FitAddon();
|
||||
|
||||
const FitAddonClass = window.FitAddon?.FitAddon || window.FitAddon;
|
||||
if (!FitAddonClass) {
|
||||
throw new Error('xterm FitAddon failed to load');
|
||||
}
|
||||
const fitAddon = new FitAddonClass();
|
||||
term.loadAddon(fitAddon);
|
||||
term.open(document.getElementById('xtermContainer'));
|
||||
term.open(document.getElementById('terminal'));
|
||||
fitAddon.fit();
|
||||
_termInstance = term;
|
||||
|
||||
const proto = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
const socket = new WebSocket(`${proto}//${window.location.host}/ws/terminal`);
|
||||
_termSocket = socket;
|
||||
window._agenticTerm = term;
|
||||
window._agenticFit = fitAddon;
|
||||
|
||||
socket.onopen = () => {
|
||||
setTerminalStatus('online', 'Connected');
|
||||
fitAddon.fit();
|
||||
socket.send(JSON.stringify({ type: 'resize', cols: term.cols, rows: term.rows }));
|
||||
term.focus();
|
||||
};
|
||||
socket.onmessage = (event) => {
|
||||
try {
|
||||
const msg = JSON.parse(event.data);
|
||||
if (msg.type === 'output') term.write(msg.data);
|
||||
} catch {}
|
||||
};
|
||||
socket.onclose = () => setTerminalStatus('offline', 'Disconnected');
|
||||
socket.onerror = () => setTerminalStatus('offline', 'Connection error');
|
||||
// Connect WebSocket
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
const wsPort = 8082;
|
||||
const wsUrl = `${protocol}//${window.location.hostname}:${wsPort}`;
|
||||
const statusEl = document.getElementById('terminalStatus');
|
||||
|
||||
term.onData((data) => {
|
||||
if (socket.readyState === WebSocket.OPEN) socket.send(JSON.stringify({ type: 'input', data }));
|
||||
});
|
||||
try {
|
||||
const ws = new WebSocket(wsUrl);
|
||||
window._agenticWs = ws;
|
||||
|
||||
const resizeHandler = () => {
|
||||
fitAddon.fit();
|
||||
if (socket.readyState === WebSocket.OPEN) socket.send(JSON.stringify({ type: 'resize', cols: term.cols, rows: term.rows }));
|
||||
};
|
||||
window.addEventListener('resize', resizeHandler);
|
||||
window._terminalResizeHandler = resizeHandler;
|
||||
ws.onopen = () => {
|
||||
statusEl.textContent = 'Connected';
|
||||
statusEl.style.background = 'var(--green)';
|
||||
term.writeln('\x1b[32m✓ Connected to Agentic OS terminal\x1b[0m');
|
||||
term.writeln(`\x1b[90m WSL • ${navigator.userAgent.includes('Windows') ? 'Windows browser → WSL shell' : 'Linux shell'}\x1b[0m\r\n`);
|
||||
ws.send(JSON.stringify({ action: 'resize', cols: term.cols, rows: term.rows }));
|
||||
};
|
||||
|
||||
window.addEventListener('hashchange', closeTerminalSession, { once: true });
|
||||
ws.onmessage = (event) => {
|
||||
if (typeof event.data === 'string') {
|
||||
term.write(event.data);
|
||||
}
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
statusEl.textContent = 'Disconnected';
|
||||
statusEl.style.background = 'var(--red)';
|
||||
term.writeln('\r\n\x1b[31m✕ Connection closed\x1b[0m');
|
||||
};
|
||||
|
||||
ws.onerror = () => {
|
||||
statusEl.textContent = 'Error';
|
||||
statusEl.style.background = 'var(--red)';
|
||||
};
|
||||
|
||||
// Send input
|
||||
term.onData((data) => {
|
||||
if (ws.readyState === WebSocket.OPEN) {
|
||||
ws.send(JSON.stringify({ action: 'input', data: data }));
|
||||
}
|
||||
});
|
||||
|
||||
// Resize handler with debounce
|
||||
let resizeTimeout;
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
clearTimeout(resizeTimeout);
|
||||
resizeTimeout = setTimeout(() => {
|
||||
fitAddon.fit();
|
||||
if (ws.readyState === WebSocket.OPEN) {
|
||||
ws.send(JSON.stringify({ action: 'resize', cols: term.cols, rows: term.rows }));
|
||||
}
|
||||
}, 100);
|
||||
});
|
||||
resizeObserver.observe(document.getElementById('terminal'));
|
||||
window._agenticResize = resizeObserver;
|
||||
|
||||
} catch (err) {
|
||||
statusEl.textContent = 'Failed';
|
||||
statusEl.style.background = 'var(--red)';
|
||||
term.writeln(`\x1b[31mError: ${err.message}\x1b[0m`);
|
||||
}
|
||||
|
||||
term.focus();
|
||||
}
|
||||
|
||||
function terminalReconnect() {
|
||||
if (window._agenticWs) window._agenticWs.close();
|
||||
if (window._agenticResize) window._agenticResize.disconnect();
|
||||
renderTerminal();
|
||||
}
|
||||
|
||||
function terminalClear() {
|
||||
if (window._agenticTerm) {
|
||||
window._agenticTerm.clear();
|
||||
// Also send clear escape sequence to PTY
|
||||
if (window._agenticWs && window._agenticWs.readyState === WebSocket.OPEN) {
|
||||
window._agenticWs.send(JSON.stringify({ action: 'input', data: '\x0c' }));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,163 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Agentic-OS :: Localhost Agent-Time Monitor Server
|
||||
-------------------------------------------------
|
||||
Stdlib-only HTTP server (no pip deps) that serves the project folder on
|
||||
localhost and exposes two action endpoints:
|
||||
|
||||
GET / -> redirects to the monitor dashboard
|
||||
GET /dashboard/pages/agent-time-monitor.html -> the dashboard
|
||||
GET /agent-time.json -> computed total-agent-time report
|
||||
GET /graphify-out/graph.html -> graphify code knowledge graph
|
||||
POST /recompute -> re-run the time analyzer, return summary
|
||||
POST /sync-mnemoverse -> push total time to Mnemoverse (graceful)
|
||||
|
||||
Run: python3 dashboard/serve_monitor.py [--port 8765]
|
||||
Then open: http://localhost:8765/dashboard/pages/agent-time-monitor.html
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
|
||||
from functools import partial
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # agentic-os/
|
||||
ANALYZER = os.path.join(ROOT, "scripts", "analyze_agent_time.py")
|
||||
AGENT_TIME = os.path.join(ROOT, "data", "agent-time.json")
|
||||
MNEMOVERSE_URL = "https://core.mnemoverse.com/api/v1/memory/write"
|
||||
DASHBOARD = "/dashboard/pages/agent-time-monitor.html"
|
||||
DEFAULT_PORT = int(os.environ.get("MONITOR_PORT", "8765"))
|
||||
|
||||
|
||||
def run_analyzer():
|
||||
"""Recompute agent-time.json. Returns (ok, summary_dict)."""
|
||||
try:
|
||||
proc = subprocess.run(
|
||||
[sys.executable, ANALYZER],
|
||||
cwd=ROOT, capture_output=True, text=True, timeout=120,
|
||||
)
|
||||
if proc.returncode != 0:
|
||||
return False, {"error": proc.stderr.strip() or "analyzer exited non-zero"}
|
||||
with open(AGENT_TIME, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
return True, {
|
||||
"total_seconds": data["total_seconds"],
|
||||
"total_human": data["total_human"],
|
||||
"agents": len(data.get("agents", {})),
|
||||
"events": data.get("event_count", 0),
|
||||
}
|
||||
except Exception as e:
|
||||
return False, {"error": str(e)}
|
||||
|
||||
|
||||
def sync_mnemoverse(api_key=None):
|
||||
"""Write the total agent time to Mnemoverse. Graceful on missing key/offline."""
|
||||
api_key = api_key or os.environ.get("MNEMOVERSE_API_KEY", "")
|
||||
if not api_key:
|
||||
return False, {
|
||||
"error": "no MNEMOVERSE_API_KEY set",
|
||||
"hint": "export MNEMOVERSE_API_KEY=mk_live_... then click again",
|
||||
}
|
||||
try:
|
||||
with open(AGENT_TIME, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
except Exception as e:
|
||||
return False, {"error": "cannot read agent-time.json: " + str(e)}
|
||||
|
||||
content = (
|
||||
f"Agentic-OS total AI-agent time on project: {data['total_human']} "
|
||||
f"across {len(data.get('agents', {}))} agents "
|
||||
f"({data.get('event_count', 0)} logged events). "
|
||||
f"Method: {data.get('method')}. Last computed {data.get('generated_at')}."
|
||||
)
|
||||
payload = json.dumps({
|
||||
"content": content,
|
||||
"concepts": ["agentic-os", "agent-time", "agentic-metrics", "ai-agent-usage"],
|
||||
"domain": "agentic-os",
|
||||
}).encode("utf-8")
|
||||
req = urllib.request.Request(
|
||||
MNEMOVERSE_URL, data=payload, method="POST",
|
||||
headers={"Content-Type": "application/json", "X-Api-Key": api_key},
|
||||
)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=20) as resp:
|
||||
body = resp.read().decode("utf-8", "replace")
|
||||
try:
|
||||
j = json.loads(body)
|
||||
except Exception:
|
||||
j = {"raw": body[:200]}
|
||||
return True, {"message": "memory written to Mnemoverse", "response": j}
|
||||
except urllib.error.HTTPError as e:
|
||||
return False, {"error": f"HTTP {e.code}: {e.read().decode('utf-8','replace')[:200]}"}
|
||||
except urllib.error.URLError as e:
|
||||
return False, {"error": "network error: " + str(e.reason),
|
||||
"hint": "Mnemoverse unreachable from this host"}
|
||||
|
||||
|
||||
class Handler(SimpleHTTPRequestHandler):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, directory=ROOT, **kwargs)
|
||||
|
||||
def _send_json(self, obj, code=200):
|
||||
body = json.dumps(obj).encode("utf-8")
|
||||
self.send_response(code)
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.send_header("Content-Length", str(len(body)))
|
||||
self.send_header("Access-Control-Allow-Origin", "*")
|
||||
self.end_headers()
|
||||
self.wfile.write(body)
|
||||
|
||||
def do_GET(self):
|
||||
if self.path in ("/", "/index.html"):
|
||||
self.send_response(302)
|
||||
self.send_header("Location", DASHBOARD)
|
||||
self.end_headers()
|
||||
return
|
||||
# Default: serve files from ROOT (html, json, graph.html, etc.)
|
||||
super().do_GET()
|
||||
|
||||
def do_POST(self):
|
||||
if self.path == "/recompute":
|
||||
ok, summary = run_analyzer()
|
||||
self._send_json({"ok": ok, **summary}, 200 if ok else 500)
|
||||
return
|
||||
if self.path == "/sync-mnemoverse":
|
||||
ok, result = sync_mnemoverse()
|
||||
self._send_json({"ok": ok, **result}, 200 if ok else 200)
|
||||
return
|
||||
self._send_json({"ok": False, "error": "unknown endpoint"}, 404)
|
||||
|
||||
def log_message(self, fmt, *args):
|
||||
sys.stderr.write("[monitor] " + (fmt % args) + "\n")
|
||||
|
||||
|
||||
def main():
|
||||
import argparse
|
||||
p = argparse.ArgumentParser()
|
||||
p.add_argument("--port", type=int, default=DEFAULT_PORT)
|
||||
p.add_argument("--host", default="127.0.0.1")
|
||||
args = p.parse_args()
|
||||
|
||||
# ensure a fresh agent-time.json exists
|
||||
if not os.path.exists(AGENT_TIME):
|
||||
ok, s = run_analyzer()
|
||||
print(("recompute ok" if ok else "recompute failed: " + str(s)))
|
||||
|
||||
httpd = ThreadingHTTPServer((args.host, args.port), Handler)
|
||||
url = f"http://{args.host}:{args.port}{DASHBOARD}"
|
||||
print("Agentic-OS Agent-Time Monitor")
|
||||
print(f" serving : {ROOT}")
|
||||
print(f" dashboard: {url}")
|
||||
print(f" mnemoverse: {'ENABLED (key set)' if os.environ.get('MNEMOVERSE_API_KEY') else 'disabled (set MNEMOVERSE_API_KEY to enable sync)'}")
|
||||
print(" ctrl-c to stop")
|
||||
try:
|
||||
httpd.serve_forever()
|
||||
except KeyboardInterrupt:
|
||||
print("\nstopped.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
#!/usr/bin/env bash
|
||||
# Start the Agentic-OS Agent-Time Monitor on localhost.
|
||||
# Usage: ./dashboard/start-monitor.sh [port]
|
||||
# Then open: http://localhost:<port>/dashboard/pages/agent-time-monitor.html
|
||||
set -e
|
||||
cd "$(dirname "$0")/.."
|
||||
PORT="${1:-8765}"
|
||||
if [ -d venv ]; then . venv/bin/activate; fi
|
||||
echo "Starting monitor on http://127.0.0.1:${PORT} ..."
|
||||
exec python3 dashboard/serve_monitor.py --port "${PORT}"
|
||||
|
|
@ -1,3 +1,74 @@
|
|||
/* ─── Terminal ─────────────────────────────────────────────────── */
|
||||
.terminal-container {
|
||||
background: #0d1117;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
height: calc(100vh - 180px);
|
||||
min-height: 400px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#terminal {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
#terminal .xterm {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#terminal .xterm-viewport {
|
||||
overflow-y: auto !important;
|
||||
}
|
||||
|
||||
/* ─── Agent Cards ──────────────────────────────────────────────── */
|
||||
.agent-card {
|
||||
transition: border-color 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
.agent-card:hover {
|
||||
border-color: var(--accent);
|
||||
box-shadow: 0 0 0 1px var(--accent-glow);
|
||||
}
|
||||
.agent-card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.agent-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
.agent-table th {
|
||||
text-align: left;
|
||||
padding: 10px 12px;
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-muted);
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.agent-table td {
|
||||
padding: 10px 12px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
font-size: 13px;
|
||||
}
|
||||
.agent-table tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
.agent-table tr:hover td {
|
||||
background: var(--bg-card-hover);
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
padding: 4px 10px;
|
||||
font-size: 11px;
|
||||
}
|
||||
.mt-4 {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
:root {
|
||||
--bg-primary: #0d1117;
|
||||
--bg-secondary: #151b24;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,20 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head><title>Agentic OS Test</title></head>
|
||||
<body>
|
||||
<h1>Connection Test</h1>
|
||||
<div id="status">Testing...</div>
|
||||
<div id="result"></div>
|
||||
<script>
|
||||
fetch('/api/status')
|
||||
.then(r => r.json())
|
||||
.then(d => {
|
||||
document.getElementById('status').textContent = 'SUCCESS: API reachable';
|
||||
document.getElementById('result').textContent = JSON.stringify(d, null, 2);
|
||||
})
|
||||
.catch(e => {
|
||||
document.getElementById('status').textContent = 'FAILED: ' + e.message;
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -111,6 +111,7 @@ function renderSkeleton(count = 3) {
|
|||
|
||||
const PAGE_TITLES = {
|
||||
dashboard: { title: 'Dashboard', breadcrumb: 'Overview' },
|
||||
terminal: { title: 'Terminal', breadcrumb: 'Browser-based shell' },
|
||||
skills: { title: 'Skills Hub', breadcrumb: 'Browse & execute skills' },
|
||||
memory: { title: 'Memory', breadcrumb: 'Shared brain context' },
|
||||
scheduler: { title: 'Scheduler', breadcrumb: 'Automated workflows' },
|
||||
|
|
@ -131,4 +132,5 @@ const PAGE_TITLES = {
|
|||
'smart-router': { title: 'Smart Router', breadcrumb: 'Task routing intelligence' },
|
||||
'learning-analytics': { title: 'Learning Analytics', breadcrumb: 'Skill improvement tracking' },
|
||||
'session-replay': { title: 'Session Replay', breadcrumb: 'Conversation history playback' },
|
||||
'agent-time': { title: 'Agent Time', breadcrumb: 'Total agent effort on this project' },
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,47 @@
|
|||
{
|
||||
"jarvis": {
|
||||
"name": "jarvis",
|
||||
"display_name": "OpenJarvis",
|
||||
"description": "Local-first personal AI: deep research, knowledge, memory, general reasoning",
|
||||
"binary": "jarvis-ask",
|
||||
"type": "cli",
|
||||
"run_args": [
|
||||
"jarvis-ask",
|
||||
"{message}"
|
||||
],
|
||||
"check_type": "binary",
|
||||
"timeout": 120,
|
||||
"builtin": false,
|
||||
"api_method": "POST"
|
||||
},
|
||||
"kilocode": {
|
||||
"name": "kilocode",
|
||||
"display_name": "Kilo Code",
|
||||
"description": "AI coding assistant by Kilo (linux binary via wrapper)",
|
||||
"binary": "kilocode",
|
||||
"type": "cli",
|
||||
"run_args": [
|
||||
"kilocode",
|
||||
"{message}"
|
||||
],
|
||||
"check_type": "binary",
|
||||
"timeout": 120,
|
||||
"builtin": false,
|
||||
"api_method": "POST"
|
||||
},
|
||||
"codex": {
|
||||
"name": "codex",
|
||||
"display_name": "OpenAI Codex",
|
||||
"description": "OpenAI Codex coding/execution agent",
|
||||
"binary": "codex",
|
||||
"type": "cli",
|
||||
"run_args": [
|
||||
"codex",
|
||||
"{message}"
|
||||
],
|
||||
"check_type": "binary",
|
||||
"timeout": 300,
|
||||
"builtin": false,
|
||||
"api_method": "POST"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
{}
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
{
|
||||
"project": "agentic-os",
|
||||
"generated_at": "2026-07-25T18:27:15.982570+00:00",
|
||||
"method": "session-gap estimate (GAP=1800s, TAIL=120s)",
|
||||
"graph": {
|
||||
"nodes": 321,
|
||||
"edges": 517,
|
||||
"communities": 41,
|
||||
"file": "graphify-out/graph.json"
|
||||
},
|
||||
"gap_seconds": 1800,
|
||||
"tail_seconds": 120,
|
||||
"total_seconds": 20568,
|
||||
"total_human": "5h 42m 48s",
|
||||
"agents": {
|
||||
"opencode": {
|
||||
"sessions": 8,
|
||||
"touches": 42,
|
||||
"total_seconds": 4566,
|
||||
"first_seen": "2026-06-23T11:17:40.771655+00:00",
|
||||
"last_seen": "2026-07-25T17:40:45.570834+00:00"
|
||||
},
|
||||
"system": {
|
||||
"sessions": 12,
|
||||
"touches": 114,
|
||||
"total_seconds": 7374,
|
||||
"first_seen": "2026-06-25T17:58:22.902546+00:00",
|
||||
"last_seen": "2026-07-25T17:10:23.236024+00:00"
|
||||
},
|
||||
"hermes": {
|
||||
"sessions": 9,
|
||||
"touches": 36,
|
||||
"total_seconds": 6808,
|
||||
"first_seen": "2026-06-26T01:25:01.320339+00:00",
|
||||
"last_seen": "2026-07-25T17:42:17.726478+00:00"
|
||||
},
|
||||
"gemini": {
|
||||
"sessions": 1,
|
||||
"touches": 3,
|
||||
"total_seconds": 220,
|
||||
"first_seen": "2026-06-28T17:26:01.078648+00:00",
|
||||
"last_seen": "2026-06-28T17:27:41.105686+00:00"
|
||||
},
|
||||
"test_claude": {
|
||||
"sessions": 1,
|
||||
"touches": 2,
|
||||
"total_seconds": 129,
|
||||
"first_seen": "2026-06-28T17:47:56.773101+00:00",
|
||||
"last_seen": "2026-06-28T17:48:05.984878+00:00"
|
||||
},
|
||||
"jarvis": {
|
||||
"sessions": 2,
|
||||
"touches": 3,
|
||||
"total_seconds": 240,
|
||||
"first_seen": "2026-06-28T20:44:32.727658+00:00",
|
||||
"last_seen": "2026-07-17T17:30:21.986571+00:00"
|
||||
},
|
||||
"kilocode": {
|
||||
"sessions": 2,
|
||||
"touches": 3,
|
||||
"total_seconds": 240,
|
||||
"first_seen": "2026-06-28T20:44:32.730628+00:00",
|
||||
"last_seen": "2026-07-17T17:30:21.988722+00:00"
|
||||
},
|
||||
"test": {
|
||||
"sessions": 1,
|
||||
"touches": 1,
|
||||
"total_seconds": 120,
|
||||
"first_seen": "2026-06-28T21:39:14.911119+00:00",
|
||||
"last_seen": "2026-06-28T21:39:14.911119+00:00"
|
||||
},
|
||||
"codex": {
|
||||
"sessions": 1,
|
||||
"touches": 3,
|
||||
"total_seconds": 871,
|
||||
"first_seen": "2026-07-17T17:18:53.396385+00:00",
|
||||
"last_seen": "2026-07-17T17:31:24.434628+00:00"
|
||||
}
|
||||
},
|
||||
"event_count": 207,
|
||||
"first_seen": "2026-06-23T11:17:40.771655+00:00",
|
||||
"last_seen": "2026-07-25T17:42:17.726478+00:00"
|
||||
}
|
||||
|
|
@ -1,35 +1,13 @@
|
|||
[
|
||||
{
|
||||
"id": "215a1f6a",
|
||||
"title": "Waste log",
|
||||
"description": "I have a couple of files that work as an applicate for a catering kitchen waste log. I need it to be local if possible and save local as well. I need it created fully functioning.",
|
||||
"category": "development",
|
||||
"target_date": "2026-07-07",
|
||||
"id": "2c4b22ac",
|
||||
"title": "Push everything in the Kanban board down the line",
|
||||
"description": "It involves different skills to get different things done.",
|
||||
"category": "general",
|
||||
"target_date": "2026-06-28",
|
||||
"status": "active",
|
||||
"progress": 0,
|
||||
"created": "2026-07-07T10:59:57.991678+00:00",
|
||||
"updated": "2026-07-07T10:59:57.991691+00:00"
|
||||
},
|
||||
{
|
||||
"id": "ac785f05",
|
||||
"title": "Pendleton-comms-live",
|
||||
"description": "I need to finish my pendleton walkie talkie app before whiskey fest so my team can talk. It needs to be an android native application.",
|
||||
"category": "development",
|
||||
"target_date": "2026-07-08",
|
||||
"status": "active",
|
||||
"progress": 0,
|
||||
"created": "2026-07-07T11:02:12.398717+00:00",
|
||||
"updated": "2026-07-07T11:02:12.398731+00:00"
|
||||
},
|
||||
{
|
||||
"id": "a0f84134",
|
||||
"title": "AUdit",
|
||||
"description": "Run a dependency audit",
|
||||
"category": "development",
|
||||
"target_date": "2026-07-19",
|
||||
"status": "active",
|
||||
"progress": 0,
|
||||
"created": "2026-07-19T07:52:31.175924+00:00",
|
||||
"updated": "2026-07-19T07:52:31.175940+00:00"
|
||||
"progress": 50,
|
||||
"created": "2026-06-28T21:48:06.763421+00:00",
|
||||
"updated": "2026-06-28T21:48:13.383386+00:00"
|
||||
}
|
||||
]
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"integrations": [
|
||||
{
|
||||
"name": "test-webhook",
|
||||
"url": "https://example.com/hook",
|
||||
"type": "webhook",
|
||||
"added": "2026-06-28T17:18:11.253089+00:00",
|
||||
"status": "active"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"id": "1314316c",
|
||||
"title": "AT",
|
||||
"body": "",
|
||||
"status": "triage",
|
||||
"priority": "medium",
|
||||
"assignee": "",
|
||||
"comments": [],
|
||||
"links": [],
|
||||
"created": "2026-07-17T07:51:37.997814+00:00",
|
||||
"updated": "2026-07-17T16:02:48.789421+00:00"
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"id": "7a250df7",
|
||||
"title": "Login to get gemini running",
|
||||
"body": "login to google so gemini will be running in this os",
|
||||
"status": "done",
|
||||
"priority": "high",
|
||||
"assignee": "hermes",
|
||||
"comments": [],
|
||||
"links": [],
|
||||
"created": "2026-06-26T01:30:00.162737+00:00",
|
||||
"updated": "2026-07-17T16:54:35.405173+00:00"
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"jarvis": [
|
||||
"jarvis",
|
||||
"research",
|
||||
"deep-research",
|
||||
"knowledge",
|
||||
"memory",
|
||||
"reasoning",
|
||||
"local"
|
||||
],
|
||||
"kilocode": [
|
||||
"kilo",
|
||||
"kilocode",
|
||||
"code",
|
||||
"coding",
|
||||
"programming",
|
||||
"refactor",
|
||||
"implement"
|
||||
],
|
||||
"codex": [
|
||||
"codex",
|
||||
"code",
|
||||
"debug",
|
||||
"test",
|
||||
"build",
|
||||
"fix",
|
||||
"implement"
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
# Agentic-OS memory layer + Graphify — honest status
|
||||
|
||||
## Can Graphify BE the memory layer?
|
||||
**Partially, and not as a replacement for the write path.**
|
||||
|
||||
What Graphify is: a tool that turns a *codebase* into a knowledge graph
|
||||
(nodes = code symbols/modules, edges = relationships) and lets you
|
||||
visualize / query it. It is a **read/visualize/query layer over your
|
||||
files** — it does NOT store or write memory. The OS's actual
|
||||
memory write path stays `brain/memory.md`, `append_audit()`,
|
||||
`data/*.json`, etc.
|
||||
|
||||
## What we verified (2026-07-25)
|
||||
- `graphify . --code-only` over the whole project → **321 nodes, 517
|
||||
edges, 41 communities** (graphify-out/graph.json + graph.html).
|
||||
This graphs the *code* (server.py, skills' .py, etc.).
|
||||
- `graphify brain skills --code-only` → **0 nodes**. The memory
|
||||
folders are almost entirely **Markdown**, which `--code-only`
|
||||
skips. So the memory *content* is invisible to a code-only build.
|
||||
- To graph the *memory docs* (markdown notes), Graphify needs an
|
||||
LLM key for semantic extraction (GEMINI_API_KEY / GOOGLE_API_KEY
|
||||
/ ANTHROPIC_API_KEY / OPENAI_API_KEY / DEEPSEEK_API_KEY /
|
||||
MOONSHOT_API_KEY). With a key:
|
||||
`graphify brain skills` (no --code-only) will build a graph that
|
||||
includes your notes as nodes. Without a key it errors out.
|
||||
|
||||
## Recommended setup (works today, upgrades with a key)
|
||||
1. Build the code graph (no key needed):
|
||||
cd ~/agentic-os && graphify . --code-only
|
||||
→ live in the dashboard at http://localhost:8080/graphify-out/graph.html
|
||||
and the "Agent Time" page's "Open interactive graph" link.
|
||||
2. (Optional, with a key) Build the memory-inclusive graph:
|
||||
export GEMINI_API_KEY=...
|
||||
graphify brain skills data # docs + code, semantic
|
||||
This adds your memory notes as queryable nodes.
|
||||
3. Expose it to the OS agents as a query tool (MCP or HTTP):
|
||||
# HTTP (team/shared):
|
||||
python -m graphify.serve graphify-out/graph.json --transport http --port 8081
|
||||
# or stdio MCP for a single agent:
|
||||
python -m graphify.serve graphify-out/graph.json
|
||||
Then point the agent/MCP client at it. Agents can ASK the graph
|
||||
("what connects memory-consolidation to audit?", "show the
|
||||
agentic-os data flow") — a retrieval aid, not a memory writer.
|
||||
|
||||
## Bottom line
|
||||
- ✅ Use Graphify as a **visualization + semantic-query layer** for the
|
||||
OS's memory/code. Great for "what's in my brain and how does it
|
||||
connect."
|
||||
- ❌ Do NOT treat it as the memory store. Writing memory still goes
|
||||
through the existing `brain/` + `audit` + `data/` files. Graphify
|
||||
reads those; it never writes them.
|
||||
- ⚠️ Memory-note graphing requires an LLM key; code graphing does not.
|
||||
|
||||
## Files
|
||||
- graphify-out/ — built code graph (graph.json, graph.html, GRAPH_REPORT.md)
|
||||
- scripts/analyze_agent_time.py — agent-time estimator (the dashboard's data source)
|
||||
- dashboard/.../agent-time.js — integrated "Agent Time" page (port 8080)
|
||||
- dashboard/serve_monitor.py — standalone monitor (kept, unused by integration)
|
||||
|
|
@ -0,0 +1,636 @@
|
|||
# Agentic OS — Documentation
|
||||
|
||||
> Multi-agent orchestration platform — FastAPI backend + vanilla JS SPA dashboard
|
||||
> Version: 1.3.0 | Last updated: 2026-06-28
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Quick Start](#quick-start)
|
||||
2. [Architecture Overview](#architecture-overview)
|
||||
3. [Dashboard Pages](#dashboard-pages)
|
||||
4. [Web Terminal](#web-terminal)
|
||||
5. [Agent System](#agent-system)
|
||||
6. [API Reference](#api-reference)
|
||||
7. [Skills](#skills)
|
||||
8. [Scheduler](#scheduler)
|
||||
9. [Configuration](#configuration)
|
||||
10. [Development Guide](#development-guide)
|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Start the server
|
||||
cd ~/agentic-os
|
||||
./start.sh
|
||||
|
||||
# Or with browser auto-open
|
||||
./start.sh --open
|
||||
|
||||
# Check status
|
||||
./start.sh --status
|
||||
|
||||
# Stop
|
||||
./start.sh --stop
|
||||
```
|
||||
|
||||
**URLs:**
|
||||
- Dashboard: `http://127.0.0.1:8081`
|
||||
- Web Terminal: `http://127.0.0.1:8082` (WebSocket)
|
||||
- API Base: `http://127.0.0.1:8081/api/`
|
||||
|
||||
---
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ AGENTIC OS DASHBOARD │
|
||||
│ (FastAPI + Vanilla JS SPA) │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌─────────┐ ┌──────────┐ ┌───────────┐ ┌───────────┐ │
|
||||
│ │ opencode │ │ Hermes │ │ Gemini │ │ Custom │ │
|
||||
│ │ (Code) │ │ (Memory) │ │(Research) │ │ Agents │ │
|
||||
│ └────┬─────┘ └─────┬─────┘ └─────┬─────┘ └─────┬─────┘ │
|
||||
│ │ │ │ │ │
|
||||
│ ─────┴──────────────┴──────────────┴──────────────┴──────── │
|
||||
│ Agent Router │
|
||||
│ ─────────────────────────────────────────────────────────── │
|
||||
│ FastAPI Backend │
|
||||
│ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ ┌──────────┐ │
|
||||
│ │ Brain │ │Skills │ │Kanban │ │Journal │ │ Terminal │ │
|
||||
│ │ (files)│ │(agents)│ │(JSON) │ │(files) │ │ (PTY) │ │
|
||||
│ └────────┘ └────────┘ └────────┘ └────────┘ └──────────┘ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Tech Stack:**
|
||||
- Backend: Python 3.11+, FastAPI, Uvicorn
|
||||
- Frontend: Vanilla JS, Chart.js, xterm.js
|
||||
- Terminal: WebSocket + PTY (pseudo-terminal)
|
||||
- Data: JSON files (no database required)
|
||||
- Agents: CLI binaries (opencode, hermes, gemini, + custom)
|
||||
|
||||
---
|
||||
|
||||
## Dashboard Pages
|
||||
|
||||
| Page | Route | Description |
|
||||
|------|-------|-------------|
|
||||
| Dashboard | `#dashboard` | System overview, agent status, recent activity |
|
||||
| AI Chat | `#chat` | Multi-agent chat terminal |
|
||||
| Terminal | `#terminal` | Browser-based bash shell (xterm.js) |
|
||||
| Skills | `#skills` | Browse and execute 17+ skills |
|
||||
| Memory | `#memory` | Shared brain context files |
|
||||
| Scheduler | `#scheduler` | Cron job management |
|
||||
| Audit | `#audit` | System activity trail |
|
||||
| Kanban | `#kanban` | 6-column task board |
|
||||
| Goals | `#goals` | Project targets with progress tracking |
|
||||
| Journal | `#journal` | Daily entries with search |
|
||||
| Agent Health | `#agent-health` | Agent monitoring + registration |
|
||||
| Smart Router | `#smart-router` | Task routing intelligence |
|
||||
| Learning Analytics | `#learning-analytics` | Skill eval scores and trends |
|
||||
| Session Replay | `#session-replay` | Conversation history playback |
|
||||
| Cost Analytics | `#cost` | Token usage and spending |
|
||||
| Plugins | `#plugins` | Plugin registry management |
|
||||
| Backups | `#backups` | Backup/restore system |
|
||||
| Prompts | `#prompts` | Reusable prompt templates |
|
||||
| Standards | `#standards` | Project conventions |
|
||||
| Settings | `#settings` | System configuration |
|
||||
| Setup Wizard | `#setup-wizard` | Guided configuration |
|
||||
|
||||
---
|
||||
|
||||
## Web Terminal
|
||||
|
||||
The terminal provides a full bash shell in the browser using xterm.js connected to a PTY via WebSocket.
|
||||
|
||||
**How it works:**
|
||||
1. Server forks a PTY (`pty.fork()`) running `/bin/bash`
|
||||
2. WebSocket server on port 8082 relays I/O between browser and PTY
|
||||
3. xterm.js renders the terminal with 256-color support
|
||||
4. Resize events propagate via `ioctl(TIOCSWINSZ)`
|
||||
|
||||
**Features:**
|
||||
- Full bash shell with tab completion, history, colors
|
||||
- Automatic terminal resizing
|
||||
- 10,000 line scrollback
|
||||
- Reconnect button if connection drops
|
||||
- Dark theme matching dashboard
|
||||
|
||||
**Security:** The terminal runs as the server process user. Only accessible from localhost by default.
|
||||
|
||||
---
|
||||
|
||||
## Agent System
|
||||
|
||||
### Built-in Agents
|
||||
|
||||
| Agent | Binary | Role | Check Method |
|
||||
|-------|--------|------|--------------|
|
||||
| OpenCode | `opencode` | Code generation, DevOps, file ops | Binary in PATH |
|
||||
| Hermes Agent | `hermes` | Memory, scheduling, coordination | Binary in PATH |
|
||||
| Gemini CLI | `gemini` | Research, analysis, multi-modal | Binary + OAuth file |
|
||||
|
||||
### Agent Registry
|
||||
|
||||
Agents are defined in `data/agent-registry.json` (custom agents) with built-in defaults in `server.py`.
|
||||
|
||||
**Agent configuration schema:**
|
||||
```json
|
||||
{
|
||||
"name": "agent_id",
|
||||
"display_name": "Human Readable Name",
|
||||
"description": "What this agent does",
|
||||
"binary": "cli-binary-name",
|
||||
"type": "cli",
|
||||
"run_args": ["binary", "arg1", "{message}"],
|
||||
"check_type": "binary",
|
||||
"timeout": 60,
|
||||
"builtin": false
|
||||
}
|
||||
```
|
||||
|
||||
**Check types:**
|
||||
- `binary` — checks if binary exists in PATH
|
||||
- `oauth_file` — checks if OAuth credential file exists
|
||||
- `http` — HTTP health check against `health_url`
|
||||
- `custom` — runs `check_command`, online if exit code 0
|
||||
|
||||
**Agent types:**
|
||||
- `cli` — executes via subprocess with `{message}` substitution
|
||||
- `http` — POSTs to `api_url` with `{"message": "..."}`
|
||||
- `mcp` — Model Context Protocol (stub, not yet implemented)
|
||||
|
||||
### Adding Custom Agents
|
||||
|
||||
Via the dashboard: **Agent Health** → **+ Add Agent**
|
||||
|
||||
Or via API:
|
||||
```bash
|
||||
curl -X POST http://127.0.0.1:8081/api/agents/register \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "my-llm",
|
||||
"display_name": "My Local LLM",
|
||||
"description": "Local LLM via Ollama",
|
||||
"binary": "ollama",
|
||||
"type": "cli",
|
||||
"run_args": ["ollama", "run", "llama3", "{message}"],
|
||||
"check_type": "binary",
|
||||
"timeout": 120,
|
||||
"router_keywords": ["local", "llama", "ollama"]
|
||||
}'
|
||||
```
|
||||
|
||||
### Removing Custom Agents
|
||||
|
||||
Via the dashboard: **Agent Health** → click **Remove** on any custom agent.
|
||||
|
||||
Or via API:
|
||||
```bash
|
||||
curl -X DELETE http://127.0.0.1:8081/api/agents/my-llm
|
||||
```
|
||||
|
||||
Built-in agents cannot be removed.
|
||||
|
||||
---
|
||||
|
||||
## API Reference
|
||||
|
||||
|
||||
### Agents
|
||||
|
||||
| Method | Endpoint | Handler |
|
||||
|--------|----------|---------|
|
||||
| GET | `/api/agents` | `list_agents` |
|
||||
| GET | `/api/agents/health` | `get_agent_health` |
|
||||
| POST | `/api/agents/health/refresh` | `refresh_agent_health` |
|
||||
| POST | `/api/agents/register` | `register_agent` |
|
||||
| DELETE | `/api/agents/{agent_name}` | `unregister_agent` |
|
||||
| GET | `/api/agents/{name}/stats` | `get_agent_stats` |
|
||||
|
||||
### Analytics
|
||||
|
||||
| Method | Endpoint | Handler |
|
||||
|--------|----------|---------|
|
||||
| GET | `/api/analytics/skills` | `get_skill_analytics` |
|
||||
| GET | `/api/analytics/trends` | `get_trend_analytics` |
|
||||
|
||||
### Audit
|
||||
|
||||
| Method | Endpoint | Handler |
|
||||
|--------|----------|---------|
|
||||
| GET | `/api/audit` | `get_audit` |
|
||||
|
||||
### Backup
|
||||
|
||||
| Method | Endpoint | Handler |
|
||||
|--------|----------|---------|
|
||||
| POST | `/api/backup` | `create_backup` |
|
||||
| POST | `/api/backup/restore` | `restore_backup` |
|
||||
|
||||
### Backups
|
||||
|
||||
| Method | Endpoint | Handler |
|
||||
|--------|----------|---------|
|
||||
| GET | `/api/backups` | `list_backups` |
|
||||
|
||||
### Brain
|
||||
|
||||
| Method | Endpoint | Handler |
|
||||
|--------|----------|---------|
|
||||
| GET | `/api/brain` | `list_brain` |
|
||||
| GET | `/api/brain/{file_name}` | `get_brain_file` |
|
||||
| PUT | `/api/brain/{file_name}` | `update_brain_file` |
|
||||
|
||||
### Chat
|
||||
|
||||
| Method | Endpoint | Handler |
|
||||
|--------|----------|---------|
|
||||
| POST | `/api/chat` | `chat` |
|
||||
| GET | `/api/chat/history` | `get_chat_history` |
|
||||
|
||||
### Cost
|
||||
|
||||
| Method | Endpoint | Handler |
|
||||
|--------|----------|---------|
|
||||
| GET | `/api/cost` | `get_cost` |
|
||||
| POST | `/api/cost/record` | `record_cost` |
|
||||
|
||||
### Goals
|
||||
|
||||
| Method | Endpoint | Handler |
|
||||
|--------|----------|---------|
|
||||
| GET | `/api/goals` | `list_goals` |
|
||||
| POST | `/api/goals` | `create_goal` |
|
||||
| PUT | `/api/goals/{goal_id}` | `update_goal` |
|
||||
| DELETE | `/api/goals/{goal_id}` | `delete_goal` |
|
||||
|
||||
### Integrations
|
||||
|
||||
| Method | Endpoint | Handler |
|
||||
|--------|----------|---------|
|
||||
| GET | `/api/integrations` | `list_integrations` |
|
||||
| POST | `/api/integrations` | `add_integration` |
|
||||
|
||||
### Journal
|
||||
|
||||
| Method | Endpoint | Handler |
|
||||
|--------|----------|---------|
|
||||
| GET | `/api/journal/entries` | `list_journal_entries` |
|
||||
| GET | `/api/journal/entries/{entry_date}` | `get_journal_entry` |
|
||||
| PUT | `/api/journal/entries/{entry_date}` | `save_journal_entry` |
|
||||
| GET | `/api/journal/search` | `search_journal` |
|
||||
|
||||
### Kanban
|
||||
|
||||
| Method | Endpoint | Handler |
|
||||
|--------|----------|---------|
|
||||
| GET | `/api/kanban/board` | `kanban_board` |
|
||||
| POST | `/api/kanban/dispatch` | `kanban_dispatch` |
|
||||
| POST | `/api/kanban/links` | `kanban_add_link` |
|
||||
| DELETE | `/api/kanban/links` | `kanban_remove_link` |
|
||||
| POST | `/api/kanban/tasks` | `kanban_create_task` |
|
||||
| GET | `/api/kanban/tasks/{task_id}` | `kanban_get_task` |
|
||||
| PATCH | `/api/kanban/tasks/{task_id}` | `kanban_update_task` |
|
||||
| POST | `/api/kanban/tasks/{task_id}/block` | `kanban_block_task` |
|
||||
| POST | `/api/kanban/tasks/{task_id}/comments` | `kanban_add_comment` |
|
||||
| POST | `/api/kanban/tasks/{task_id}/complete` | `kanban_complete_task` |
|
||||
| POST | `/api/kanban/tasks/{task_id}/decompose` | `kanban_decompose_task` |
|
||||
| POST | `/api/kanban/tasks/{task_id}/specify` | `kanban_specify_task` |
|
||||
| POST | `/api/kanban/tasks/{task_id}/unblock` | `kanban_unblock_task` |
|
||||
|
||||
### Plugins
|
||||
|
||||
| Method | Endpoint | Handler |
|
||||
|--------|----------|---------|
|
||||
| GET | `/api/plugins` | `list_plugins` |
|
||||
| POST | `/api/plugins/install` | `install_plugin` |
|
||||
| DELETE | `/api/plugins/{plugin_name}` | `uninstall_plugin` |
|
||||
|
||||
### Prompts
|
||||
|
||||
| Method | Endpoint | Handler |
|
||||
|--------|----------|---------|
|
||||
| GET | `/api/prompts` | `list_prompts` |
|
||||
|
||||
### Root
|
||||
|
||||
| Method | Endpoint | Handler |
|
||||
|--------|----------|---------|
|
||||
| GET | `/` | `index` |
|
||||
| GET | `/favicon.ico` | `favicon` |
|
||||
| GET | `/favicon.svg` | `favicon_svg` |
|
||||
| GET | `/test` | `test_page` |
|
||||
|
||||
### Router
|
||||
|
||||
| Method | Endpoint | Handler |
|
||||
|--------|----------|---------|
|
||||
| POST | `/api/router/route` | `router_route` |
|
||||
| POST | `/api/router/suggest` | `router_suggest` |
|
||||
|
||||
### Scheduler
|
||||
|
||||
| Method | Endpoint | Handler |
|
||||
|--------|----------|---------|
|
||||
| GET | `/api/scheduler/jobs` | `list_jobs` |
|
||||
| POST | `/api/scheduler/jobs` | `create_job` |
|
||||
| DELETE | `/api/scheduler/jobs/{job_id}` | `delete_job` |
|
||||
|
||||
### Sessions
|
||||
|
||||
| Method | Endpoint | Handler |
|
||||
|--------|----------|---------|
|
||||
| GET | `/api/sessions/list` | `list_sessions` |
|
||||
| GET | `/api/sessions/{session_id}/replay` | `get_session_replay` |
|
||||
|
||||
### Settings
|
||||
|
||||
| Method | Endpoint | Handler |
|
||||
|--------|----------|---------|
|
||||
| GET | `/api/settings` | `get_settings` |
|
||||
| PUT | `/api/settings` | `update_settings` |
|
||||
|
||||
### Skills
|
||||
|
||||
| Method | Endpoint | Handler |
|
||||
|--------|----------|---------|
|
||||
| GET | `/api/skills` | `list_skills` |
|
||||
| GET | `/api/skills/{name}` | `get_skill` |
|
||||
| GET | `/api/skills/{name}/eval` | `get_skill_eval` |
|
||||
| POST | `/api/skills/{name}/run` | `run_skill` |
|
||||
|
||||
### Standards
|
||||
|
||||
| Method | Endpoint | Handler |
|
||||
|--------|----------|---------|
|
||||
| GET | `/api/standards` | `list_standards` |
|
||||
| POST | `/api/standards/discover` | `discover_standards` |
|
||||
|
||||
### Status
|
||||
|
||||
| Method | Endpoint | Handler |
|
||||
|--------|----------|---------|
|
||||
| GET | `/api/status` | `get_status` |
|
||||
|
||||
## Skills
|
||||
|
||||
Skills are stored in `skills/<name>/` directories with this structure:
|
||||
|
||||
```
|
||||
skills/
|
||||
skill-name/
|
||||
SKILL.md # Instructions (YAML frontmatter + markdown)
|
||||
learnings.md # Accumulated run logs
|
||||
eval.json # Evaluation criteria
|
||||
score-history.json # Historical scores
|
||||
context/ # Ephemeral task inputs
|
||||
```
|
||||
|
||||
**Skill frontmatter (SKILL.md):**
|
||||
```yaml
|
||||
---
|
||||
name: skill-name
|
||||
description: What this skill does
|
||||
version: 1.0.0
|
||||
author: Agentic OS
|
||||
tags: [category]
|
||||
---
|
||||
```
|
||||
|
||||
**Built-in skills (17):**
|
||||
- `brainstorming` — Socratic design refinement
|
||||
- `code-review` — Automated code review
|
||||
- `content-draft` — Blog/newsletter writing
|
||||
- `cost-analytics` — Token usage tracking
|
||||
- `daily-standup` — Morning briefing
|
||||
- `devops-audit` — GCP/K8s infrastructure audit
|
||||
- `goal-planner` — Step-by-step goal planning
|
||||
- `heartbeat` — System health monitoring
|
||||
- `meeting-minutes` — Meeting notes processor
|
||||
- `memory-consolidation` — Weekly memory synthesis
|
||||
- `project-planner` — Implementation plans
|
||||
- `research-synthesis` — Web research via Gemini
|
||||
- `systematic-debug` — 4-phase root cause debugging
|
||||
- `tdd-cycle` — Red-Green-Refactor TDD
|
||||
- `backup-skill` — Backup snapshot creation
|
||||
- `test-plugin` — Plugin test fixture
|
||||
- `notion-knowledge-capture` — Third-party (via npx skills add)
|
||||
|
||||
**Skill execution flow:**
|
||||
1. `POST /api/skills/:name/run` with optional `input` and `agent` override
|
||||
2. Router determines agent (auto-detect from keywords or SKILL.md `Primary:` line)
|
||||
3. Prompt built from SKILL.md + learnings.md + user input
|
||||
4. Agent executes via CLI
|
||||
5. Output appended to learnings.md, audit log updated
|
||||
|
||||
---
|
||||
|
||||
## Scheduler
|
||||
|
||||
Two scheduling systems exist:
|
||||
|
||||
### 1. Hermes Cron (active)
|
||||
- Managed via `hermes cron` CLI or Hermes cronjob tool
|
||||
- Currently running: `brain-guardian-autoupdate` (every 2h)
|
||||
- Persistent across restarts
|
||||
|
||||
### 2. APScheduler (configured, not auto-started)
|
||||
- Script: `scheduler/scheduler.py`
|
||||
- Jobs stored in: `scheduler/jobs/*.json`
|
||||
- Loaded at script start, runs in background thread
|
||||
- Jobs: heartbeat (5min), daily-standup (weekdays 9am), devops-audit, memory-consolidation
|
||||
|
||||
**Job JSON format:**
|
||||
```json
|
||||
{
|
||||
"id": "unique-id",
|
||||
"name": "Job Name",
|
||||
"skill": "skill-to-run",
|
||||
"cron": "*/5 * * * *",
|
||||
"enabled": true
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Configuration
|
||||
|
||||
### Server Configuration
|
||||
|
||||
**`data/settings.json`:**
|
||||
```json
|
||||
{
|
||||
"agent_preferences": {
|
||||
"opencode": {"enabled": false, "binary": "opencode"},
|
||||
"hermes": {"enabled": false, "binary": "hermes"},
|
||||
"gemini": {"enabled": true, "binary": "gemini"}
|
||||
},
|
||||
"dashboard": {"port": 8080, "host": "127.0.0.1", "dark_mode": true},
|
||||
"free_tier_limits": {
|
||||
"gemini_flash": {"requests_per_day": 1500, "tokens_per_day": 1000000},
|
||||
"openrouter_free": {"requests_per_day": 100, "tokens_per_day": 200000}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Hermes Model Configuration
|
||||
|
||||
**`~/.hermes/config.yaml`:**
|
||||
```yaml
|
||||
model:
|
||||
default: qwen/qwen3-next-80b-a3b-instruct:free
|
||||
provider: openrouter
|
||||
base_url: https://openrouter.ai/api/v1
|
||||
fallback_providers:
|
||||
- provider: openrouter
|
||||
model: openai/gpt-oss-120b:free
|
||||
- provider: openrouter
|
||||
model: google/gemma-4-31b-it:free
|
||||
- provider: openrouter
|
||||
model: nvidia/nemotron-3-super-120b-a12b:free
|
||||
```
|
||||
|
||||
**Free model fallback chain:**
|
||||
1. `qwen/qwen3-next-80b-a3b-instruct:free` — primary (Qwen 3, 80B)
|
||||
2. `openai/gpt-oss-120b:free` — OpenAI open source
|
||||
3. `google/gemma-4-31b-it:free` — Google Gemma
|
||||
4. `nvidia/nemotron-3-super-120b-a12b:free` — NVIDIA Nemotron
|
||||
|
||||
### Environment Variables
|
||||
|
||||
**`~/.hermes/.env`:**
|
||||
- `OPENROUTER_API_KEY` — OpenRouter API key (required for free models)
|
||||
- `GOOGLE_API_KEY` — Google AI Studio key (optional)
|
||||
- `DEEPSEEK_API_KEY` — DeepSeek API key (optional)
|
||||
|
||||
### Data Directory Structure
|
||||
|
||||
```
|
||||
agentic-os/
|
||||
├── brain/ # Persistent memory
|
||||
│ ├── memory.md # Cross-session memory
|
||||
│ ├── business-brain.md # Project context
|
||||
│ ├── active-projects.md # Auto-synced from goals
|
||||
│ ├── identity.md # Agent persona
|
||||
│ ├── constitution.md # Governance rules
|
||||
│ ├── recent-decisions.md # Decision log
|
||||
│ └── journal/ # Daily entries (YYYY-MM-DD.md)
|
||||
├── data/
|
||||
│ ├── settings.json # System settings
|
||||
│ ├── goals.json # Project goals
|
||||
│ ├── chat-history.json # Chat messages (last 200)
|
||||
│ ├── cost-history.json # Token cost tracking
|
||||
│ ├── kanban/ # Task files (UUID.json)
|
||||
│ ├── agent-registry.json # Custom agent definitions
|
||||
│ ├── router-keywords.json # Custom agent routing keywords
|
||||
│ └── integrations.json # Connected external apps
|
||||
├── skills/ # Skill definitions
|
||||
├── agents/ # Agent profiles
|
||||
│ ├── opencode/AGENTS.md
|
||||
│ ├── hermes/SOUL.md, MEMORY.md, USER.md
|
||||
│ └── gemini/GEMINI.md
|
||||
├── scheduler/jobs/ # Cron job definitions
|
||||
├── registry/plugins.json # Plugin registry
|
||||
├── standards/ # Project standards
|
||||
├── prompts/ # Reusable prompt templates
|
||||
├── audit/audit.log # Append-only audit trail
|
||||
└── backups/ # tar.gz snapshots
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Development Guide
|
||||
|
||||
### Adding a Dashboard Page
|
||||
|
||||
1. Create `dashboard/pages/mypage.js` with `async function renderMypage() { ... }`
|
||||
2. Add navigation link in `dashboard/index.html`: `<a href="#mypage" class="nav-item" data-page="mypage">`
|
||||
3. Add title in `dashboard/utils.js` `PAGE_TITLES` object
|
||||
4. Add any CSS to `dashboard/styles.css`
|
||||
|
||||
### Adding an API Endpoint
|
||||
|
||||
1. Add route in `server.py` under the appropriate section
|
||||
2. Add Pydantic model if needed (in Models section)
|
||||
3. Add frontend API method in `dashboard/api.js`
|
||||
4. Call from page renderer
|
||||
|
||||
### Adding a Skill
|
||||
|
||||
```bash
|
||||
cp -r skills/_template skills/my-skill
|
||||
# Edit SKILL.md with instructions
|
||||
# Add to registry/plugins.json if desired
|
||||
```
|
||||
|
||||
### Adding an Agent (Programmatic)
|
||||
|
||||
```python
|
||||
# In server.py, add to BUILTIN_AGENTS dict, or use the API:
|
||||
curl -X POST http://127.0.0.1:8081/api/agents/register \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"name":"my-agent","binary":"my-binary","type":"cli"}'
|
||||
```
|
||||
|
||||
### Key Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `server.py` | FastAPI backend (1780+ lines, 58+ endpoints) |
|
||||
| `dashboard/index.html` | SPA shell with sidebar nav |
|
||||
| `dashboard/app.js` | Page router and agent status polling |
|
||||
| `dashboard/api.js` | API client wrapper |
|
||||
| `dashboard/utils.js` | Shared utilities (toasts, formatting, PAGE_TITLES) |
|
||||
| `dashboard/styles.css` | All styling (dark/light theme) |
|
||||
| `dashboard/pages/*.js` | Individual page renderers |
|
||||
| `start.sh` | Start/stop/status script |
|
||||
| `data/agent-registry.json` | Custom agent persistence |
|
||||
| `brain/` | All persistent memory |
|
||||
|
||||
### Server Lifecycle
|
||||
|
||||
```bash
|
||||
./start.sh # Start (port 8081 + terminal on 8082)
|
||||
./start.sh --open # Start + open browser
|
||||
./start.sh --stop # Stop
|
||||
./start.sh --status # Check if running
|
||||
```
|
||||
|
||||
Logs: `tail -f /tmp/agentic-os.log`
|
||||
|
||||
PID file: `.agentic-os.pid`
|
||||
|
||||
---
|
||||
|
||||
## Changelog
|
||||
|
||||
### v1.3.0 (2026-06-28)
|
||||
- **Web Terminal**: Added browser-based bash shell (xterm.js + WebSocket PTY on port 8082)
|
||||
- **Agent Registry**: Dynamic agent registration system (add/remove custom agents via dashboard)
|
||||
- **Agent Health Page**: Complete rewrite with card view, registry table, add agent form
|
||||
- **Hermes Free Models**: Switched to Qwen 3 80B free + 3 fallback chain (OpenAI, Google, NVIDIA)
|
||||
- **Dynamic Agent Discovery**: All endpoints now use registry instead of hardcoded agent list
|
||||
- **Router Extensibility**: Custom agents can register keywords for smart routing
|
||||
- **API**: New `/api/agents`, `/api/agents/register`, `/api/agents/:name` (DELETE)
|
||||
|
||||
### v1.2.0 (2026-06-26)
|
||||
- Agent profiles for opencode, hermes, gemini
|
||||
- APScheduler integration (scheduler.py)
|
||||
- Plugin install from git repo or template
|
||||
|
||||
### v1.1.0 (2026-06-05)
|
||||
- Kanban board (13 endpoints)
|
||||
- Goals with auto-sync to brain
|
||||
- Journal with search
|
||||
- Agent health monitoring
|
||||
- Smart router with keyword scoring
|
||||
- Learning analytics
|
||||
- Session replay
|
||||
|
||||
### v1.0.0 (2026-05-17)
|
||||
- Initial release: FastAPI backend, SPA dashboard, 3 agents, 16 skills
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"id": "c334edd1",
|
||||
"name": "test-audit-job",
|
||||
"skill": "daily-standup",
|
||||
"cron": "0 9 * * *",
|
||||
"enabled": false,
|
||||
"created": "2026-06-28T21:39:14.908697+00:00",
|
||||
"last_run": null,
|
||||
"next_run": null
|
||||
}
|
||||
|
|
@ -1,8 +1,20 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Agentic OS — APScheduler engine for recurring tasks"""
|
||||
"""Agentic OS — APScheduler engine for recurring tasks.
|
||||
|
||||
Loads job definitions from scheduler/jobs/*.json and, when each cron
|
||||
trigger fires, actually executes the referenced skill by calling the
|
||||
running Agentic OS server's /api/skills/{name}/run endpoint. Running
|
||||
skills through the API means they go through the same code path as the
|
||||
dashboard: real agent invocation, real agent-health stats, and real
|
||||
eval-score population.
|
||||
|
||||
If the server isn't reachable, the job is logged (audit) but skipped,
|
||||
so the scheduler never crashes the loop.
|
||||
"""
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
from datetime import datetime, timezone
|
||||
|
||||
|
|
@ -13,65 +25,122 @@ except ImportError:
|
|||
print("Install APScheduler: pip install apscheduler")
|
||||
sys.exit(1)
|
||||
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
|
||||
BASE_DIR = Path(__file__).parent.resolve()
|
||||
JOBS_DIR = BASE_DIR / "jobs"
|
||||
# Server URL — same host, main API port. Override with AGENTIC_OS_URL env.
|
||||
SERVER_URL = "http://127.0.0.1:8080"
|
||||
RUN_TIMEOUT = 300 # seconds per skill run
|
||||
|
||||
def run_skill(skill_name: str):
|
||||
"""Execute a skill by invoking the appropriate agent."""
|
||||
|
||||
def log_audit(entry: dict):
|
||||
audit_file = BASE_DIR.parent / "audit" / "audit.log"
|
||||
entry = {
|
||||
"action": "scheduler_run",
|
||||
"skill": skill_name,
|
||||
"timestamp": datetime.now(timezone.utc).isoformat(),
|
||||
}
|
||||
entry["timestamp"] = datetime.now(timezone.utc).isoformat()
|
||||
try:
|
||||
audit_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(audit_file, "a", encoding="utf-8") as f:
|
||||
f.write(json.dumps(entry) + "\n")
|
||||
except OSError as e:
|
||||
print(f" [audit] failed to record run of {skill_name!r}: {e}")
|
||||
print(f"[{datetime.now().isoformat()}] Ran skill: {skill_name}")
|
||||
print(f" [audit] failed to record: {e}")
|
||||
|
||||
|
||||
def run_skill_via_api(skill_name: str, agent: str = "auto") -> dict:
|
||||
"""Invoke a skill through the live server API.
|
||||
|
||||
Returns a dict with at least {'ok': bool, 'reason'/'output': ...}.
|
||||
"""
|
||||
url = f"{SERVER_URL}/api/skills/{skill_name}/run"
|
||||
payload = json.dumps({"input": "", "agent": agent}).encode()
|
||||
req = urllib.request.Request(
|
||||
url, data=payload, headers={"Content-Type": "application/json"}, method="POST"
|
||||
)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=RUN_TIMEOUT) as resp:
|
||||
data = json.loads(resp.read())
|
||||
return {"ok": True, "data": data}
|
||||
except urllib.error.HTTPError as e:
|
||||
return {"ok": False, "reason": f"HTTP {e.code}: {e.reason}"}
|
||||
except urllib.error.URLError as e:
|
||||
return {"ok": False, "reason": f"server unreachable: {e.reason}"}
|
||||
except Exception as e: # timeout, json error, etc.
|
||||
return {"ok": False, "reason": str(e)}
|
||||
|
||||
|
||||
def run_job(job: dict):
|
||||
skill = job.get("skill")
|
||||
name = job.get("name", skill)
|
||||
agent = job.get("agent", "auto")
|
||||
print(f"[{datetime.now().isoformat()}] Firing job '{name}' -> skill '{skill}'")
|
||||
log_audit({"action": "scheduler_run", "job": name, "skill": skill, "stage": "start"})
|
||||
|
||||
if not skill:
|
||||
log_audit({"action": "scheduler_run", "job": name, "error": "no skill defined"})
|
||||
return
|
||||
|
||||
result = run_skill_via_api(skill, agent)
|
||||
if result["ok"]:
|
||||
data = result["data"]
|
||||
log_audit({
|
||||
"action": "scheduler_run",
|
||||
"job": name,
|
||||
"skill": skill,
|
||||
"agent": data.get("agent"),
|
||||
"run_id": data.get("run_id"),
|
||||
"stage": "done",
|
||||
})
|
||||
print(f" -> OK (agent={data.get('agent')}, run_id={data.get('run_id')})")
|
||||
else:
|
||||
reason = result["reason"]
|
||||
log_audit({"action": "scheduler_run", "job": name, "skill": skill, "error": reason})
|
||||
print(f" -> FAILED: {reason}")
|
||||
|
||||
|
||||
def load_jobs(scheduler: BackgroundScheduler):
|
||||
"""Load job definitions from jobs/ directory.
|
||||
|
||||
A single malformed job file is logged and skipped rather than being allowed
|
||||
to abort loading of every other job.
|
||||
"""
|
||||
for job_file in JOBS_DIR.glob("*.json"):
|
||||
if not JOBS_DIR.exists():
|
||||
print(f"No jobs directory at {JOBS_DIR}")
|
||||
return
|
||||
for job_file in sorted(JOBS_DIR.glob("*.json")):
|
||||
try:
|
||||
data = json.loads(job_file.read_text())
|
||||
except (json.JSONDecodeError, OSError) as e:
|
||||
print(f" Skipping {job_file.name}: could not read job ({e})")
|
||||
except (json.JSONDecodeError, KeyError) as e:
|
||||
print(f" Skipping malformed job file {job_file.name}: {e}")
|
||||
continue
|
||||
if not data.get("enabled", True):
|
||||
print(f" Skipping disabled job: {data.get('name', job_file.stem)}")
|
||||
continue
|
||||
try:
|
||||
scheduler.add_job(
|
||||
run_skill,
|
||||
CronTrigger.from_crontab(data["cron"]),
|
||||
args=[data["skill"]],
|
||||
id=data.get("id", data["name"]),
|
||||
name=data["name"],
|
||||
replace_existing=True,
|
||||
)
|
||||
except (KeyError, ValueError) as e:
|
||||
print(f" Skipping {job_file.name}: invalid job definition ({e})")
|
||||
cron = data.get("cron")
|
||||
skill = data.get("skill")
|
||||
if not cron or not skill:
|
||||
print(f" Skipping job {data.get('name')}: missing cron or skill")
|
||||
continue
|
||||
print(f" Scheduled: {data['name']} ({data['cron']})")
|
||||
scheduler.add_job(
|
||||
run_job,
|
||||
CronTrigger.from_crontab(cron),
|
||||
args=[data],
|
||||
id=data.get("id", data["name"]),
|
||||
name=data.get("name", skill),
|
||||
replace_existing=True,
|
||||
max_instances=1,
|
||||
coalesce=True,
|
||||
)
|
||||
print(f" Scheduled: {data.get('name')} (skill={skill}, cron={cron})")
|
||||
|
||||
|
||||
def main():
|
||||
scheduler = BackgroundScheduler()
|
||||
load_jobs(scheduler)
|
||||
scheduler.start()
|
||||
print(f"Agentic OS Scheduler running. Jobs loaded from: {JOBS_DIR}")
|
||||
print("Server API target: " + SERVER_URL)
|
||||
try:
|
||||
while True:
|
||||
import time
|
||||
time.sleep(60)
|
||||
except KeyboardInterrupt:
|
||||
scheduler.shutdown()
|
||||
print("Scheduler stopped.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,229 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Agentic-OS :: Agent Time Analyzer
|
||||
-----------------------------------
|
||||
Derives total time AI agents spent on the project from event logs.
|
||||
|
||||
The agentic-os project does NOT store explicit per-session durations,
|
||||
so we estimate total agent time with a session-gap model:
|
||||
|
||||
* Every logged event (audit.log line, chat message, cost entry) is a
|
||||
timestamped touch by an agent.
|
||||
* A "session" for an agent = a maximal run of touches where each touch
|
||||
is within GAP seconds of the previous one.
|
||||
* Session duration = (last touch - first touch) + TAIL.
|
||||
TAIL accounts for the agent working after its last logged event
|
||||
(e.g. finishing a task, writing files, thinking).
|
||||
|
||||
This is an ESTIMATE, clearly labeled as such in the dashboard.
|
||||
|
||||
Tunable: GAP (default 30 min), TAIL (default 2 min).
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
AUDIT_LOG = os.path.join(ROOT, "audit", "audit.log")
|
||||
CHAT_HISTORY = os.path.join(ROOT, "data", "chat-history.json")
|
||||
COST_HISTORY = os.path.join(ROOT, "data", "cost-history.json")
|
||||
OUT = os.path.join(ROOT, "data", "agent-time.json")
|
||||
|
||||
GAP_SECONDS = int(os.environ.get("AGENT_TIME_GAP", "1800")) # 30 min
|
||||
TAIL_SECONDS = int(os.environ.get("AGENT_TIME_TAIL", "120")) # 2 min
|
||||
|
||||
|
||||
def parse_ts(s):
|
||||
if not s:
|
||||
return None
|
||||
try:
|
||||
# normalize Z -> +00:00
|
||||
s = s.replace("Z", "+00:00")
|
||||
dt = datetime.fromisoformat(s)
|
||||
if dt.tzinfo is None:
|
||||
dt = dt.replace(tzinfo=timezone.utc)
|
||||
return dt.astimezone(timezone.utc)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def load_audit_events():
|
||||
"""Read audit.log (JSON lines). Returns list of (ts, agent)."""
|
||||
events = []
|
||||
if not os.path.exists(AUDIT_LOG):
|
||||
return events
|
||||
with open(AUDIT_LOG, "r", encoding="utf-8") as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
try:
|
||||
rec = json.loads(line)
|
||||
except Exception:
|
||||
continue
|
||||
ts = parse_ts(rec.get("timestamp"))
|
||||
if ts is None:
|
||||
continue
|
||||
agent = rec.get("agent") or "system"
|
||||
events.append((ts, agent))
|
||||
return events
|
||||
|
||||
|
||||
def load_json_file(path):
|
||||
if not os.path.exists(path):
|
||||
return None
|
||||
try:
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def load_chat_events():
|
||||
events = []
|
||||
data = load_json_file(CHAT_HISTORY)
|
||||
if not data:
|
||||
return events
|
||||
for m in data.get("messages", []):
|
||||
ts = parse_ts(m.get("timestamp"))
|
||||
if ts is None:
|
||||
continue
|
||||
agent = m.get("agent") or "unknown"
|
||||
events.append((ts, agent))
|
||||
return events
|
||||
|
||||
|
||||
def load_cost_events():
|
||||
events = []
|
||||
data = load_json_file(COST_HISTORY)
|
||||
if not data:
|
||||
return events
|
||||
for e in data.get("entries", []):
|
||||
ts = parse_ts(e.get("timestamp"))
|
||||
if ts is None:
|
||||
continue
|
||||
agent = e.get("agent") or "unknown"
|
||||
events.append((ts, agent))
|
||||
return events
|
||||
|
||||
|
||||
def compute_sessions(events):
|
||||
"""Group (ts, agent) events into per-agent sessions via gap model."""
|
||||
by_agent = {}
|
||||
for ts, agent in events:
|
||||
by_agent.setdefault(agent, []).append(ts)
|
||||
|
||||
per_agent = {}
|
||||
for agent, times in by_agent.items():
|
||||
times.sort()
|
||||
sessions = []
|
||||
cur_start = times[0]
|
||||
cur_last = times[0]
|
||||
for t in times[1:]:
|
||||
if (t - cur_last).total_seconds() <= GAP_SECONDS:
|
||||
cur_last = t
|
||||
else:
|
||||
sessions.append((cur_start, cur_last))
|
||||
cur_start = t
|
||||
cur_last = t
|
||||
sessions.append((cur_start, cur_last))
|
||||
total = sum(((last - start).total_seconds() + TAIL_SECONDS) for start, last in sessions)
|
||||
per_agent[agent] = {
|
||||
"sessions": len(sessions),
|
||||
"touches": len(times),
|
||||
"total_seconds": int(total),
|
||||
"first_seen": times[0].isoformat(),
|
||||
"last_seen": times[-1].isoformat(),
|
||||
}
|
||||
return per_agent
|
||||
|
||||
|
||||
def fmt_hms(seconds):
|
||||
seconds = int(seconds)
|
||||
h = seconds // 3600
|
||||
m = (seconds % 3600) // 60
|
||||
s = seconds % 60
|
||||
return f"{h}h {m}m {s}s"
|
||||
|
||||
|
||||
def main():
|
||||
events = []
|
||||
events += load_audit_events()
|
||||
events += load_chat_events()
|
||||
events += load_cost_events()
|
||||
|
||||
if not events:
|
||||
print("No events found.", file=sys.stderr)
|
||||
out = {
|
||||
"project": "agentic-os",
|
||||
"generated_at": datetime.now(timezone.utc).isoformat(),
|
||||
"method": "session-gap estimate",
|
||||
"gap_seconds": GAP_SECONDS,
|
||||
"tail_seconds": TAIL_SECONDS,
|
||||
"total_seconds": 0,
|
||||
"total_human": "0h 0m 0s",
|
||||
"agents": {},
|
||||
"event_count": 0,
|
||||
"first_seen": None,
|
||||
"last_seen": None,
|
||||
}
|
||||
with open(OUT, "w", encoding="utf-8") as f:
|
||||
json.dump(out, f, indent=2)
|
||||
print(json.dumps(out, indent=2))
|
||||
return
|
||||
|
||||
events.sort(key=lambda x: x[0])
|
||||
per_agent = compute_sessions(events)
|
||||
total = sum(a["total_seconds"] for a in per_agent.values())
|
||||
|
||||
# graphify metadata (best-effort)
|
||||
graph_meta = {}
|
||||
gpath = os.path.join(ROOT, "graphify-out", "graph.json")
|
||||
if os.path.exists(gpath):
|
||||
try:
|
||||
with open(gpath, "r", encoding="utf-8") as f:
|
||||
gj = json.load(f)
|
||||
nodes = gj.get("nodes", [])
|
||||
edges = gj.get("links", []) or gj.get("edges", [])
|
||||
graph_meta = {
|
||||
"nodes": len(nodes) if hasattr(nodes, "__len__") else "?",
|
||||
"edges": len(edges) if hasattr(edges, "__len__") else "?",
|
||||
}
|
||||
# count communities
|
||||
comms = set()
|
||||
for n in nodes:
|
||||
c = n.get("community")
|
||||
if c is not None:
|
||||
comms.add(c)
|
||||
graph_meta["communities"] = len(comms)
|
||||
graph_meta["file"] = "graphify-out/graph.json"
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
out = {
|
||||
"project": "agentic-os",
|
||||
"generated_at": datetime.now(timezone.utc).isoformat(),
|
||||
"method": "session-gap estimate (GAP=%ds, TAIL=%ds)" % (GAP_SECONDS, TAIL_SECONDS),
|
||||
"graph": graph_meta,
|
||||
"gap_seconds": GAP_SECONDS,
|
||||
"tail_seconds": TAIL_SECONDS,
|
||||
"total_seconds": total,
|
||||
"total_human": fmt_hms(total),
|
||||
"agents": per_agent,
|
||||
"event_count": len(events),
|
||||
"first_seen": events[0][0].isoformat(),
|
||||
"last_seen": events[-1][0].isoformat(),
|
||||
}
|
||||
|
||||
os.makedirs(os.path.dirname(OUT), exist_ok=True)
|
||||
with open(OUT, "w", encoding="utf-8") as f:
|
||||
json.dump(out, f, indent=2)
|
||||
print("Wrote", OUT)
|
||||
print("TOTAL:", out["total_human"], "across", len(per_agent), "agents,", len(events), "events")
|
||||
for agent, a in sorted(per_agent.items(), key=lambda kv: -kv[1]["total_seconds"]):
|
||||
print(f" {agent:12s} {fmt_hms(a['total_seconds']):>14s} sessions={a['sessions']:<4d} touches={a['touches']}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Extract API documentation from server.py and update docs/README.md
|
||||
Run this after adding/removing/changing API endpoints.
|
||||
"""
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
BASE = Path(__file__).parent.parent.resolve() # Go up from scripts/ to project root
|
||||
SERVER = BASE / "server.py"
|
||||
DOCS = BASE / "docs" / "README.md"
|
||||
|
||||
def extract_endpoints():
|
||||
"""Parse server.py for FastAPI route definitions."""
|
||||
content = SERVER.read_text()
|
||||
|
||||
# Match @app.get/post/put/patch/delete("path")
|
||||
pattern = r'@app\.(get|post|put|patch|delete)\("([^"]+)"'
|
||||
endpoints = []
|
||||
for match in re.finditer(pattern, content):
|
||||
method = match.group(1).upper()
|
||||
path = match.group(2)
|
||||
# Find the function name after the decorator
|
||||
rest = content[match.end():match.end()+200]
|
||||
func_match = re.search(r'def\s+(\w+)', rest)
|
||||
func_name = func_match.group(1) if func_match else "?"
|
||||
endpoints.append((method, path, func_name))
|
||||
|
||||
return endpoints
|
||||
|
||||
def generate_api_table(endpoints):
|
||||
"""Generate markdown table for API reference."""
|
||||
# Group by category (based on path prefix)
|
||||
categories = {}
|
||||
for method, path, func in endpoints:
|
||||
# Extract category from path
|
||||
parts = path.split('/')
|
||||
if len(parts) >= 3:
|
||||
cat = parts[2] # e.g. "status", "brain", "skills"
|
||||
else:
|
||||
cat = "root"
|
||||
categories.setdefault(cat, []).append((method, path, func))
|
||||
|
||||
lines = []
|
||||
for cat in sorted(categories.keys()):
|
||||
lines.append(f"\n### {cat.replace('-', ' ').title()}\n")
|
||||
lines.append("| Method | Endpoint | Handler |")
|
||||
lines.append("|--------|----------|---------|")
|
||||
for method, path, func in sorted(categories[cat], key=lambda x: x[1]):
|
||||
lines.append(f"| {method} | `{path}` | `{func}` |")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
def update_docs():
|
||||
"""Update the API reference section in docs/README.md."""
|
||||
endpoints = extract_endpoints()
|
||||
new_api_section = generate_api_table(endpoints)
|
||||
|
||||
if not DOCS.exists():
|
||||
print(f"ERROR: {DOCS} not found")
|
||||
sys.exit(1)
|
||||
|
||||
content = DOCS.read_text()
|
||||
|
||||
# Find and replace the API Reference section
|
||||
# Look for "## API Reference" until next "##" section
|
||||
pattern = r'(## API Reference\n).*?(?=\n## )'
|
||||
replacement = f"## API Reference\n\n{new_api_section}\n"
|
||||
|
||||
if re.search(pattern, content, re.DOTALL):
|
||||
new_content = re.sub(pattern, replacement, content, flags=re.DOTALL)
|
||||
DOCS.write_text(new_content)
|
||||
print(f"Updated {DOCS} with {len(endpoints)} API endpoints")
|
||||
else:
|
||||
print("WARNING: Could not find '## API Reference' section in docs")
|
||||
print("Adding it before the last section...")
|
||||
# Find last ## section
|
||||
last_section = content.rfind("\n## ")
|
||||
if last_section >= 0:
|
||||
new_content = content[:last_section] + f"\n{replacement}\n" + content[last_section:]
|
||||
DOCS.write_text(new_content)
|
||||
print(f"Updated {DOCS} with {len(endpoints)} API endpoints")
|
||||
else:
|
||||
print("ERROR: No sections found in docs")
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
update_docs()
|
||||
772
server.py
772
server.py
|
|
@ -12,6 +12,7 @@ import shlex
|
|||
import shutil
|
||||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
import tarfile
|
||||
import threading
|
||||
import time
|
||||
|
|
@ -172,7 +173,9 @@ def get_timestamp():
|
|||
return datetime.now(timezone.utc).isoformat()
|
||||
|
||||
def append_audit(entry: dict):
|
||||
audit_file = BASE_DIR / "audit" / "audit.log"
|
||||
audit_dir = BASE_DIR / "audit"
|
||||
audit_dir.mkdir(parents=True, exist_ok=True)
|
||||
audit_file = audit_dir / "audit.log"
|
||||
entry["timestamp"] = get_timestamp()
|
||||
entry["id"] = new_id()
|
||||
try:
|
||||
|
|
@ -184,7 +187,188 @@ def append_audit(entry: dict):
|
|||
# underlying operation, but surface it on the server console.
|
||||
print(f"[audit] failed to write entry {entry.get('action')!r}: {e}")
|
||||
|
||||
# ─── Agent Discovery (instant filesystem checks) ────────────────────
|
||||
# ─── Agent Stats (real, persisted) ──────────────────────────────
|
||||
AGENT_STATS_FILE = BASE_DIR / "data" / "agent-stats.json"
|
||||
|
||||
def load_agent_stats() -> dict:
|
||||
if AGENT_STATS_FILE.exists():
|
||||
try:
|
||||
return json.loads(AGENT_STATS_FILE.read_text())
|
||||
except (json.JSONDecodeError, KeyError):
|
||||
return {}
|
||||
return {}
|
||||
|
||||
def save_agent_stats(stats: dict):
|
||||
AGENT_STATS_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||||
AGENT_STATS_FILE.write_text(json.dumps(stats, indent=2))
|
||||
|
||||
def record_agent_run(agent: str, success: bool, duration: float):
|
||||
"""Persist real invocation stats for an agent (consumed by /api/agents/health)."""
|
||||
stats = load_agent_stats()
|
||||
now = get_timestamp()
|
||||
s = stats.get(agent, {
|
||||
"total_runs": 0, "successful_runs": 0, "failed_runs": 0,
|
||||
"first_seen": now, "last_seen": now, "total_time": 0.0,
|
||||
})
|
||||
s["total_runs"] += 1
|
||||
if success:
|
||||
s["successful_runs"] += 1
|
||||
else:
|
||||
s["failed_runs"] += 1
|
||||
s["last_seen"] = now
|
||||
s["total_time"] = s.get("total_time", 0.0) + duration
|
||||
s["avg_response_time"] = round(s["total_time"] / s["total_runs"], 3)
|
||||
stats[agent] = s
|
||||
save_agent_stats(stats)
|
||||
|
||||
def is_error_response(text: str) -> bool:
|
||||
"""Detect the error/refusal/busy sentinels this server emits on agent failures.
|
||||
|
||||
Must match COMPLETE sentinel phrases, never bare glyphs — the warning
|
||||
sign (⚠) and clock (⏱) can appear inside legitimate agent prose,
|
||||
so matching them alone would false-positive real successes.
|
||||
"""
|
||||
t = (text or "").strip()
|
||||
if not t:
|
||||
return True
|
||||
# Full-phrase sentinels only (start-anchored where appropriate).
|
||||
hard = (
|
||||
"timed out", # '...timed out.' / 'timed out after'
|
||||
"CLI not installed", # '⚠ Agent X CLI not installed'
|
||||
"Error communicating", # '⚠ Error communicating with X'
|
||||
"did not return", # 'Gemini CLI did not return a response'
|
||||
"needs setup", # '**Hermes needs setup**'
|
||||
"needs re-auth", # '**Gemini needs re-auth**'
|
||||
"Error executing skill", # '⚠ Error executing skill'
|
||||
"not configured", # hermes setup markers
|
||||
"no api key", "api_key not",
|
||||
"config file not found", "command not found",
|
||||
)
|
||||
if any(p in t for p in hard):
|
||||
return True
|
||||
# Whole-line / start-of-response sentinels.
|
||||
if t.startswith("Unknown agent"):
|
||||
return True
|
||||
if t.startswith("**Hermes") and "error" in t.lower():
|
||||
return True
|
||||
if t.startswith("**Gemini needs"):
|
||||
return True
|
||||
return False
|
||||
|
||||
# ─── Skill Eval Scoring (heuristic self-improvement metric) ───────
|
||||
def compute_skill_score(name: str, response_text: str) -> int:
|
||||
"""Weighted 0-100 quality score from eval.json criteria + response features."""
|
||||
skill_dir = BASE_DIR / "skills" / name
|
||||
criteria = []
|
||||
eval_path = skill_dir / "eval.json"
|
||||
if eval_path.exists():
|
||||
try:
|
||||
criteria = json.loads(eval_path.read_text()).get("criteria", [])
|
||||
except Exception:
|
||||
criteria = []
|
||||
if not criteria:
|
||||
criteria = [{"name": "completeness", "weight": 0.4},
|
||||
{"name": "accuracy", "weight": 0.3},
|
||||
{"name": "clarity", "weight": 0.3}]
|
||||
total_w = sum(c.get("weight", 0) for c in criteria) or 1.0
|
||||
|
||||
text = response_text or ""
|
||||
low = text.lower()
|
||||
is_err = is_error_response(text)
|
||||
|
||||
completeness = 0 if is_err else min(100, int(len(text) / 8))
|
||||
accuracy = 0 if is_err else 60
|
||||
if not is_err:
|
||||
if "```" in text: accuracy += 15
|
||||
if any(m in low for m in ["step", "1.", "first", "example", "because"]): accuracy += 15
|
||||
accuracy = min(100, accuracy)
|
||||
clarity = 10 if is_err else 30
|
||||
if not is_err:
|
||||
if "\n#" in text or "##" in text: clarity += 25
|
||||
if "- " in text or "* " in text: clarity += 20
|
||||
if "```" in text: clarity += 15
|
||||
clarity = min(100, clarity)
|
||||
|
||||
score_map = {"completeness": completeness, "accuracy": accuracy, "clarity": clarity}
|
||||
overall = sum(score_map.get(c["name"], 50) * c.get("weight", 0) for c in criteria) / total_w
|
||||
return max(0, min(100, int(round(overall))))
|
||||
|
||||
def record_skill_score(name: str, score: int, agent: str):
|
||||
"""Append a run's score to skills/<name>/score-history.json."""
|
||||
skill_dir = BASE_DIR / "skills" / name
|
||||
if not skill_dir.exists():
|
||||
return
|
||||
hist_path = skill_dir / "score-history.json"
|
||||
hist = json.loads(hist_path.read_text()) if hist_path.exists() else []
|
||||
hist.append({
|
||||
"date": get_timestamp()[:10],
|
||||
"timestamp": get_timestamp(),
|
||||
"score": score,
|
||||
"agent": agent,
|
||||
})
|
||||
hist_path.write_text(json.dumps(hist, indent=2))
|
||||
|
||||
# ─── Agent Registry (dynamic, user-extensible) ─────────────────────
|
||||
|
||||
AGENT_REGISTRY_FILE = BASE_DIR / "data" / "agent-registry.json"
|
||||
|
||||
# Built-in agents (always present)
|
||||
BUILTIN_AGENTS = {
|
||||
"opencode": {
|
||||
"name": "opencode",
|
||||
"display_name": "OpenCode",
|
||||
"description": "Code generation, DevOps, file operations",
|
||||
"binary": "opencode",
|
||||
"type": "cli",
|
||||
"run_args": ["opencode", "run", "--format", "json", "{message}"],
|
||||
"check_type": "binary",
|
||||
"builtin": True,
|
||||
},
|
||||
"hermes": {
|
||||
"name": "hermes",
|
||||
"display_name": "Hermes Agent",
|
||||
"description": "Memory, scheduling, multi-agent coordination",
|
||||
"binary": "hermes",
|
||||
"type": "cli",
|
||||
"run_args": ["hermes", "chat", "-q", "{message}"],
|
||||
"check_type": "binary",
|
||||
"builtin": True,
|
||||
},
|
||||
"gemini": {
|
||||
"name": "gemini",
|
||||
"display_name": "Gemini CLI",
|
||||
"description": "Research, analysis, multi-modal understanding",
|
||||
"binary": "gemini",
|
||||
"type": "cli",
|
||||
"run_args": ["gemini", "-y", "-m", "gemini-2.5-flash", "{message}"],
|
||||
"check_type": "oauth_file",
|
||||
"oauth_file": ".gemini/gemini-credentials.json",
|
||||
"builtin": True,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def load_agent_registry() -> dict:
|
||||
"""Load agent registry from disk, merging with builtins."""
|
||||
registry = dict(BUILTIN_AGENTS)
|
||||
if AGENT_REGISTRY_FILE.exists():
|
||||
try:
|
||||
custom = json.loads(AGENT_REGISTRY_FILE.read_text())
|
||||
for name, agent in custom.items():
|
||||
if name not in BUILTIN_AGENTS:
|
||||
agent["builtin"] = False
|
||||
registry[name] = agent
|
||||
except (json.JSONDecodeError, KeyError):
|
||||
pass
|
||||
return registry
|
||||
|
||||
|
||||
def save_agent_registry(agents: dict):
|
||||
"""Save custom agents to disk (excluding builtins)."""
|
||||
AGENT_REGISTRY_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||||
custom = {k: v for k, v in agents.items() if not v.get("builtin", False)}
|
||||
AGENT_REGISTRY_FILE.write_text(json.dumps(custom, indent=2))
|
||||
|
||||
|
||||
def _cli_has_subcommand(base_args: list, subcommand: str) -> bool:
|
||||
try:
|
||||
|
|
@ -193,6 +377,66 @@ def _cli_has_subcommand(base_args: list, subcommand: str) -> bool:
|
|||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def check_agent(name: str) -> dict:
|
||||
"""Dynamic agent check based on registry configuration."""
|
||||
registry = load_agent_registry()
|
||||
agent = registry.get(name)
|
||||
if not agent:
|
||||
return {"name": name, "status": "unknown", "display_name": name}
|
||||
|
||||
try:
|
||||
check_type = agent.get("check_type", "binary")
|
||||
binary = agent.get("binary", name)
|
||||
exists = shutil.which(binary) is not None
|
||||
|
||||
if check_type == "binary":
|
||||
status = "online" if exists else "offline"
|
||||
elif check_type == "oauth_file":
|
||||
oauth_path = Path.home() / agent.get("oauth_file", "")
|
||||
logged_in = oauth_path.exists()
|
||||
status = "online" if exists and logged_in else "offline" if not exists else "warning"
|
||||
elif check_type == "http":
|
||||
# HTTP health check endpoint
|
||||
import urllib.request
|
||||
try:
|
||||
url = agent.get("health_url", "")
|
||||
req = urllib.request.Request(url, method="GET")
|
||||
with urllib.request.urlopen(req, timeout=5) as resp:
|
||||
status = "online" if resp.status == 200 else "warning"
|
||||
except Exception:
|
||||
status = "offline" if not exists else "warning"
|
||||
elif check_type == "custom":
|
||||
# Run a custom check command. IMPORTANT: never use shell=True —
|
||||
# check_command comes from agent-registry config and a malicious
|
||||
# value ("; rm -rf ~") would otherwise execute arbitrary shell.
|
||||
check_cmd = agent.get("check_command", "")
|
||||
if check_cmd:
|
||||
import shlex
|
||||
try:
|
||||
cmd = shlex.split(check_cmd)
|
||||
result = subprocess.run(
|
||||
cmd, shell=False, capture_output=True, timeout=10
|
||||
)
|
||||
status = "online" if result.returncode == 0 else "offline"
|
||||
except (ValueError, OSError):
|
||||
status = "offline"
|
||||
else:
|
||||
status = "online" if exists else "offline"
|
||||
else:
|
||||
status = "online" if exists else "offline"
|
||||
except Exception:
|
||||
status = "offline"
|
||||
|
||||
return {
|
||||
"name": name,
|
||||
"display_name": agent.get("display_name", name),
|
||||
"status": status,
|
||||
"description": agent.get("description", ""),
|
||||
"type": agent.get("type", "cli"),
|
||||
"builtin": agent.get("builtin", False),
|
||||
}
|
||||
|
||||
def hermes_cli_args(*args: str) -> list:
|
||||
"""Build the command to invoke Hermes, bridging through WSL if the real agent only lives there.
|
||||
|
||||
|
|
@ -248,13 +492,99 @@ def check_agent(name: str) -> dict:
|
|||
status = "online" if exists and logged_in else "offline" if not exists else "warning"
|
||||
else:
|
||||
status = "offline"
|
||||
return {"name": name, "status": status}
|
||||
|
||||
return {
|
||||
"name": name,
|
||||
"display_name": agent.get("display_name", name),
|
||||
"status": status,
|
||||
"description": agent.get("description", ""),
|
||||
"type": agent.get("type", "cli"),
|
||||
"builtin": agent.get("builtin", False),
|
||||
}
|
||||
|
||||
|
||||
def execute_agent_dynamic(agent_name: str, message: str) -> str:
|
||||
"""Execute a message on any registered agent."""
|
||||
registry = load_agent_registry()
|
||||
agent = registry.get(agent_name)
|
||||
|
||||
if not agent:
|
||||
return f"Unknown agent: '{agent_name}'"
|
||||
|
||||
agent_type = agent.get("type", "cli")
|
||||
|
||||
if agent_type == "cli":
|
||||
return execute_agent_cli(agent, message)
|
||||
elif agent_type == "http":
|
||||
return execute_agent_http(agent, message)
|
||||
elif agent_type == "mcp":
|
||||
return execute_agent_mcp(agent, message)
|
||||
else:
|
||||
return f"Unsupported agent type: '{agent_type}'"
|
||||
|
||||
|
||||
def execute_agent_cli(agent: dict, message: str) -> str:
|
||||
"""Execute via CLI binary."""
|
||||
binary = agent.get("binary", agent["name"])
|
||||
run_args_template = agent.get("run_args", [binary, "{message}"])
|
||||
|
||||
# Build command with message substituted
|
||||
cmd = []
|
||||
for arg in run_args_template:
|
||||
if "{message}" in arg:
|
||||
cmd.append(arg.replace("{message}", message))
|
||||
else:
|
||||
cmd.append(arg)
|
||||
|
||||
timeout = agent.get("timeout", 60)
|
||||
|
||||
try:
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
|
||||
if result.returncode == 0:
|
||||
output = (result.stdout or "").strip()
|
||||
if output:
|
||||
return output
|
||||
return f"**{agent.get('display_name', binary)}**\n\nProcessed your message.\n\n**Message:** {message[:100]}"
|
||||
err = (result.stderr or "").strip()
|
||||
return err or f"{binary} returned exit code {result.returncode}"
|
||||
except subprocess.TimeoutExpired:
|
||||
return f"⏱ Agent '{agent.get('display_name', binary)}' timed out after {timeout}s.\n\n**Message:** {message[:100]}"
|
||||
except FileNotFoundError:
|
||||
return f"⚠ Agent '{agent.get('display_name', binary)}' CLI not installed. Install it and try again."
|
||||
except Exception as e:
|
||||
return f"⚠ Error communicating with {binary}: {str(e)}"
|
||||
|
||||
|
||||
def execute_agent_http(agent: dict, message: str) -> str:
|
||||
"""Execute via HTTP API endpoint."""
|
||||
import urllib.request
|
||||
|
||||
url = agent.get("api_url", "")
|
||||
method = agent.get("api_method", "POST")
|
||||
headers = agent.get("api_headers", {"Content-Type": "application/json"})
|
||||
timeout = agent.get("timeout", 60)
|
||||
|
||||
body = json.dumps({"message": message}).encode()
|
||||
|
||||
try:
|
||||
req = urllib.request.Request(url, data=body, headers=headers, method=method)
|
||||
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
||||
result = json.loads(resp.read())
|
||||
return result.get("response", result.get("output", str(result)))
|
||||
except Exception as e:
|
||||
return f"⚠ HTTP agent error: {str(e)}"
|
||||
|
||||
|
||||
def execute_agent_mcp(agent: dict, message: str) -> str:
|
||||
"""Execute via MCP server."""
|
||||
return f"⚠ MCP agent type not yet implemented for '{agent.get('display_name', agent['name'])}'"
|
||||
|
||||
# ─── Routes: Status ───────────────────────────────────────────────
|
||||
|
||||
@app.get("/api/status")
|
||||
def get_status():
|
||||
agents = [check_agent(a) for a in AGENTS]
|
||||
registry = load_agent_registry()
|
||||
agents = [check_agent(name) for name in registry]
|
||||
skills = list_dir(BASE_DIR / "skills")
|
||||
return {
|
||||
"status": "healthy",
|
||||
|
|
@ -429,7 +759,7 @@ def run_skill(name: str, req: Optional[SkillRunRequest] = None):
|
|||
line = line.strip()
|
||||
if "Primary:" in line:
|
||||
candidate = line.split(":")[-1].strip().lower()
|
||||
if candidate in ("opencode", "hermes", "gemini"):
|
||||
if candidate in load_agent_registry():
|
||||
agent_choice = candidate
|
||||
break
|
||||
if agent_choice == "auto":
|
||||
|
|
@ -446,15 +776,31 @@ def run_skill(name: str, req: Optional[SkillRunRequest] = None):
|
|||
|
||||
run_id = new_id()
|
||||
|
||||
# Execute via agent
|
||||
# Execute via agent (track real duration for agent-stats)
|
||||
import time as _t
|
||||
_t0 = _t.time()
|
||||
try:
|
||||
response_text = execute_agent(agent_choice, prompt)
|
||||
except subprocess.TimeoutExpired:
|
||||
response_text = f"⏱ Skill '{name}' timed out on agent '{agent_choice}'."
|
||||
except FileNotFoundError:
|
||||
response_text = f"⚠ Agent '{agent_choice}' CLI not installed. Install it and try again."
|
||||
except Exception as e:
|
||||
except Exception as e: # last-resort guard (execute_agent usually returns an error string)
|
||||
response_text = f"⚠ Error executing skill: {str(e)}"
|
||||
_elapsed = _t.time() - _t0
|
||||
|
||||
_agent_used = agent_choice or "auto"
|
||||
# Record REAL agent stats (consumed by /api/agents/health)
|
||||
success = not is_error_response(response_text)
|
||||
record_agent_run(_agent_used, success, _elapsed)
|
||||
|
||||
# Record REAL skill eval score (consumed by /api/analytics/skills)
|
||||
score = compute_skill_score(name, response_text)
|
||||
record_skill_score(name, score, _agent_used)
|
||||
append_audit({
|
||||
"action": "skill_scored",
|
||||
"skill": name,
|
||||
"agent": agent_choice,
|
||||
"score": score,
|
||||
"duration_s": round(_elapsed, 2),
|
||||
"success": success,
|
||||
})
|
||||
|
||||
# Save output to learnings.md
|
||||
timestamp = get_timestamp()[:10]
|
||||
|
|
@ -547,8 +893,8 @@ def get_audit(limit: int = Query(100, le=500)):
|
|||
continue
|
||||
try:
|
||||
entries.append(json.loads(l))
|
||||
except json.JSONDecodeError:
|
||||
# Skip a corrupt line rather than failing the whole audit view.
|
||||
except (json.JSONDecodeError, ValueError):
|
||||
# Skip malformed lines in the audit log
|
||||
continue
|
||||
return {"entries": entries[-limit:]}
|
||||
|
||||
|
|
@ -583,21 +929,112 @@ def list_plugins():
|
|||
@app.post("/api/plugins/install")
|
||||
def install_plugin(data: dict):
|
||||
name = data.get("name", "").strip()
|
||||
if not name:
|
||||
raise HTTPException(400, "Plugin name required")
|
||||
repo_url = data.get("repo_url", "").strip()
|
||||
if not name and not repo_url:
|
||||
raise HTTPException(400, "Plugin name or repo_url required")
|
||||
|
||||
# Derive name from repo_url if not given
|
||||
if not name and repo_url:
|
||||
name = repo_url.rstrip("/").split("/")[-1].replace(".git", "")
|
||||
|
||||
reg_file = BASE_DIR / "registry" / "plugins.json"
|
||||
reg = read_json(reg_file, {"plugins": []})
|
||||
if any(p["name"] == name for p in reg["plugins"]):
|
||||
return {"status": "already_installed"}
|
||||
return {"status": "already_installed", "plugin": name}
|
||||
|
||||
plugin_dir = BASE_DIR / "skills" / name
|
||||
|
||||
# If repo_url provided, clone it
|
||||
if repo_url:
|
||||
import subprocess
|
||||
if plugin_dir.exists():
|
||||
# Pull latest if already cloned
|
||||
subprocess.run(["git", "pull"], cwd=str(plugin_dir), capture_output=True)
|
||||
else:
|
||||
subprocess.run(["git", "clone", "--depth=1", repo_url, str(plugin_dir)], capture_output=True)
|
||||
|
||||
# If no repo_url, create from template
|
||||
elif not plugin_dir.exists():
|
||||
import shutil
|
||||
template_dir = BASE_DIR / "skills" / "_template"
|
||||
shutil.copytree(template_dir, plugin_dir)
|
||||
|
||||
# Detect metadata from SKILL.md frontmatter
|
||||
skill_md = plugin_dir / "SKILL.md"
|
||||
version = "1.0.0"
|
||||
description = ""
|
||||
plugin_type = "skill"
|
||||
if skill_md.exists():
|
||||
content = skill_md.read_text()
|
||||
import re
|
||||
ver_match = re.search(r'^version:\s*(.+)$', content, re.MULTILINE)
|
||||
desc_match = re.search(r'^description:\s*(.+)$', content, re.MULTILINE)
|
||||
type_match = re.search(r'^type:\s*(.+)$', content, re.MULTILINE)
|
||||
if ver_match:
|
||||
version = ver_match.group(1).strip().strip('"').strip("'")
|
||||
if desc_match:
|
||||
description = desc_match.group(1).strip().strip('"').strip("'")
|
||||
if type_match:
|
||||
plugin_type = type_match.group(1).strip()
|
||||
|
||||
reg["plugins"].append({
|
||||
"name": name,
|
||||
"version": version,
|
||||
"description": description,
|
||||
"installed": get_timestamp(),
|
||||
"version": "1.0.0",
|
||||
"type": plugin_type,
|
||||
"source": repo_url or "local",
|
||||
})
|
||||
write_json(reg_file, reg)
|
||||
append_audit({"action": "plugin_installed", "plugin": name})
|
||||
return {"status": "installed", "plugin": name}
|
||||
|
||||
@app.delete("/api/plugins/{plugin_name}")
|
||||
def uninstall_plugin(plugin_name: str):
|
||||
reg_file = BASE_DIR / "registry" / "plugins.json"
|
||||
reg = json.loads(reg_file.read_text()) if reg_file.exists() else {"plugins": []}
|
||||
reg["plugins"] = [p for p in reg["plugins"] if p["name"] != plugin_name]
|
||||
reg_file.write_text(json.dumps(reg, indent=2))
|
||||
append_audit({"action": "plugin_uninstalled", "plugin": plugin_name})
|
||||
return {"status": "uninstalled", "plugin": plugin_name}
|
||||
|
||||
# ─── Routes: Connected Apps ────────────────────────────────────────
|
||||
|
||||
@app.get("/api/integrations")
|
||||
def list_integrations():
|
||||
"""List connected external apps/integrations."""
|
||||
int_file = BASE_DIR / "data" / "integrations.json"
|
||||
if not int_file.exists():
|
||||
return {"integrations": []}
|
||||
return json.loads(int_file.read_text())
|
||||
|
||||
@app.post("/api/integrations")
|
||||
def add_integration(data: dict):
|
||||
"""Connect an external app via URL/webhook."""
|
||||
name = data.get("name", "").strip()
|
||||
url = data.get("url", "").strip()
|
||||
int_type = data.get("type", "webhook")
|
||||
if not name or not url:
|
||||
raise HTTPException(400, "name and url required")
|
||||
|
||||
int_file = BASE_DIR / "data" / "integrations.json"
|
||||
integrations = []
|
||||
if int_file.exists():
|
||||
integrations = json.loads(int_file.read_text()).get("integrations", [])
|
||||
if any(i["name"] == name for i in integrations):
|
||||
raise HTTPException(409, f"Integration '{name}' already exists")
|
||||
|
||||
integrations.append({
|
||||
"name": name,
|
||||
"url": url,
|
||||
"type": int_type,
|
||||
"added": get_timestamp(),
|
||||
"status": "active",
|
||||
})
|
||||
int_file.write_text(json.dumps({"integrations": integrations}, indent=2))
|
||||
append_audit({"action": "integration_added", "name": name, "url": url})
|
||||
return {"status": "connected", "name": name}
|
||||
|
||||
# ─── Routes: Backup ───────────────────────────────────────────────
|
||||
|
||||
@app.get("/api/backups")
|
||||
|
|
@ -759,6 +1196,9 @@ def clean_hermes_output(raw: str) -> str:
|
|||
return '\n'.join(non_meta[-5:]) or raw
|
||||
|
||||
def execute_agent(agent: str, message: str) -> str:
|
||||
import time as _t
|
||||
_t0 = _t.time()
|
||||
_ok = True
|
||||
try:
|
||||
if agent == "opencode":
|
||||
try:
|
||||
|
|
@ -797,9 +1237,17 @@ def execute_agent(agent: str, message: str) -> str:
|
|||
# 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:
|
||||
# Only flag as "needs setup" when the error clearly indicates Hermes
|
||||
# is genuinely unconfigured (missing key/config). Transient failures
|
||||
# (rate limits, timeouts, empty model output, API hiccups) must NOT
|
||||
# be mislabeled as a setup problem.
|
||||
setup_markers = ("not configured", "no api key", "api_key not", "missing config", "config file not found", "command not found")
|
||||
if code != 0 and any(m in err_msg.lower() for m in setup_markers):
|
||||
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}"
|
||||
# Otherwise surface the real error so failures are diagnosable.
|
||||
if err_msg:
|
||||
return f"**Hermes error (exit {code})**\n\n{err_msg[:400]}\n\nIf this looks like a config problem, run `hermes setup`."
|
||||
return f"hermes returned exit code {code}"
|
||||
|
||||
elif agent == "gemini":
|
||||
for attempt, (args, to) in enumerate([
|
||||
|
|
@ -834,8 +1282,9 @@ def execute_agent(agent: str, message: str) -> str:
|
|||
@app.post("/api/chat")
|
||||
def chat(req: ChatRequest):
|
||||
agent = req.agent.lower().strip()
|
||||
if agent not in AGENTS:
|
||||
raise HTTPException(400, "Agent must be one of: opencode, hermes, gemini")
|
||||
registry = load_agent_registry()
|
||||
if agent not in registry:
|
||||
raise HTTPException(400, f"Agent must be one of: {', '.join(registry.keys())}")
|
||||
|
||||
user_msg = {
|
||||
"id": new_id(),
|
||||
|
|
@ -847,6 +1296,8 @@ def chat(req: ChatRequest):
|
|||
save_chat_message(user_msg)
|
||||
|
||||
response_text = execute_agent(agent, req.message)
|
||||
# Record REAL agent stats (consumed by /api/agents/health)
|
||||
record_agent_run(agent, not is_error_response(response_text), 0.0)
|
||||
|
||||
agent_msg = {
|
||||
"id": new_id(),
|
||||
|
|
@ -1320,6 +1771,15 @@ def kanban_remove_link(parent_id: str = Query(...), child_id: str = Query(...)):
|
|||
save_kanban_task(t)
|
||||
return {"status": "unlinked"}
|
||||
|
||||
@app.delete("/api/kanban/tasks/{task_id}")
|
||||
def kanban_delete_task(task_id: str):
|
||||
path = KANBAN_DIR / f"{task_id}.json"
|
||||
if not path.exists():
|
||||
raise HTTPException(404, "Task not found")
|
||||
path.unlink()
|
||||
append_audit({"action": "kanban_task_deleted", "task_id": task_id})
|
||||
return {"status": "deleted"}
|
||||
|
||||
@app.post("/api/kanban/dispatch")
|
||||
def kanban_dispatch():
|
||||
dispatched = []
|
||||
|
|
@ -1494,12 +1954,19 @@ def search_journal(q: str = Query("")):
|
|||
@app.get("/api/agents/health")
|
||||
def get_agent_health():
|
||||
try:
|
||||
stats = load_agent_stats()
|
||||
agents = []
|
||||
for name in AGENTS:
|
||||
for name in load_agent_registry():
|
||||
info = check_agent(name)
|
||||
info["uptime"] = 0
|
||||
info["success_rate"] = 100
|
||||
info["last_seen"] = get_timestamp()
|
||||
s = stats.get(name, {})
|
||||
total = s.get("total_runs", 0)
|
||||
info["total_runs"] = total
|
||||
info["successful_runs"] = s.get("successful_runs", 0)
|
||||
info["failed_runs"] = s.get("failed_runs", 0)
|
||||
info["success_rate"] = round(100.0 * s.get("successful_runs", 0) / total, 1) if total else 0.0
|
||||
info["avg_response_time"] = s.get("avg_response_time", 0.0)
|
||||
info["uptime"] = 0 # process-level uptime not tracked per-agent
|
||||
info["last_seen"] = s.get("last_seen", "")
|
||||
agents.append(info)
|
||||
return {"agents": agents, "updated": get_timestamp()}
|
||||
except Exception as e:
|
||||
|
|
@ -1508,17 +1975,21 @@ def get_agent_health():
|
|||
@app.get("/api/agents/{name}/stats")
|
||||
def get_agent_stats(name: str):
|
||||
try:
|
||||
if name not in AGENTS:
|
||||
if name not in load_agent_registry():
|
||||
raise HTTPException(400, "Invalid agent")
|
||||
info = check_agent(name)
|
||||
stats = load_agent_stats().get(name, {})
|
||||
total = stats.get("total_runs", 0)
|
||||
return {
|
||||
"name": name,
|
||||
"status": info["status"],
|
||||
"total_runs": 0,
|
||||
"successful_runs": 0,
|
||||
"failed_runs": 0,
|
||||
"avg_response_time": 0,
|
||||
"last_seen": get_timestamp(),
|
||||
"total_runs": total,
|
||||
"successful_runs": stats.get("successful_runs", 0),
|
||||
"failed_runs": stats.get("failed_runs", 0),
|
||||
"success_rate": round(100.0 * stats.get("successful_runs", 0) / total, 1) if total else 0.0,
|
||||
"avg_response_time": stats.get("avg_response_time", 0.0),
|
||||
"first_seen": stats.get("first_seen", ""),
|
||||
"last_seen": stats.get("last_seen", ""),
|
||||
}
|
||||
except HTTPException:
|
||||
raise
|
||||
|
|
@ -1529,7 +2000,7 @@ def get_agent_stats(name: str):
|
|||
def refresh_agent_health():
|
||||
try:
|
||||
agents = []
|
||||
for name in AGENTS:
|
||||
for name in load_agent_registry():
|
||||
info = check_agent(name)
|
||||
agents.append(info)
|
||||
append_audit({"action": "agent_health_refreshed"})
|
||||
|
|
@ -1537,6 +2008,114 @@ def refresh_agent_health():
|
|||
except Exception as e:
|
||||
return {"agents": [], "error": str(e)}
|
||||
|
||||
# ─── Routes: Agent Registry Management ───────────────────────────
|
||||
|
||||
class AgentRegisterRequest(BaseModel):
|
||||
name: str
|
||||
display_name: str = ""
|
||||
description: str = ""
|
||||
binary: str = ""
|
||||
type: str = "cli" # cli, http, mcp
|
||||
run_args: list = []
|
||||
check_type: str = "binary" # binary, oauth_file, http, custom
|
||||
oauth_file: str = ""
|
||||
health_url: str = ""
|
||||
api_url: str = ""
|
||||
api_method: str = "POST"
|
||||
check_command: str = ""
|
||||
timeout: int = 60
|
||||
router_keywords: list = []
|
||||
|
||||
|
||||
@app.get("/api/agents")
|
||||
def list_agents():
|
||||
"""List all registered agents (built-in + custom)."""
|
||||
registry = load_agent_registry()
|
||||
agents = []
|
||||
for name, agent in registry.items():
|
||||
info = check_agent(name)
|
||||
agents.append(info)
|
||||
return {"agents": agents}
|
||||
|
||||
|
||||
@app.post("/api/agents/register")
|
||||
def register_agent(req: AgentRegisterRequest):
|
||||
"""Register a new custom agent."""
|
||||
name = req.name.lower().strip().replace(" ", "_").replace("-", "_")
|
||||
if not name:
|
||||
raise HTTPException(400, "Agent name required")
|
||||
if name in BUILTIN_AGENTS:
|
||||
raise HTTPException(409, f"'{name}' is a built-in agent")
|
||||
|
||||
registry = load_agent_registry()
|
||||
if name in registry:
|
||||
raise HTTPException(409, f"Agent '{name}' already exists")
|
||||
|
||||
if not req.display_name:
|
||||
req.display_name = req.name.replace("_", " ").replace("-", " ").title()
|
||||
if not req.binary:
|
||||
req.binary = req.name
|
||||
if not req.run_args:
|
||||
req.run_args = [req.binary, "{message}"]
|
||||
|
||||
agent_config = {
|
||||
"name": name,
|
||||
"display_name": req.display_name,
|
||||
"description": req.description,
|
||||
"binary": req.binary,
|
||||
"type": req.type,
|
||||
"run_args": req.run_args,
|
||||
"check_type": req.check_type,
|
||||
"timeout": req.timeout,
|
||||
"builtin": False,
|
||||
}
|
||||
if req.oauth_file:
|
||||
agent_config["oauth_file"] = req.oauth_file
|
||||
if req.health_url:
|
||||
agent_config["health_url"] = req.health_url
|
||||
if req.api_url:
|
||||
agent_config["api_url"] = req.api_url
|
||||
if req.api_method:
|
||||
agent_config["api_method"] = req.api_method
|
||||
if req.check_command:
|
||||
agent_config["check_command"] = req.check_command
|
||||
|
||||
registry[name] = agent_config
|
||||
save_agent_registry(registry)
|
||||
|
||||
if req.router_keywords:
|
||||
rkf = BASE_DIR / "data" / "router-keywords.json"
|
||||
kw = json.loads(rkf.read_text()) if rkf.exists() else {}
|
||||
kw[name] = req.router_keywords
|
||||
rkf.write_text(json.dumps(kw, indent=2))
|
||||
|
||||
append_audit({"action": "agent_registered", "agent": name})
|
||||
return {"status": "registered", "agent": check_agent(name)}
|
||||
|
||||
|
||||
@app.delete("/api/agents/{agent_name}")
|
||||
def unregister_agent(agent_name: str):
|
||||
"""Remove a custom agent (built-ins cannot be removed)."""
|
||||
if agent_name in BUILTIN_AGENTS:
|
||||
raise HTTPException(403, "Cannot remove built-in agents")
|
||||
|
||||
registry = load_agent_registry()
|
||||
if agent_name not in registry:
|
||||
raise HTTPException(404, "Agent not found")
|
||||
|
||||
del registry[agent_name]
|
||||
save_agent_registry(registry)
|
||||
|
||||
rkf = BASE_DIR / "data" / "router-keywords.json"
|
||||
if rkf.exists():
|
||||
kw = json.loads(rkf.read_text())
|
||||
kw.pop(agent_name, None)
|
||||
rkf.write_text(json.dumps(kw, indent=2))
|
||||
|
||||
append_audit({"action": "agent_unregistered", "agent": agent_name})
|
||||
return {"status": "unregistered", "agent": agent_name}
|
||||
|
||||
|
||||
# ─── Routes: Smart Router (2 endpoints) ─────────────────────────
|
||||
|
||||
ROUTER_RULES = {
|
||||
|
|
@ -1549,6 +2128,14 @@ ROUTER_RULES = {
|
|||
def router_suggest(data: RouterSuggest):
|
||||
try:
|
||||
task_lower = data.task.lower()
|
||||
# Include custom agent router keywords
|
||||
rkf = BASE_DIR / "data" / "router-keywords.json"
|
||||
if rkf.exists():
|
||||
custom_kw = json.loads(rkf.read_text())
|
||||
for agent, kw in custom_kw.items():
|
||||
if agent not in ROUTER_RULES:
|
||||
ROUTER_RULES[agent] = kw
|
||||
|
||||
scores = {}
|
||||
for agent, keywords in ROUTER_RULES.items():
|
||||
scores[agent] = sum(1 for k in keywords if k in task_lower)
|
||||
|
|
@ -1567,7 +2154,7 @@ def router_suggest(data: RouterSuggest):
|
|||
def router_route(data: RouterRoute):
|
||||
try:
|
||||
agent = data.agent.lower()
|
||||
if agent not in AGENTS:
|
||||
if agent not in load_agent_registry():
|
||||
return {"status": "error", "message": f"Invalid agent: {agent}"}
|
||||
append_audit({"action": "task_routed", "agent": agent, "task_preview": data.task[:50]})
|
||||
return {
|
||||
|
|
@ -1667,12 +2254,35 @@ def get_session_replay(session_id: str):
|
|||
except Exception as e:
|
||||
return {"session_id": session_id, "messages": [], "error": str(e)}
|
||||
|
||||
# ─── WebSocket Terminal ───────────────────────────────────────────
|
||||
|
||||
import asyncio
|
||||
import websockets
|
||||
import pty
|
||||
import os
|
||||
import select
|
||||
import struct
|
||||
import fcntl
|
||||
import signal
|
||||
|
||||
# NOTE: The interactive terminal is provided by the in-app WebSocket
|
||||
# endpoint `/ws/terminal` (see PtySession above). The previous standalone
|
||||
# WebSocket terminal server on port 8082 was removed to avoid two
|
||||
# redundant terminal implementations.
|
||||
|
||||
|
||||
# ─── Routes: Dashboard Static Files ──────────────────────────────
|
||||
|
||||
dashboard_dir = BASE_DIR / "dashboard"
|
||||
if dashboard_dir.exists():
|
||||
app.mount("/dashboard", StaticFiles(directory=str(dashboard_dir)), name="dashboard")
|
||||
|
||||
# graphify knowledge-graph output (built by `graphify . --code-only`).
|
||||
# Served so dashboard pages can link/open the interactive graph.
|
||||
graphify_dir = BASE_DIR / "graphify-out"
|
||||
if graphify_dir.exists():
|
||||
app.mount("/graphify-out", StaticFiles(directory=str(graphify_dir)), name="graphify-out")
|
||||
|
||||
@app.get("/", response_class=HTMLResponse)
|
||||
def index():
|
||||
html_file = BASE_DIR / "dashboard" / "index.html"
|
||||
|
|
@ -1686,6 +2296,80 @@ def index():
|
|||
return HTMLResponse(content=content)
|
||||
return HTMLResponse("<h1>Agentic OS</h1><p>Dashboard not built yet. Run the installer for your platform first (<code>./install.sh</code> on Linux/macOS or <code>.\\install.ps1</code> on Windows).</p>")
|
||||
|
||||
@app.get("/test", response_class=HTMLResponse)
|
||||
def test_page():
|
||||
html_file = BASE_DIR / "dashboard" / "test.html"
|
||||
if html_file.exists():
|
||||
return HTMLResponse(html_file.read_text())
|
||||
return HTMLResponse("<h1>Test page not found</h1>")
|
||||
|
||||
# ─── Routes: Agent Time Monitor ───────────────────────────────
|
||||
# Tracks total time AI agents spent on the project. Time is DERIVED from
|
||||
# event logs (audit.log, chat-history.json, cost-history.json) via a
|
||||
# session-gap estimator — see scripts/analyze_agent_time.py.
|
||||
# NOTE: no recompute happens at startup or import; the report is only
|
||||
# regenerated on explicit /api/agent-time/recompute (or if the cache
|
||||
# file is missing). This keeps app boot fast and side-effect free.
|
||||
|
||||
AGENT_TIME_REPORT = BASE_DIR / "data" / "agent-time.json"
|
||||
AGENT_TIME_ANALYZER = BASE_DIR / "scripts" / "analyze_agent_time.py"
|
||||
|
||||
def _recompute_agent_time() -> dict:
|
||||
"""Run the analyzer to (re)generate data/agent-time.json. Returns report."""
|
||||
import subprocess
|
||||
try:
|
||||
subprocess.run(
|
||||
[sys.executable, str(AGENT_TIME_ANALYZER)],
|
||||
cwd=str(BASE_DIR), capture_output=True, text=True, timeout=120,
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
# The analyzer always writes the file (even with 0 events), but guard
|
||||
# in case the script is missing.
|
||||
if AGENT_TIME_REPORT.exists():
|
||||
return json.loads(AGENT_TIME_REPORT.read_text(encoding="utf-8"))
|
||||
return {"project": "agentic-os", "total_seconds": 0, "total_human": "0h 0m 0s",
|
||||
"agents": {}, "event_count": 0, "method": "unavailable"}
|
||||
|
||||
@app.get("/api/agent-time")
|
||||
def get_agent_time(recompute: bool = False):
|
||||
if recompute or not AGENT_TIME_REPORT.exists():
|
||||
return _recompute_agent_time()
|
||||
try:
|
||||
return json.loads(AGENT_TIME_REPORT.read_text(encoding="utf-8"))
|
||||
except Exception:
|
||||
return _recompute_agent_time()
|
||||
|
||||
@app.post("/api/agent-time/recompute")
|
||||
def recompute_agent_time():
|
||||
report = _recompute_agent_time()
|
||||
return {"ok": True, "total_human": report.get("total_human"),
|
||||
"agents": len(report.get("agents", {})),
|
||||
"event_count": report.get("event_count", 0)}
|
||||
|
||||
# Skin-friendly view for Rainmeter (WebParser). Returns plain text:
|
||||
# Line 1: human total, e.g. "5h 34m 45s"
|
||||
# Line 2: total_seconds + fixed-order per-agent seconds (0 if absent)
|
||||
# system hermes opencode gemini codex jarvis kilocode test_claude test
|
||||
# This fixed-width layout lets a single RegExp grab every value reliably.
|
||||
_AGENT_ORDER = ["system", "hermes", "opencode", "gemini", "codex",
|
||||
"jarvis", "kilocode", "test_claude", "test"]
|
||||
|
||||
@app.get("/api/agent-time/skin")
|
||||
def agent_time_skin():
|
||||
if not AGENT_TIME_REPORT.exists():
|
||||
report = _recompute_agent_time()
|
||||
else:
|
||||
try:
|
||||
report = json.loads(AGENT_TIME_REPORT.read_text(encoding="utf-8"))
|
||||
except Exception:
|
||||
report = _recompute_agent_time()
|
||||
agents = report.get("agents", {})
|
||||
total = report.get("total_seconds", 0)
|
||||
per = " ".join(str(agents.get(a, {}).get("total_seconds", 0)) for a in _AGENT_ORDER)
|
||||
body = f"{report.get('total_human', '0h 0m 0s')}\n{total} {per}"
|
||||
return Response(content=body, media_type="text/plain; charset=utf-8")
|
||||
|
||||
# ─── Favicon ──────────────────────────────────────────────────────
|
||||
|
||||
FAVICON_SVG = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><defs><linearGradient id="g" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="#6c5ce7"/><stop offset="100%" stop-color="#fd79a8"/></linearGradient></defs><rect width="32" height="32" rx="8" fill="url(#g)"/><polygon points="16,6 24,11 24,21 16,26 8,21 8,11" fill="none" stroke="white" stroke-width="2" stroke-linejoin="round"/><circle cx="16" cy="16" r="3" fill="white"/></svg>'
|
||||
|
|
@ -1701,9 +2385,29 @@ def favicon_svg():
|
|||
# ─── Main ─────────────────────────────────────────────────────────
|
||||
|
||||
if __name__ == "__main__":
|
||||
import socket
|
||||
|
||||
import uvicorn
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--port", type=int, default=8080)
|
||||
parser.add_argument("--host", type=str, default="127.0.0.1")
|
||||
parser.add_argument("--host", type=str, default="0.0.0.0")
|
||||
args = parser.parse_args()
|
||||
|
||||
# ── Double-launch guard ───────────────────────────────────────────
|
||||
# Refuse to start if the API port is already bound by another process.
|
||||
# Without this, two server.py instances collide on 8081 (and 8082),
|
||||
# producing phantom "register doesn't persist" bugs and bind crashes.
|
||||
probe = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
try:
|
||||
probe.bind((args.host, args.port))
|
||||
probe.close()
|
||||
except OSError:
|
||||
print(
|
||||
f"ERROR: port {args.port} on {args.host} is already in use. "
|
||||
f"Agentic OS appears to be running already — stop it first "
|
||||
f"(./start.sh --stop) before launching another instance."
|
||||
)
|
||||
raise SystemExit(1)
|
||||
|
||||
uvicorn.run(app, host=args.host, port=args.port)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,77 @@
|
|||
{
|
||||
"version": 1,
|
||||
"skills": {
|
||||
"firebase-ai-logic-basics": {
|
||||
"source": "firebase/skills",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/firebase-ai-logic-basics/SKILL.md",
|
||||
"computedHash": "aba2d689236a21ac7989286b88ecb3399d616b90f2dd8946e675466e57802c35"
|
||||
},
|
||||
"firebase-app-hosting-basics": {
|
||||
"source": "firebase/skills",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/firebase-app-hosting-basics/SKILL.md",
|
||||
"computedHash": "562f27d560ff640a1e18ec4f5dd4ae70bd98447433c077be19292793ae095572"
|
||||
},
|
||||
"firebase-auth-basics": {
|
||||
"source": "firebase/skills",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/firebase-auth-basics/SKILL.md",
|
||||
"computedHash": "f4850f578bacdda96cacaa57b5e1121ec047e99bf7d389033042b1975af91040"
|
||||
},
|
||||
"firebase-basics": {
|
||||
"source": "firebase/skills",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/firebase-basics/SKILL.md",
|
||||
"computedHash": "8210ee53231b8299df100561d4ef776d39a293fd54403008bd738c65a856d54f"
|
||||
},
|
||||
"firebase-crashlytics": {
|
||||
"source": "firebase/skills",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/firebase-crashlytics/SKILL.md",
|
||||
"computedHash": "62a51efdc3240eed27697e14bbb8a29ebb8c94530fd3a1c851e747b90804d69b"
|
||||
},
|
||||
"firebase-data-connect": {
|
||||
"source": "firebase/skills",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/firebase-data-connect-basics/SKILL.md",
|
||||
"computedHash": "92d3dff5c94753c4077cd44a5c7c1f5e077b8c07dfe28455776204fe4f087d81"
|
||||
},
|
||||
"firebase-firestore": {
|
||||
"source": "firebase/skills",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/firebase-firestore/SKILL.md",
|
||||
"computedHash": "be5ed12b7402b1f36364e20d4f9ccd2caac90302e23427395a02570db9f1434a"
|
||||
},
|
||||
"firebase-hosting-basics": {
|
||||
"source": "firebase/skills",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/firebase-hosting-basics/SKILL.md",
|
||||
"computedHash": "fd0fede590512379a3103b74a7c1e612e38e6948b08e763a75962cd8c7f5b8e2"
|
||||
},
|
||||
"firebase-remote-config-basics": {
|
||||
"source": "firebase/skills",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/firebase-remote-config-basics/SKILL.md",
|
||||
"computedHash": "d5e9090dec35e050be958b72b672e39bd931014ebb311bac3a46f542fc04e10d"
|
||||
},
|
||||
"firebase-security-rules-auditor": {
|
||||
"source": "firebase/skills",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/firebase-security-rules-auditor/SKILL.md",
|
||||
"computedHash": "7d05c33d2e00c1d26cbd99ae174272fdd962e63b67c801e2fc050cc63e681723"
|
||||
},
|
||||
"notion-knowledge-capture": {
|
||||
"source": "openai/skills",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/.curated/notion-knowledge-capture/SKILL.md",
|
||||
"computedHash": "c1376422aec0fb3a4733e5a8a99eed4feff24fa45e25a2ffddaf07a68eed9198"
|
||||
},
|
||||
"xcode-project-setup": {
|
||||
"source": "firebase/skills",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/xcode-project-setup/SKILL.md",
|
||||
"computedHash": "e7ddb878c879eda7a3e462cebebcebb827d90836e4cb8a11b513e54403176e7d"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
---
|
||||
name: _template
|
||||
description: Template for creating new Agentic OS skills
|
||||
version: 1.0.0
|
||||
author: Agentic OS
|
||||
tags: [template, meta]
|
||||
---
|
||||
|
||||
# {Skill Name}
|
||||
|
||||
## Description
|
||||
Brief description of what this skill does.
|
||||
|
||||
## When to Use
|
||||
- Trigger condition 1
|
||||
- Trigger condition 2
|
||||
|
||||
## Input
|
||||
- What this skill expects as input
|
||||
|
||||
## Process
|
||||
1. Step one
|
||||
2. Step two
|
||||
3. Step three
|
||||
|
||||
## Output
|
||||
- What this skill produces
|
||||
|
||||
## Agent Assignment
|
||||
- Primary: opencode / hermes / gemini
|
||||
- Fallback: {fallback agent}
|
||||
|
||||
## Dependencies
|
||||
- Any prerequisites
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"criteria": [
|
||||
{ "name": "completeness", "weight": 0.4 },
|
||||
{ "name": "accuracy", "weight": 0.3 },
|
||||
{ "name": "clarity", "weight": 0.3 }
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
# Learnings
|
||||
|
||||
Add lessons learned from each run here.
|
||||
|
||||
## Initial Setup
|
||||
- Template created with standard skill structure
|
||||
|
|
@ -0,0 +1 @@
|
|||
[]
|
||||
|
|
@ -4,3 +4,7 @@
|
|||
- Exclude data/settings.json from backups
|
||||
- Keep last 30 backups, remove older
|
||||
- Timestamp filenames for easy sorting
|
||||
|
||||
## 2026-06-26
|
||||
- opencode timed out on backup-skill execution
|
||||
- Recommend: use hermes agent for backup-skill
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@
|
|||
- Socratic questioning reveals hidden constraints
|
||||
- 3+ alternatives before recommending
|
||||
|
||||
## 2026-07-06 (Run a3123fc4)
|
||||
- Agent: opencode
|
||||
- Input: (none)
|
||||
- Output: ⚠ Agent 'opencode' CLI not installed. Install it and try again.
|
||||
## 2026-06-23
|
||||
- opencode consistently times out on brainstorming runs (4+ attempts logged)
|
||||
- Recommend: use hermes agent (not opencode) for brainstorming tasks
|
||||
|
|
|
|||
|
|
@ -2,3 +2,24 @@
|
|||
|
||||
## 2026-05-17
|
||||
- Follows Superpowers methodology for code review
|
||||
|
||||
## 2026-06-28
|
||||
- opencode consistently times out on non-code tasks → route to hermes
|
||||
|
||||
## 2026-07-25 (Run 07aca844)
|
||||
- Agent: opencode
|
||||
- Input: Review the server.py router logic
|
||||
- Output: ⏱ Agent 'opencode' timed out.
|
||||
|
||||
OpenCode's model is taking too long. Try running `opencode run "Execute the 'code-review' skill.
|
||||
|
||||
## Skill Instructions
|
||||
---
|
||||
"` directly in your terminal.
|
||||
|
||||
**Message:** Execute the 'code-review' skill.
|
||||
|
||||
## Skill Instructions
|
||||
---
|
||||
name: code-review
|
||||
description: Automated
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
[]
|
||||
[]
|
||||
|
|
@ -4,99 +4,41 @@
|
|||
- Reads from brain/ files and audit log
|
||||
- Runs best at session start
|
||||
|
||||
## 2026-05-18 (Run 7215be12)
|
||||
## 2026-05-18
|
||||
- Multipleup runs confirm: consistent pattern of reading brain/ + audit log
|
||||
- Both opencode and hermes agents execute standup successfully
|
||||
- Known backup filenagentic-os-20260517_163610.tar.gz created & restored
|
||||
|
||||
## 2026-06-23
|
||||
- hermes agent returns partial output when session times out mid-execution
|
||||
- openerate agent reads brain/ files correctly via Read tool
|
||||
- Recommend: use `hermes chat -q "morning briefing"` for faster standup execution
|
||||
|
||||
## 2026-07-25 (Run efcfb319)
|
||||
- Agent: hermes
|
||||
- Input: (none)
|
||||
- Output: I'll execute the daily-standup skill by reading the relevant brain files and audit log.
|
||||
Here's your morning briefing:
|
||||
AGENTIC OS — Daily Standup (May 18, 2026)
|
||||
YESTERDAY (May 17):
|
||||
- Agentic OS project created from scratch — full 3-agent architecture (opencode + Hermes + Gemini CLI)
|
||||
- 51 features + 10 extras planned across 8 phases
|
||||
- Dashboard built: FastAPI backend + vanilla JS SPA
|
||||
- Skills hub seeded with 15+ skills (DevOps, content, research, coding, productivity)
|
||||
- Git auto-versioning enabled
|
||||
- Input: Generate today's standup notes
|
||||
- Output: I'll execute the daily-standup skill. Let me gather the context from the
|
||||
brain files and audit log.
|
||||
Now let me check the audit log and pending tasks (kanban/goals).
|
||||
Let me read the kanban task files to see pending work, and check today's
|
||||
journal.
|
||||
I have all the context I need. Here's today's daily standup.
|
||||
══════════════════════════════════════════════
|
||||
DAILY STANDUP — Saturday, July 25, 2026
|
||||
══════════════════════════════════════════════
|
||||
YESTERDAY / RECENT ACTIVITY (last 24h + recent log)
|
||||
- 2026
|
||||
|
||||
## 2026-05-18 (Run 2de16a6e)
|
||||
## 2026-07-25 (Run ba720ad7)
|
||||
- Agent: hermes
|
||||
- Input: (none)
|
||||
- Output: I'll execute the daily-standup skill by reading the relevant brain files and audit log.
|
||||
Daily Standup — Monday, May 18, 2026
|
||||
Yesterday (May 17)
|
||||
- Built Agentic OS from scratch — full project structure, 51 features + 10 extras
|
||||
- Configured 3-agent architecture: opencode + Hermes + Gemini CLI
|
||||
- Set up FastAPI dashboard with SPA frontend
|
||||
- Created 15+ skills following _template/ convention
|
||||
- Ran heartbeat and devops-audit skills successfully
|
||||
- Created + restored backup snapshot (agentic-os-20260517
|
||||
|
||||
## 2026-05-18 (Run 524c5285)
|
||||
- Agent: hermes
|
||||
- Input: (none)
|
||||
- Output: I'll execute the daily-standup skill by reading the relevant brain files and audit log.
|
||||
Here's your morning briefing:
|
||||
AGENTIC OS — Daily Standup (May 18, 2026)
|
||||
YESTERDAY (May 17):
|
||||
- Built Agentic OS v1.0 from scratch — full 3-agent architecture (opencode + Hermes + Gemini CLI)
|
||||
- Created 51 features + 10 extras across 7 layers
|
||||
- Set up FastAPI dashboard + SPA frontend
|
||||
- Created 15+ skills with eval scoring and learnings tracking
|
||||
- Ran heartbeat and devops-audit skills successfully
|
||||
- Tested backup
|
||||
|
||||
## 2026-05-18 (Run dacf584a)
|
||||
- Agent: hermes
|
||||
- Input: (none)
|
||||
- Output: I'll execute the daily-standup skill by reading the relevant brain files and audit log.
|
||||
Now let me check the last 24h of audit entries more carefully and get the latest entries:
|
||||
Here's your morning briefing:
|
||||
AGENTIC OS — Daily Standup (May 18, 2026)
|
||||
YESTERDAY (May 17):
|
||||
- Agentic OS project created from scratch — full 3-agent architecture (opencode + Hermes + Gemini CLI)
|
||||
- 51 features + 10 extras planned across 8 phases
|
||||
- Dashboard built: FastAPI backend + vanilla JS SPA
|
||||
- Skills hub seeded with
|
||||
|
||||
## 2026-05-18 (Run b6069709)
|
||||
- Agent: opencode
|
||||
- Input: (none)
|
||||
- Output: # Agentic OS — Daily Standup (May 18, 2026)
|
||||
|
||||
## Yesterday (May 17)
|
||||
- Built Agentic OS v1.0 from scratch — full 3-agent architecture, 7 layers, 51 features + 10 extras
|
||||
- FastAPI dashboard + SPA frontend created with 13 dashboard pages
|
||||
- 15+ skills seeded (_template/, heartbeat, devops-audit, content-draft, code-review, daily-standup, etc.)
|
||||
- All 3 agents tested: opencode, Hermes, Gemini CLI
|
||||
- Backup created & restored successfully (agentic-os-20260517_163610.tar.gz)
|
||||
- Standards discovery run comp
|
||||
|
||||
## 2026-05-18 (Run a6d21650)
|
||||
- Agent: hermes
|
||||
- Input: (none)
|
||||
- Output: Let me gather the data for today's standup.
|
||||
AGENTIC OS — Daily Standup (May 18, 2026)
|
||||
YESTERDAY (May 17):
|
||||
- Built Agentic OS v1.0 from scratch — full 3-agent architecture (opencode + Hermes + Gemini CLI)
|
||||
- 51 features + 10 extras across 7 layers
|
||||
- FastAPI dashboard + SPA frontend live
|
||||
- 15+ skills created with eval scoring and learnings tracking
|
||||
- Heartbeat + devops-audit skills ran successfully
|
||||
- Backup created and restored (agentic-os-20260517_163610.tar.gz)
|
||||
- Standards discovery ran
|
||||
- Tested
|
||||
|
||||
## 2026-05-18 (Run e1fffffb)
|
||||
- Agent: opencode
|
||||
- Input: (none)
|
||||
- Output: **Agentic OS — Daily Standup (May 18, 2026)**
|
||||
|
||||
**Yesterday (May 17)**
|
||||
- Built Agentic OS v1.0 from scratch — 3-agent architecture, 7 layers, 51+10 features
|
||||
- FastAPI dashboard + 13 SPA pages, 15+ skills seeded
|
||||
- All 3 agents tested (opencode, Hermes, Gemini CLI)
|
||||
- Backup created & restored, standards discovery run
|
||||
|
||||
**Overnight Activity**
|
||||
- Multiple daily-standup skill runs across opencode + Hermes (runs 7215be12 through a6d21650)
|
||||
- Heartbeat tested successfully
|
||||
- Chat interactions across all 3 a
|
||||
- Input: standup
|
||||
- Output: I'll execute the daily-standup skill. Let me gather context from the
|
||||
brain files, audit log, and task systems in parallel.
|
||||
Now let me check the kanban tasks, goals, and today's journal entry.
|
||||
Let me read the kanban task files to assess their status.
|
||||
Daily Standup gathered. Here's the briefing.
|
||||
══════════════════════════════════════════════
|
||||
DAILY STANDUP — Saturday, July 25, 2026
|
||||
══════════════════════════════════════════════
|
||||
YESTERDAY / RECENT ACTIVITY (last 24h + recent log)
|
||||
- 2026-07-25 17:10
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
[]
|
||||
[]
|
||||
|
|
@ -0,0 +1 @@
|
|||
../.agents/skills/firebase-ai-logic-basics
|
||||
|
|
@ -0,0 +1 @@
|
|||
../.agents/skills/firebase-app-hosting-basics
|
||||
|
|
@ -0,0 +1 @@
|
|||
../.agents/skills/firebase-auth-basics
|
||||
|
|
@ -0,0 +1 @@
|
|||
../.agents/skills/firebase-basics
|
||||
|
|
@ -0,0 +1 @@
|
|||
../.agents/skills/firebase-crashlytics
|
||||
|
|
@ -0,0 +1 @@
|
|||
../.agents/skills/firebase-data-connect
|
||||
|
|
@ -0,0 +1 @@
|
|||
../.agents/skills/firebase-firestore
|
||||
|
|
@ -0,0 +1 @@
|
|||
../.agents/skills/firebase-hosting-basics
|
||||
|
|
@ -0,0 +1 @@
|
|||
../.agents/skills/firebase-remote-config-basics
|
||||
|
|
@ -0,0 +1 @@
|
|||
../.agents/skills/firebase-security-rules-auditor
|
||||
|
|
@ -4,3 +4,6 @@
|
|||
- Always clarify goal with targeted questions first
|
||||
- Identify critical path for complex projects
|
||||
- Break into milestones, then tasks, then estimates
|
||||
|
||||
## 2026-06-28
|
||||
- opencode consistently times out on non-code tasks → route to hermes
|
||||
|
|
|
|||
|
|
@ -5,195 +5,52 @@
|
|||
- Archive decisions older than 30 days
|
||||
- Check for contradictions across skills
|
||||
|
||||
## 2026-07-05 (Run c1a36c88)
|
||||
## 2026-06-26
|
||||
- hermes timed out on memory-consolidation (model too slow)
|
||||
- Recommend: try shorter queries or check OpenRouter rate limits
|
||||
|
||||
## 2026-06-28
|
||||
- All 18 learnings.md files under 600 words — no compression needed
|
||||
- All score-history.json files empty — no pruning needed
|
||||
- recent-decisions.md: 3 archived entries (older than 30 days), 0 recent
|
||||
- No contradictions found across skill learnings
|
||||
- Stale truncated run logs cleaned from memory-consolidation, code-review, goal-planner, test-plugin
|
||||
|
||||
## 2026-06-29 (Run 2)
|
||||
- All 18 learnings.md files still under 600 words (largest: memory-consolidation at 186)
|
||||
- All 18 score-history.json files empty — no pruning needed
|
||||
- recent-decisions.md: 3 archived entries (from 2026-05-17), 0 recent — no archiving needed
|
||||
- No contradictions found across skill learnings
|
||||
- Cleaned stale truncated run log from memory-consolidation learnings.md (leftover from previous run)
|
||||
|
||||
## 2026-07-17 (Run — full)
|
||||
- Agent: hermes
|
||||
- Input: (none)
|
||||
- Output: error: uv trampoline failed to canonicalize script path
|
||||
- All 18 learnings.md files under 600 words (largest: memory-consolidation at 245) — no compression needed
|
||||
- All 18 score-history.json files empty (`[]`) — no pruning needed
|
||||
- recent-decisions.md: 3 archived entries (from 2026-05-17, already >30 days old), 0 recent — no archiving needed
|
||||
- No contradictions found across skill learnings
|
||||
- Cleaned stale truncated run log (Run 0e7b20b5) from this file — it cut off mid-line and was leftover from an earlier failed run
|
||||
|
||||
## 2026-07-06 (Run e43971ff)
|
||||
## 2026-07-17 (Run 4f8a2c19)
|
||||
- Agent: hermes
|
||||
- Input: (none)
|
||||
- Output: error: uv trampoline failed to canonicalize script path
|
||||
- 19 learnings.md files under 600 words (largest: memory-consolidation at 333) — no compression needed
|
||||
- All 19 score-history.json files empty (`[]`) — no pruning needed
|
||||
- recent-decisions.md: 3 archived entries (from 2026-05-17, >30 days old), 0 recent — no archiving needed
|
||||
- No contradictions found across skill learnings
|
||||
- Cleaned stale truncated run log (Run 82b75bcd) and stale opencode-timeout logs from firebase-hosting-basics
|
||||
|
||||
## 2026-07-06 (Run 92dadbb3)
|
||||
## 2026-07-17 (Run 5e7d871e)
|
||||
- Agent: hermes
|
||||
- Input: (none)
|
||||
- Output: error: uv trampoline failed to canonicalize script path
|
||||
- Output: ⏱ Hermes timed out.
|
||||
|
||||
## 2026-07-06 (Run 1f988d3e)
|
||||
- Agent: hermes
|
||||
- Input: (none)
|
||||
- Output: error: uv trampoline failed to canonicalize script path
|
||||
The model took too long to respond. Try a shorter query or check your OpenRouter rate limits.
|
||||
|
||||
## 2026-07-06 (Run 25512ecc)
|
||||
- Agent: hermes
|
||||
- Input: (none)
|
||||
- Output: error: uv trampoline failed to canonicalize script path
|
||||
**Message:** Execute the 'memory-consolidation' skill.
|
||||
|
||||
## 2026-07-06 (Run 161cf0c5)
|
||||
- Agent: hermes
|
||||
- Input: (none)
|
||||
- Output: **Hermes needs setup**
|
||||
|
||||
Run `hermes setup` or check your config.
|
||||
|
||||
**Details:** usage: hermes [-h]
|
||||
{help,version,init,clean,harvest,process,curate,deposit,postprocess}
|
||||
...
|
||||
hermes: error: argument subcommand: invalid choice: 'chat' (choose from 'help',
|
||||
|
||||
## 2026-07-06 (Run 53f4ae13)
|
||||
- Agent: hermes
|
||||
- Input: (none)
|
||||
- Output: **Hermes needs setup**
|
||||
|
||||
Run `hermes setup` or check your config.
|
||||
|
||||
**Details:** usage: hermes [-h]
|
||||
{help,version,init,clean,harvest,process,curate,deposit,postprocess}
|
||||
...
|
||||
hermes: error: argument subcommand: invalid choice: 'chat' (choose from 'help',
|
||||
|
||||
## 2026-07-06 (Run 0dc32925)
|
||||
- Agent: hermes
|
||||
- Input: (none)
|
||||
- Output: **Hermes needs setup**
|
||||
|
||||
Run `hermes setup` or check your config.
|
||||
|
||||
**Details:** usage: hermes [-h]
|
||||
{help,version,init,clean,harvest,process,curate,deposit,postprocess}
|
||||
...
|
||||
hermes: error: argument subcommand: invalid choice: 'chat' (choose from 'help',
|
||||
|
||||
## 2026-07-07 (Run 68a154bf)
|
||||
- Agent: hermes
|
||||
- Input: (none)
|
||||
- Output: **Hermes needs setup**
|
||||
|
||||
Run `hermes setup` or check your config.
|
||||
|
||||
**Details:** usage: hermes [-h]
|
||||
{help,version,init,clean,harvest,process,curate,deposit,postprocess}
|
||||
...
|
||||
hermes: error: argument subcommand: invalid choice: 'chat' (choose from 'help',
|
||||
|
||||
## 2026-07-07 (Run 282f0ee8)
|
||||
- Agent: hermes
|
||||
- Input: (none)
|
||||
- Output: **Hermes needs setup**
|
||||
|
||||
Run `hermes setup` or check your config.
|
||||
|
||||
**Details:** usage: hermes [-h]
|
||||
{help,version,init,clean,harvest,process,curate,deposit,postprocess}
|
||||
...
|
||||
hermes: error: argument subcommand: invalid choice: 'chat' (choose from 'help',
|
||||
|
||||
## 2026-07-07 (Run 5c5f05a6)
|
||||
- Agent: hermes
|
||||
- Input: (none)
|
||||
- Output: **Hermes needs setup**
|
||||
|
||||
Run `hermes setup` or check your config.
|
||||
|
||||
**Details:** usage: hermes [-h]
|
||||
{help,version,init,clean,harvest,process,curate,deposit,postprocess}
|
||||
...
|
||||
hermes: error: argument subcommand: invalid choice: 'chat' (choose from 'help',
|
||||
|
||||
## 2026-07-07 (Run 9f9ba9a6)
|
||||
- Agent: hermes
|
||||
- Input: (none)
|
||||
- Output: **Hermes needs setup**
|
||||
|
||||
Run `hermes setup` or check your config.
|
||||
|
||||
**Details:** usage: hermes [-h]
|
||||
{help,version,init,clean,harvest,process,curate,deposit,postprocess}
|
||||
...
|
||||
hermes: error: argument subcommand: invalid choice: 'chat' (choose from 'help',
|
||||
|
||||
## 2026-07-08 (Run 641c1cf9)
|
||||
- Agent: hermes
|
||||
- Input: (none)
|
||||
- Output: **Hermes needs setup**
|
||||
|
||||
Run `hermes setup` or check your config.
|
||||
|
||||
**Details:** usage: hermes [-h]
|
||||
{help,version,init,clean,harvest,process,curate,deposit,postprocess}
|
||||
...
|
||||
hermes: error: argument subcommand: invalid choice: 'chat' (choose from 'help',
|
||||
|
||||
## 2026-07-09 (Run 2249d632)
|
||||
- Agent: hermes
|
||||
- Input: (none)
|
||||
- Output: **Hermes needs setup**
|
||||
|
||||
Run `hermes setup` or check your config.
|
||||
|
||||
**Details:** usage: hermes [-h]
|
||||
{help,version,init,clean,harvest,process,curate,deposit,postprocess}
|
||||
...
|
||||
hermes: error: argument subcommand: invalid choice: 'chat' (choose from 'help',
|
||||
|
||||
## 2026-07-09 (Run 6e486ade)
|
||||
- Agent: hermes
|
||||
- Input: (none)
|
||||
- Output: **Hermes needs setup**
|
||||
|
||||
Run `hermes setup` or check your config.
|
||||
|
||||
**Details:** usage: hermes [-h]
|
||||
{help,version,init,clean,harvest,process,curate,deposit,postprocess}
|
||||
...
|
||||
hermes: error: argument subcommand: invalid choice: 'chat' (choose from 'help',
|
||||
|
||||
## 2026-07-10 (Run 17fdf401)
|
||||
- Agent: hermes
|
||||
- Input: (none)
|
||||
- Output: **Hermes needs setup**
|
||||
|
||||
Run `hermes setup` or check your config.
|
||||
|
||||
**Details:** usage: hermes [-h]
|
||||
{help,version,init,clean,harvest,process,curate,deposit,postprocess}
|
||||
...
|
||||
hermes: error: argument subcommand: invalid choice: 'chat' (choose from 'help',
|
||||
|
||||
## 2026-07-18 (Run e1a81794)
|
||||
- Agent: hermes
|
||||
- Input: (none)
|
||||
- Output: **Hermes needs setup**
|
||||
|
||||
Run `hermes setup` or check your config.
|
||||
|
||||
**Details:** usage: hermes [-h]
|
||||
{help,version,init,clean,harvest,process,curate,deposit,postprocess}
|
||||
...
|
||||
hermes: error: argument subcommand: invalid choice: 'chat' (choose from 'help',
|
||||
|
||||
## 2026-07-19 (Run 0faa0ff0)
|
||||
- Agent: hermes
|
||||
- Input: (none)
|
||||
- Output: **Hermes needs setup**
|
||||
|
||||
Run `hermes setup` or check your config.
|
||||
|
||||
**Details:** usage: hermes [-h]
|
||||
{help,version,init,clean,harvest,process,curate,deposit,postprocess}
|
||||
...
|
||||
hermes: error: argument subcommand: invalid choice: 'chat' (choose from 'help',
|
||||
|
||||
## 2026-07-19 (Run c63df1b3)
|
||||
- Agent: hermes
|
||||
- Input: (none)
|
||||
- Output: **Hermes needs setup**
|
||||
|
||||
Run `hermes setup` or check your config.
|
||||
|
||||
**Details:** usage: hermes [-h]
|
||||
{help,version,init,clean,harvest,process,curate,deposit,postprocess}
|
||||
...
|
||||
hermes: error: argument subcommand: invalid choice: 'chat' (choose from 'help',
|
||||
## Skill Instructions
|
||||
---
|
||||
name: memory-consolidation
|
||||
desc
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
../.agents/skills/notion-knowledge-capture
|
||||
|
|
@ -5,3 +5,7 @@
|
|||
- GREEN phase: minimal code to pass
|
||||
- REFACTOR phase: clean while tests stay green
|
||||
- Never skip RED phase
|
||||
|
||||
## 2026-06-26
|
||||
- opencode timed out 3 times on tdd-cycle execution
|
||||
- Recommend: use hermes agent for tdd-cycle skill
|
||||
|
|
|
|||
|
|
@ -0,0 +1,34 @@
|
|||
---
|
||||
name: _template
|
||||
description: Template for creating new Agentic OS skills
|
||||
version: 1.0.0
|
||||
author: Agentic OS
|
||||
tags: [template, meta]
|
||||
---
|
||||
|
||||
# {Skill Name}
|
||||
|
||||
## Description
|
||||
Brief description of what this skill does.
|
||||
|
||||
## When to Use
|
||||
- Trigger condition 1
|
||||
- Trigger condition 2
|
||||
|
||||
## Input
|
||||
- What this skill expects as input
|
||||
|
||||
## Process
|
||||
1. Step one
|
||||
2. Step two
|
||||
3. Step three
|
||||
|
||||
## Output
|
||||
- What this skill produces
|
||||
|
||||
## Agent Assignment
|
||||
- Primary: opencode / hermes / gemini
|
||||
- Fallback: {fallback agent}
|
||||
|
||||
## Dependencies
|
||||
- Any prerequisites
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"criteria": [
|
||||
{ "name": "completeness", "weight": 0.4 },
|
||||
{ "name": "accuracy", "weight": 0.3 },
|
||||
{ "name": "clarity", "weight": 0.3 }
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
# Learnings
|
||||
|
||||
Add lessons learned from each run here.
|
||||
|
||||
## Initial Setup
|
||||
- Template created with standard skill structure
|
||||
|
||||
## 2026-06-28
|
||||
- opencode timed out on test-plugin execution; non-code tasks should route to hermes
|
||||
|
|
@ -0,0 +1 @@
|
|||
[]
|
||||
|
|
@ -0,0 +1 @@
|
|||
../.agents/skills/xcode-project-setup
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
#!/usr/bin/env bash
|
||||
# Agentic OS server launcher — idempotent + fully detached.
|
||||
# Safe to run repeatedly: if the server is already listening it does nothing.
|
||||
# This is the FALLBACK path used only when systemd is unavailable
|
||||
# (e.g. a WSL instance that hasn't started the user session).
|
||||
set -u
|
||||
|
||||
DIR="/home/austin/agentic-os"
|
||||
LOG="/tmp/agentic-os.log"
|
||||
PIDFILE="$DIR/.agentic-os.pid"
|
||||
HOST="0.0.0.0"
|
||||
PORT="8080"
|
||||
|
||||
# Already serving? Then do nothing (idempotent).
|
||||
if ss -ltn 2>/dev/null | grep -q ":$PORT "; then
|
||||
echo "Agentic OS already listening on :$PORT — nothing to do."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
cd "$DIR" || { echo "FATAL: cannot cd to $DIR"; exit 1; }
|
||||
|
||||
# Remove a stale pidfile if its process is no longer alive.
|
||||
if [ -f "$PIDFILE" ]; then
|
||||
OLD="$(cat "$PIDFILE" 2>/dev/null)"
|
||||
if [ -n "$OLD" ] && ! kill -0 "$OLD" 2>/dev/null; then
|
||||
rm -f "$PIDFILE"
|
||||
fi
|
||||
fi
|
||||
|
||||
# setsid detaches into a new session so the server outlives the launching
|
||||
# shell (cron tick, session end, terminal close).
|
||||
setsid python3 server.py >> "$LOG" 2>&1 < /dev/null &
|
||||
echo "$!" > "$PIDFILE"
|
||||
echo "Launched Agentic OS (pid $(cat "$PIDFILE")) — log: $LOG"
|
||||
|
||||
# Brief wait, then verify it actually came up.
|
||||
sleep 4
|
||||
if ss -ltn 2>/dev/null | grep -q ":$PORT "; then
|
||||
echo "OK: serving on http://$HOST:$PORT"
|
||||
else
|
||||
echo "WARN: not listening after 4s — tail of $LOG:"
|
||||
tail -15 "$LOG"
|
||||
fi
|
||||
144
start.sh
144
start.sh
|
|
@ -1,62 +1,104 @@
|
|||
#!/usr/bin/env bash
|
||||
# Agentic OS Quick Start
|
||||
# Usage: ./start.sh — start server
|
||||
# ./start.sh --open — start server + open browser
|
||||
# ./start.sh --stop — stop running server
|
||||
# ./start.sh --status — check if running
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
echo "Starting Agentic OS Dashboard..."
|
||||
PORT="${PORT:-8081}"
|
||||
PID_FILE=".agentic-os.pid"
|
||||
DASHBOARD_URL="http://127.0.0.1:${PORT}"
|
||||
|
||||
# ── Handle flags ───────────────────────────────────────────────────────────
|
||||
|
||||
case "${1:-}" in
|
||||
--stop)
|
||||
if [ -f "$PID_FILE" ]; then
|
||||
PID=$(cat "$PID_FILE")
|
||||
if kill -0 "$PID" 2>/dev/null; then
|
||||
kill "$PID" && echo "Agentic OS stopped (PID $PID)."
|
||||
rm -f "$PID_FILE"
|
||||
else
|
||||
echo "Agentic OS is not running (stale PID file removed)."
|
||||
rm -f "$PID_FILE"
|
||||
fi
|
||||
else
|
||||
echo "Agentic OS is not running."
|
||||
fi
|
||||
exit 0
|
||||
;;
|
||||
--status)
|
||||
if [ -f "$PID_FILE" ] && kill -0 "$(cat "$PID_FILE")" 2>/dev/null; then
|
||||
echo "Agentic OS is running (PID $(cat "$PID_FILE")) — $DASHBOARD_URL"
|
||||
else
|
||||
echo "Agentic OS is not running."
|
||||
fi
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
|
||||
# ── Already running? ───────────────────────────────────────────────────────
|
||||
|
||||
if [ -f "$PID_FILE" ] && kill -0 "$(cat "$PID_FILE")" 2>/dev/null; then
|
||||
echo "Agentic OS is already running at $DASHBOARD_URL"
|
||||
echo "Use ./start.sh --stop to stop it first."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ── Setup ──────────────────────────────────────────────────────────────────
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
cd "$SCRIPT_DIR"
|
||||
|
||||
# Activate venv if it exists
|
||||
if [ -d "venv" ]; then
|
||||
source venv/bin/activate
|
||||
fi
|
||||
|
||||
# Install deps if needed
|
||||
if ! python3 -c "import fastapi" 2>/dev/null; then
|
||||
echo "Installing dependencies..."
|
||||
pip3 install -r requirements.txt --quiet
|
||||
fi
|
||||
|
||||
# ── Start server ───────────────────────────────────────────────────────────
|
||||
|
||||
echo ""
|
||||
echo " Agentic OS Dashboard"
|
||||
echo " ─────────────────────"
|
||||
echo " Starting on port ${PORT}..."
|
||||
echo " URL: ${DASHBOARD_URL}"
|
||||
echo " Press Ctrl+C to stop"
|
||||
echo ""
|
||||
|
||||
# Check if server.py exists
|
||||
if [ ! -f server.py ]; then
|
||||
echo "ERROR: server.py not found. Are you in the right directory?"
|
||||
exit 1
|
||||
fi
|
||||
nohup python3 server.py --port "$PORT" --host 0.0.0.0 > /tmp/agentic-os.log 2>&1 &
|
||||
echo $! > "$PID_FILE"
|
||||
|
||||
# Resolve Python
|
||||
if command -v python &>/dev/null; then
|
||||
PYTHON="python"
|
||||
elif command -v python3 &>/dev/null; then
|
||||
PYTHON="python3"
|
||||
else
|
||||
echo "ERROR: Python 3.10+ required. Install from https://www.python.org/downloads/ or your OS package manager."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check dependencies
|
||||
$PYTHON -m pip install -r requirements.txt --quiet 2>/dev/null
|
||||
|
||||
# Get port from settings or default
|
||||
CONFIGURED_PORT=$($PYTHON -c "import json; f=open('data/settings.json'); d=json.load(f); print(d.get('dashboard',{}).get('port',8080)); f.close()" 2>/dev/null || echo "8080")
|
||||
|
||||
# Find a free port starting at CONFIGURED_PORT (in case it's already taken by another app)
|
||||
PORT="$CONFIGURED_PORT"
|
||||
FOUND=0
|
||||
for _ in $(seq 1 20); do
|
||||
if ! (exec 3<>"/dev/tcp/127.0.0.1/${PORT}") 2>/dev/null; then
|
||||
FOUND=1
|
||||
break
|
||||
fi
|
||||
exec 3>&- 2>/dev/null || true
|
||||
PORT=$((PORT + 1))
|
||||
# Wait for server to be ready
|
||||
for i in $(seq 1 10); do
|
||||
if curl -s http://127.0.0.1:${PORT}/api/status > /dev/null 2>&1; then
|
||||
echo " Ready! ($DASHBOARD_URL)"
|
||||
break
|
||||
fi
|
||||
sleep 0.5
|
||||
done
|
||||
|
||||
if [ "$FOUND" -eq 0 ]; then
|
||||
echo "ERROR: Could not find a free port after 20 attempts starting at ${CONFIGURED_PORT}."
|
||||
exit 1
|
||||
fi
|
||||
# ── Open browser ───────────────────────────────────────────────────────────
|
||||
|
||||
if [ "$PORT" != "$CONFIGURED_PORT" ]; then
|
||||
echo "WARNING: Port ${CONFIGURED_PORT} is already in use - using ${PORT} instead."
|
||||
fi
|
||||
case "${1:-}" in
|
||||
--open)
|
||||
echo " Opening browser..."
|
||||
if command -v xdg-open &>/dev/null; then
|
||||
xdg-open "$DASHBOARD_URL" 2>/dev/null || true
|
||||
elif command -v open &>/dev/null; then
|
||||
open "$DASHBOARD_URL" 2>/dev/null || true
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "Dashboard: http://127.0.0.1:${PORT}"
|
||||
echo "Press Ctrl+C to stop"
|
||||
echo ""
|
||||
|
||||
# Best-effort auto-open browser once the server is up
|
||||
( sleep 2
|
||||
if command -v xdg-open &>/dev/null; then xdg-open "http://127.0.0.1:${PORT}" &>/dev/null
|
||||
elif command -v open &>/dev/null; then open "http://127.0.0.1:${PORT}" &>/dev/null
|
||||
fi
|
||||
) &
|
||||
|
||||
# Start server
|
||||
$PYTHON server.py --port "${PORT}"
|
||||
echo " Logs: tail -f /tmp/agentic-os.log"
|
||||
echo " Stop: ./start.sh --stop"
|
||||
echo ""
|
||||
|
|
|
|||
Loading…
Reference in New Issue