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
d9b1b0d9fd
commit
94ce01007d
|
|
@ -11,3 +11,8 @@ audit/*
|
||||||
data/settings.json
|
data/settings.json
|
||||||
data/chat-history.json
|
data/chat-history.json
|
||||||
data/cost-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 |
|
| Detail | Info |
|
||||||
|--------|------|
|
|--------|------|
|
||||||
| **Budget** | Strictly free tiers (GCP Free, GitHub Student Pack, Colab, Kaggle) |
|
| **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 |
|
| **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) |
|
| **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
|
## User
|
||||||
- Name: User
|
- Name: User
|
||||||
- Tech Stack: opencode, Hermes Agent, Gemini CLI, deepseek-v4-flash-free
|
- 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
|
- Budget: Free tiers only
|
||||||
|
|
||||||
## Preferences
|
## Preferences
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,25 @@
|
||||||
- Building complete agent orchestration platform
|
- Building complete agent orchestration platform
|
||||||
- 8 phases planned
|
- 8 phases planned
|
||||||
|
|
||||||
## CloudMart
|
## Kitchen-Inventory (Active)
|
||||||
- Status: Active (ongoing)
|
- Status: In development — catering kitchen management system
|
||||||
- GCP DevOps multi-region e-commerce platform
|
- Deployed on antigravity (server/host)
|
||||||
- GKE Autopilot, Cloud SQL, Cloud CDN, Istio, Next.js
|
- 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
|
||||||
|
|
||||||
|
## 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
|
||||||
|
|
||||||
|
- [Ship Agentic OS v1](goal:c24b3d85) — Complete audit and fix all issues
|
||||||
|
|
||||||
|
- [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
|
## Key Relationships
|
||||||
- User: Developer — AI/ML and DevOps enthusiast
|
- User: Developer — AI/ML and DevOps enthusiast
|
||||||
- Tool stack: opencode, Hermes Agent, Gemini CLI, deepseek-v4-flash-free
|
- 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
|
## Standing Decisions
|
||||||
- All memory is stored in markdown for cross-agent compatibility
|
- 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
|
- 3 agents configured: opencode, Hermes Agent, Gemini CLI
|
||||||
- Dashboard runs on FastAPI (Python) — localhost only
|
- Dashboard runs on FastAPI (Python) — localhost only
|
||||||
- All skills follow _template/ convention
|
- All skills follow _template/ convention
|
||||||
|
- Memory consolidation: 2026-07-17 (run — all files clean, stale truncated run log from 2026-06-29 cleaned)
|
||||||
|
|
||||||
## User
|
## User
|
||||||
- Name: User
|
- Name: User
|
||||||
|
|
@ -11,9 +12,19 @@
|
||||||
|
|
||||||
## Active Work
|
## Active Work
|
||||||
- Building Agentic OS from scratch (May 17, 2026)
|
- 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
|
## Technology Preferences
|
||||||
- Preferred model: deepseek-v4-flash-free
|
- Preferred model: deepseek-v4-flash-free
|
||||||
- Free tiers: GCP Free, GitHub Student Dev Pack, Colab, Kaggle
|
- Free tiers: GCP Free, GitHub Student Dev Pack, Colab, Kaggle
|
||||||
- Knowledge management via markdown vaults
|
- 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
|
# 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
|
- Created Agentic OS project structure
|
||||||
- 3-agent architecture: opencode (code) + Hermes (memory/channels) + Gemini CLI (research)
|
- 3-agent architecture: opencode (code) + Hermes (memory/channels) + Gemini CLI (research)
|
||||||
- Web dashboard only (no CLI dashboard) — FastAPI + vanilla JS SPA
|
- 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)
|
||||||
|
|
@ -41,7 +41,10 @@ const api = {
|
||||||
getCost: () => api.get('/api/cost'),
|
getCost: () => api.get('/api/cost'),
|
||||||
recordCost: (data) => api.post('/api/cost/record', data),
|
recordCost: (data) => api.post('/api/cost/record', data),
|
||||||
getPlugins: () => api.get('/api/plugins'),
|
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'),
|
getBackups: () => api.get('/api/backups'),
|
||||||
createBackup: () => api.post('/api/backup'),
|
createBackup: () => api.post('/api/backup'),
|
||||||
restoreBackup: (file) => api.post('/api/backup/restore', { file }),
|
restoreBackup: (file) => api.post('/api/backup/restore', { file }),
|
||||||
|
|
@ -89,4 +92,11 @@ const api = {
|
||||||
// Session Replay
|
// Session Replay
|
||||||
listSessions: () => api.get('/api/sessions/list'),
|
listSessions: () => api.get('/api/sessions/list'),
|
||||||
getSessionReplay: (id) => api.get(`/api/sessions/${encodeURIComponent(id)}/replay`),
|
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 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 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="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>
|
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.min.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
@ -33,6 +34,7 @@
|
||||||
<div class="sidebar-nav">
|
<div class="sidebar-nav">
|
||||||
<div class="sidebar-section"><div class="sidebar-section-label">Primary</div></div>
|
<div class="sidebar-section"><div class="sidebar-section-label">Primary</div></div>
|
||||||
<a href="#chat" class="nav-item" data-page="chat"><span class="nav-icon">💬</span><span class="nav-label">AI Chat</span></a>
|
<a href="#chat" class="nav-item" data-page="chat"><span class="nav-icon">💬</span><span class="nav-label">AI Chat</span></a>
|
||||||
|
<a href="#terminal" class="nav-item" data-page="terminal"><span class="nav-icon">⌨</span><span class="nav-label">Terminal</span></a>
|
||||||
<a href="#dashboard" class="nav-item active" data-page="dashboard"><span class="nav-icon">◉</span><span class="nav-label">Dashboard</span></a>
|
<a href="#dashboard" class="nav-item active" data-page="dashboard"><span class="nav-icon">◉</span><span class="nav-label">Dashboard</span></a>
|
||||||
<div class="sidebar-section"><div class="sidebar-section-label">Agents</div></div>
|
<div class="sidebar-section"><div class="sidebar-section-label">Agents</div></div>
|
||||||
<a href="#skills" class="nav-item" data-page="skills"><span class="nav-icon">⚡</span><span class="nav-label">Skills</span><span class="nav-badge" id="skillCount">0</span></a>
|
<a href="#skills" class="nav-item" data-page="skills"><span class="nav-icon">⚡</span><span class="nav-label">Skills</span><span class="nav-badge" id="skillCount">0</span></a>
|
||||||
|
|
@ -48,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="#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="#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="#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>
|
<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="#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>
|
<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() {
|
async function renderAgentHealth() {
|
||||||
const content = document.getElementById('pageContent');
|
const content = document.getElementById('pageContent');
|
||||||
content.innerHTML = `
|
content.innerHTML = `
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
<div class="page-header-left">
|
<div class="page-header-left">
|
||||||
<div class="page-title">Agent Health</div>
|
<h1 class="page-title">Agent Health</h1>
|
||||||
<div class="page-subtitle">Real-time monitoring of all 3 agents</div>
|
<p class="page-subtitle">Monitor and manage all connected agents</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="btn-group">
|
<div class="btn-group">
|
||||||
<label class="switch" title="Auto-refresh every 5s">
|
<button class="btn btn-ghost" onclick="loadAgents()">↻ Refresh</button>
|
||||||
<input type="checkbox" id="healthAutoRefresh" checked onchange="toggleHealthAutoRefresh()">
|
<button class="btn btn-primary" onclick="showAddAgentModal()">+ Add Agent</button>
|
||||||
<span class="switch-slider"></span>
|
|
||||||
</label>
|
|
||||||
<span class="text-sm text-muted">Auto</span>
|
|
||||||
<button class="btn btn-primary" onclick="refreshAgentHealth()">🔄 Refresh Now</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="agentHealthCards" class="grid grid-3" style="margin-bottom:20px">
|
<div id="agentCards" class="grid grid-3">
|
||||||
<div class="skeleton" style="height:180px"></div>
|
<div class="card"><div class="skeleton" style="height:120px"></div></div>
|
||||||
<div class="skeleton" style="height:180px"></div>
|
<div class="card"><div class="skeleton" style="height:120px"></div></div>
|
||||||
<div class="skeleton" style="height:180px"></div>
|
<div class="card"><div class="skeleton" style="height:120px"></div></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="section-title">Health Overview</div>
|
<div class="card mt-4">
|
||||||
<div class="card" id="healthOverviewCard">
|
<div class="card-header"><span class="card-title">Agent Registry</span></div>
|
||||||
<div class="loading"><div class="loading-spinner"></div><span>Loading health data...</span></div>
|
<div id="agentTable"></div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
await refreshAgentHealth();
|
await loadAgents();
|
||||||
if (document.getElementById('healthAutoRefresh')?.checked) {
|
|
||||||
startHealthAutoRefresh();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function startHealthAutoRefresh() {
|
async function loadAgents() {
|
||||||
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() {
|
|
||||||
try {
|
try {
|
||||||
const data = await api.getAgentHealth();
|
const { agents } = await api.getAgents();
|
||||||
const agents = data.agents || [];
|
const container = document.getElementById('agentCards');
|
||||||
const cards = document.getElementById('agentHealthCards');
|
const table = document.getElementById('agentTable');
|
||||||
if (!cards) return;
|
|
||||||
const agentIcons = { opencode: '🔧', hermes: '⚡', gemini: '🧠' };
|
// Card view
|
||||||
const agentColors = { opencode: 'purple', hermes: 'green', gemini: 'blue' };
|
container.innerHTML = agents.map(a => {
|
||||||
cards.innerHTML = agents.map(a => `
|
const sc = statusColor(a.status);
|
||||||
<div class="agent-health-card">
|
return `
|
||||||
<div class="agent-health-avatar" style="background:var(--${agentColors[a.name] || 'accent'}-dim);color:var(--${agentColors[a.name] || 'accent'})">
|
<div class="card agent-card">
|
||||||
${agentIcons[a.name] || '🤖'}
|
<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>
|
||||||
<div class="agent-health-info">
|
<div style="font-size:12px;color:var(--text-secondary);margin:8px 0">${escapeHtml(a.description || 'No description')}</div>
|
||||||
<div class="agent-health-name" style="text-transform:capitalize">${a.name}</div>
|
<div style="display:flex;gap:8px;align-items:center">
|
||||||
<div class="agent-health-status">
|
<span style="font-size:11px;color:${sc.text};text-transform:uppercase;font-weight:600">${a.status}</span>
|
||||||
<span class="agent-dot ${a.status === 'online' ? 'online' : a.status === 'warning' ? 'warning' : 'offline'}"></span>
|
<span style="font-size:11px;color:var(--text-muted)">${a.type || 'cli'}</span>
|
||||||
<span style="text-transform:capitalize;color:var(--text-secondary)">${a.status}</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>
|
</div>
|
||||||
|
${!a.builtin ? `<div style="margin-top:12px"><button class="btn btn-ghost btn-sm" onclick="removeAgent('${a.name}')">Remove</button></div>` : ''}
|
||||||
</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) {
|
} catch (err) {
|
||||||
const cards = document.getElementById('agentHealthCards');
|
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>`;
|
||||||
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>`;
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
if (wc) wc.textContent = words;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const status = document.getElementById('journalSaveStatus');
|
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">
|
||||||
<div class="page-header-left">
|
<div class="page-header-left">
|
||||||
<h1 class="page-title">Plugin Registry</h1>
|
<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>
|
</div>
|
||||||
<button class="btn btn-primary" onclick="showInstallPlugin()">+ Install</button>
|
<button class="btn btn-primary" onclick="showInstallPlugin()">+ Install from GitHub</button>
|
||||||
</div>
|
</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>
|
<div id="pluginList"><div class="loading"><div class="loading-spinner"></div></div></div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await api.getPlugins();
|
const [pluginsData, integrationsData] = await Promise.all([
|
||||||
const plugins = data.plugins || [];
|
api.getPlugins(),
|
||||||
const container = document.getElementById('pluginList');
|
api.getIntegrations(),
|
||||||
|
]);
|
||||||
|
|
||||||
if (plugins.length === 0) {
|
renderPluginTable(pluginsData.plugins || []);
|
||||||
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>';
|
renderIntegrations(integrationsData.integrations || []);
|
||||||
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>
|
|
||||||
`;
|
|
||||||
} catch (err) {
|
} 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>`;
|
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() {
|
function renderPluginTable(plugins) {
|
||||||
showModal('Install Plugin', `
|
const container = document.getElementById('pluginList');
|
||||||
<div class="form-group">
|
if (plugins.length === 0) {
|
||||||
<label class="form-label">Plugin Name</label>
|
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>';
|
||||||
<input id="pluginNameInput" class="form-input" placeholder="e.g., my-custom-skill">
|
return;
|
||||||
<div class="form-hint">Enter the name of the plugin to install from the registry</div>
|
}
|
||||||
|
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>
|
||||||
`, `
|
</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();
|
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 {
|
try {
|
||||||
const r = await api.installPlugin(name);
|
const r = await api.installPlugin(name || repoUrl, repoUrl);
|
||||||
closeModal();
|
statusEl.innerHTML = `<span style="color:#22c55e">✓ ${escapeHtml(r.plugin)} installed</span>`;
|
||||||
showToast(r.status === 'already_installed' ? 'Already installed' : `"${name}" installed`, r.status === 'already_installed' ? 'info' : 'success');
|
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();
|
renderPlugins();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showToast(`Error: ${err.message}`, 'error');
|
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() {
|
async function renderSmartRouter() {
|
||||||
const content = document.getElementById('pageContent');
|
const content = document.getElementById('pageContent');
|
||||||
content.innerHTML = `
|
content.innerHTML = `
|
||||||
|
|
@ -20,9 +33,7 @@ async function renderSmartRouter() {
|
||||||
<label class="form-label">Route to Agent</label>
|
<label class="form-label">Route to Agent</label>
|
||||||
<select class="form-select" id="routerAgentSelect">
|
<select class="form-select" id="routerAgentSelect">
|
||||||
<option value="auto">🤖 Auto (AI suggests)</option>
|
<option value="auto">🤖 Auto (AI suggests)</option>
|
||||||
<option value="opencode">🔧 opencode (Code/DevOps)</option>
|
<!-- agent options injected by loadRouterAgents() -->
|
||||||
<option value="hermes">⚡ Hermes (Memory/Scheduling)</option>
|
|
||||||
<option value="gemini">🧠 Gemini CLI (Research/Analysis)</option>
|
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-primary" onclick="suggestRouter()" style="margin-bottom:16px">🤖 Suggest Agent</button>
|
<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 id="routerResult"></div>
|
||||||
<div class="section-title" style="margin-top:20px">Routing Rules</div>
|
<div class="section-title" style="margin-top:20px">Routing Rules</div>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<table>
|
<table id="routerRulesTable">
|
||||||
<tr><th>Agent</th><th>Best For</th><th>Keywords</th></tr>
|
<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>
|
<!-- rows injected by loadRouterAgents() -->
|
||||||
<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>
|
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</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() {
|
async function suggestRouter() {
|
||||||
|
|
@ -49,8 +97,13 @@ async function suggestRouter() {
|
||||||
if (btn) { btn.disabled = true; btn.textContent = '⏳ Thinking...'; }
|
if (btn) { btn.disabled = true; btn.textContent = '⏳ Thinking...'; }
|
||||||
try {
|
try {
|
||||||
const data = await api.suggestRouter(task);
|
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 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)' };
|
const confidenceColors = { high: 'var(--green)', medium: 'var(--yellow)', low: 'var(--text-muted)' };
|
||||||
result.innerHTML = `
|
result.innerHTML = `
|
||||||
<div class="card" style="border-color:${confidenceColors[data.confidence] || 'var(--border)'};margin-bottom:12px">
|
<div class="card" style="border-color:${confidenceColors[data.confidence] || 'var(--border)'};margin-bottom:12px">
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,150 @@
|
||||||
|
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">Full bash shell in the browser — WSL environment</p>
|
||||||
|
</div>
|
||||||
|
<div class="btn-group">
|
||||||
|
<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-container">
|
||||||
|
<div id="terminal"></div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Load xterm.js from CDN (with fallback)
|
||||||
|
try {
|
||||||
|
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) {
|
||||||
|
// 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');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
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 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('terminal'));
|
||||||
|
fitAddon.fit();
|
||||||
|
|
||||||
|
window._agenticTerm = term;
|
||||||
|
window._agenticFit = fitAddon;
|
||||||
|
|
||||||
|
// 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');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const ws = new WebSocket(wsUrl);
|
||||||
|
window._agenticWs = ws;
|
||||||
|
|
||||||
|
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 }));
|
||||||
|
};
|
||||||
|
|
||||||
|
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 {
|
:root {
|
||||||
--bg-primary: #0d1117;
|
--bg-primary: #0d1117;
|
||||||
--bg-secondary: #151b24;
|
--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 = {
|
const PAGE_TITLES = {
|
||||||
dashboard: { title: 'Dashboard', breadcrumb: 'Overview' },
|
dashboard: { title: 'Dashboard', breadcrumb: 'Overview' },
|
||||||
|
terminal: { title: 'Terminal', breadcrumb: 'Browser-based shell' },
|
||||||
skills: { title: 'Skills Hub', breadcrumb: 'Browse & execute skills' },
|
skills: { title: 'Skills Hub', breadcrumb: 'Browse & execute skills' },
|
||||||
memory: { title: 'Memory', breadcrumb: 'Shared brain context' },
|
memory: { title: 'Memory', breadcrumb: 'Shared brain context' },
|
||||||
scheduler: { title: 'Scheduler', breadcrumb: 'Automated workflows' },
|
scheduler: { title: 'Scheduler', breadcrumb: 'Automated workflows' },
|
||||||
|
|
@ -130,4 +131,5 @@ const PAGE_TITLES = {
|
||||||
'smart-router': { title: 'Smart Router', breadcrumb: 'Task routing intelligence' },
|
'smart-router': { title: 'Smart Router', breadcrumb: 'Task routing intelligence' },
|
||||||
'learning-analytics': { title: 'Learning Analytics', breadcrumb: 'Skill improvement tracking' },
|
'learning-analytics': { title: 'Learning Analytics', breadcrumb: 'Skill improvement tracking' },
|
||||||
'session-replay': { title: 'Session Replay', breadcrumb: 'Conversation history playback' },
|
'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,13 +1,13 @@
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"id": "c1c992ef",
|
"id": "2c4b22ac",
|
||||||
"title": "Test goal",
|
"title": "Push everything in the Kanban board down the line",
|
||||||
"description": "",
|
"description": "It involves different skills to get different things done.",
|
||||||
"category": "development",
|
"category": "general",
|
||||||
"target_date": "",
|
"target_date": "2026-06-28",
|
||||||
"status": "active",
|
"status": "active",
|
||||||
"progress": 0,
|
"progress": 50,
|
||||||
"created": "2026-06-05T09:50:30.334519+00:00",
|
"created": "2026-06-28T21:48:06.763421+00:00",
|
||||||
"updated": "2026-06-05T09:50:30.334541+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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -2,11 +2,11 @@
|
||||||
"id": "0f822987",
|
"id": "0f822987",
|
||||||
"title": "Fix login bug",
|
"title": "Fix login bug",
|
||||||
"body": "The login page has a race condition",
|
"body": "The login page has a race condition",
|
||||||
"status": "todo",
|
"status": "done",
|
||||||
"priority": "high",
|
"priority": "high",
|
||||||
"assignee": "opencode",
|
"assignee": "opencode",
|
||||||
"comments": [],
|
"comments": [],
|
||||||
"links": [],
|
"links": [],
|
||||||
"created": "2026-06-05T09:52:18.236452+00:00",
|
"created": "2026-06-05T09:52:18.236452+00:00",
|
||||||
"updated": "2026-06-05T09:52:18.236473+00:00"
|
"updated": "2026-07-17T16:54:37.061957+00:00"
|
||||||
}
|
}
|
||||||
|
|
@ -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"
|
||||||
|
}
|
||||||
|
|
@ -2,13 +2,13 @@
|
||||||
"id": "8893ad14",
|
"id": "8893ad14",
|
||||||
"title": "Test kanban task",
|
"title": "Test kanban task",
|
||||||
"body": "",
|
"body": "",
|
||||||
"status": "blocked",
|
"status": "done",
|
||||||
"priority": "medium",
|
"priority": "medium",
|
||||||
"assignee": "",
|
"assignee": "",
|
||||||
"comments": [],
|
"comments": [],
|
||||||
"links": [],
|
"links": [],
|
||||||
"created": "2026-06-05T09:50:30.293643+00:00",
|
"created": "2026-06-05T09:50:30.293643+00:00",
|
||||||
"updated": "2026-06-05T09:52:18.274595+00:00",
|
"updated": "2026-07-17T17:19:04.258650+00:00",
|
||||||
"summary": "Test done",
|
"summary": "Test done",
|
||||||
"completed_at": "2026-06-05T09:50:46.473612+00:00",
|
"completed_at": "2026-06-05T09:50:46.473612+00:00",
|
||||||
"block_reason": "Waiting for API review"
|
"block_reason": "Waiting for API review"
|
||||||
|
|
|
||||||
|
|
@ -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
|
#!/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 json
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
import time
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
|
@ -13,49 +25,119 @@ except ImportError:
|
||||||
print("Install APScheduler: pip install apscheduler")
|
print("Install APScheduler: pip install apscheduler")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
import urllib.request
|
||||||
|
import urllib.error
|
||||||
|
|
||||||
BASE_DIR = Path(__file__).parent.resolve()
|
BASE_DIR = Path(__file__).parent.resolve()
|
||||||
JOBS_DIR = BASE_DIR / "jobs"
|
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"
|
audit_file = BASE_DIR.parent / "audit" / "audit.log"
|
||||||
entry = {
|
audit_file.parent.mkdir(parents=True, exist_ok=True)
|
||||||
"action": "scheduler_run",
|
entry["timestamp"] = datetime.now(timezone.utc).isoformat()
|
||||||
"skill": skill_name,
|
|
||||||
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
||||||
}
|
|
||||||
with open(audit_file, "a") as f:
|
with open(audit_file, "a") as f:
|
||||||
f.write(json.dumps(entry) + "\n")
|
f.write(json.dumps(entry) + "\n")
|
||||||
print(f"[{datetime.now().isoformat()}] Ran skill: {skill_name}")
|
|
||||||
|
|
||||||
|
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):
|
def load_jobs(scheduler: BackgroundScheduler):
|
||||||
"""Load job definitions from jobs/ directory."""
|
if not JOBS_DIR.exists():
|
||||||
for job_file in JOBS_DIR.glob("*.json"):
|
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())
|
data = json.loads(job_file.read_text())
|
||||||
|
except (json.JSONDecodeError, KeyError) as e:
|
||||||
|
print(f" Skipping malformed job file {job_file.name}: {e}")
|
||||||
|
continue
|
||||||
if not data.get("enabled", True):
|
if not data.get("enabled", True):
|
||||||
|
print(f" Skipping disabled job: {data.get('name', job_file.stem)}")
|
||||||
|
continue
|
||||||
|
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
|
continue
|
||||||
scheduler.add_job(
|
scheduler.add_job(
|
||||||
run_skill,
|
run_job,
|
||||||
CronTrigger.from_crontab(data["cron"]),
|
CronTrigger.from_crontab(cron),
|
||||||
args=[data["skill"]],
|
args=[data],
|
||||||
id=data.get("id", data["name"]),
|
id=data.get("id", data["name"]),
|
||||||
name=data["name"],
|
name=data.get("name", skill),
|
||||||
replace_existing=True,
|
replace_existing=True,
|
||||||
|
max_instances=1,
|
||||||
|
coalesce=True,
|
||||||
)
|
)
|
||||||
print(f" Scheduled: {data['name']} ({data['cron']})")
|
print(f" Scheduled: {data.get('name')} (skill={skill}, cron={cron})")
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
scheduler = BackgroundScheduler()
|
scheduler = BackgroundScheduler()
|
||||||
load_jobs(scheduler)
|
load_jobs(scheduler)
|
||||||
scheduler.start()
|
scheduler.start()
|
||||||
print(f"Agentic OS Scheduler running. Jobs loaded from: {JOBS_DIR}")
|
print(f"Agentic OS Scheduler running. Jobs loaded from: {JOBS_DIR}")
|
||||||
|
print("Server API target: " + SERVER_URL)
|
||||||
try:
|
try:
|
||||||
while True:
|
while True:
|
||||||
import time
|
|
||||||
time.sleep(60)
|
time.sleep(60)
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
scheduler.shutdown()
|
scheduler.shutdown()
|
||||||
print("Scheduler stopped.")
|
print("Scheduler stopped.")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
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()
|
||||||
|
|
@ -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
|
- Exclude data/settings.json from backups
|
||||||
- Keep last 30 backups, remove older
|
- Keep last 30 backups, remove older
|
||||||
- Timestamp filenames for easy sorting
|
- Timestamp filenames for easy sorting
|
||||||
|
|
||||||
|
## 2026-06-26
|
||||||
|
- opencode timed out on backup-skill execution
|
||||||
|
- Recommend: use hermes agent for backup-skill
|
||||||
|
|
|
||||||
|
|
@ -3,3 +3,7 @@
|
||||||
## 2026-05-17
|
## 2026-05-17
|
||||||
- Socratic questioning reveals hidden constraints
|
- Socratic questioning reveals hidden constraints
|
||||||
- 3+ alternatives before recommending
|
- 3+ alternatives before recommending
|
||||||
|
|
||||||
|
## 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
|
## 2026-05-17
|
||||||
- Follows Superpowers methodology for code review
|
- 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
|
||||||
|
|
|
||||||
|
|
@ -4,99 +4,41 @@
|
||||||
- Reads from brain/ files and audit log
|
- Reads from brain/ files and audit log
|
||||||
- Runs best at session start
|
- 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
|
- Agent: hermes
|
||||||
- Input: (none)
|
- Input: Generate today's standup notes
|
||||||
- Output: I'll execute the daily-standup skill by reading the relevant brain files and audit log.
|
- Output: I'll execute the daily-standup skill. Let me gather the context from the
|
||||||
Here's your morning briefing:
|
brain files and audit log.
|
||||||
AGENTIC OS — Daily Standup (May 18, 2026)
|
Now let me check the audit log and pending tasks (kanban/goals).
|
||||||
YESTERDAY (May 17):
|
Let me read the kanban task files to see pending work, and check today's
|
||||||
- Agentic OS project created from scratch — full 3-agent architecture (opencode + Hermes + Gemini CLI)
|
journal.
|
||||||
- 51 features + 10 extras planned across 8 phases
|
I have all the context I need. Here's today's daily standup.
|
||||||
- Dashboard built: FastAPI backend + vanilla JS SPA
|
══════════════════════════════════════════════
|
||||||
- Skills hub seeded with 15+ skills (DevOps, content, research, coding, productivity)
|
DAILY STANDUP — Saturday, July 25, 2026
|
||||||
- Git auto-versioning enabled
|
══════════════════════════════════════════════
|
||||||
|
YESTERDAY / RECENT ACTIVITY (last 24h + recent log)
|
||||||
|
- 2026
|
||||||
|
|
||||||
## 2026-05-18 (Run 2de16a6e)
|
## 2026-07-25 (Run ba720ad7)
|
||||||
- Agent: hermes
|
- Agent: hermes
|
||||||
- Input: (none)
|
- Input: standup
|
||||||
- Output: I'll execute the daily-standup skill by reading the relevant brain files and audit log.
|
- Output: I'll execute the daily-standup skill. Let me gather context from the
|
||||||
Daily Standup — Monday, May 18, 2026
|
brain files, audit log, and task systems in parallel.
|
||||||
Yesterday (May 17)
|
Now let me check the kanban tasks, goals, and today's journal entry.
|
||||||
- Built Agentic OS from scratch — full project structure, 51 features + 10 extras
|
Let me read the kanban task files to assess their status.
|
||||||
- Configured 3-agent architecture: opencode + Hermes + Gemini CLI
|
Daily Standup gathered. Here's the briefing.
|
||||||
- Set up FastAPI dashboard with SPA frontend
|
══════════════════════════════════════════════
|
||||||
- Created 15+ skills following _template/ convention
|
DAILY STANDUP — Saturday, July 25, 2026
|
||||||
- Ran heartbeat and devops-audit skills successfully
|
══════════════════════════════════════════════
|
||||||
- Created + restored backup snapshot (agentic-os-20260517
|
YESTERDAY / RECENT ACTIVITY (last 24h + recent log)
|
||||||
|
- 2026-07-25 17:10
|
||||||
## 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
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
- Always clarify goal with targeted questions first
|
||||||
- Identify critical path for complex projects
|
- Identify critical path for complex projects
|
||||||
- Break into milestones, then tasks, then estimates
|
- Break into milestones, then tasks, then estimates
|
||||||
|
|
||||||
|
## 2026-06-28
|
||||||
|
- opencode consistently times out on non-code tasks → route to hermes
|
||||||
|
|
|
||||||
|
|
@ -4,3 +4,53 @@
|
||||||
- Run weekly to keep memory files under 600 words
|
- Run weekly to keep memory files under 600 words
|
||||||
- Archive decisions older than 30 days
|
- Archive decisions older than 30 days
|
||||||
- Check for contradictions across skills
|
- Check for contradictions across skills
|
||||||
|
|
||||||
|
## 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)
|
||||||
|
- 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-17 (Run 4f8a2c19)
|
||||||
|
- Agent: hermes
|
||||||
|
- Input: (none)
|
||||||
|
- 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-17 (Run 5e7d871e)
|
||||||
|
- Agent: hermes
|
||||||
|
- Input: (none)
|
||||||
|
- Output: ⏱ Hermes timed out.
|
||||||
|
|
||||||
|
The model took too long to respond. Try a shorter query or check your OpenRouter rate limits.
|
||||||
|
|
||||||
|
**Message:** Execute the 'memory-consolidation' skill.
|
||||||
|
|
||||||
|
## Skill Instructions
|
||||||
|
---
|
||||||
|
name: memory-consolidation
|
||||||
|
desc
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
../.agents/skills/notion-knowledge-capture
|
||||||
|
|
@ -5,3 +5,7 @@
|
||||||
- GREEN phase: minimal code to pass
|
- GREEN phase: minimal code to pass
|
||||||
- REFACTOR phase: clean while tests stay green
|
- REFACTOR phase: clean while tests stay green
|
||||||
- Never skip RED phase
|
- 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
|
||||||
109
start.sh
109
start.sh
|
|
@ -1,27 +1,104 @@
|
||||||
#!/usr/bin/env bash
|
#!/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
|
set -euo pipefail
|
||||||
|
|
||||||
echo "Starting Agentic OS Dashboard..."
|
PORT="${PORT:-8081}"
|
||||||
echo ""
|
PID_FILE=".agentic-os.pid"
|
||||||
|
DASHBOARD_URL="http://127.0.0.1:${PORT}"
|
||||||
|
|
||||||
# Check if server.py exists
|
# ── Handle flags ───────────────────────────────────────────────────────────
|
||||||
if [ ! -f server.py ]; then
|
|
||||||
echo "ERROR: server.py not found. Are you in the right directory?"
|
case "${1:-}" in
|
||||||
exit 1
|
--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
|
fi
|
||||||
|
|
||||||
# Check dependencies
|
# ── Setup ──────────────────────────────────────────────────────────────────
|
||||||
pip3 install -r requirements.txt --quiet 2>/dev/null
|
|
||||||
|
|
||||||
# Get port from settings or default
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
PORT=8080
|
cd "$SCRIPT_DIR"
|
||||||
if command -v python3 &>/dev/null; then
|
|
||||||
PORT=$(python3 -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")
|
# Activate venv if it exists
|
||||||
|
if [ -d "venv" ]; then
|
||||||
|
source venv/bin/activate
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Dashboard: http://127.0.0.1:${PORT}"
|
# Install deps if needed
|
||||||
echo "Press Ctrl+C to stop"
|
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 ""
|
echo ""
|
||||||
|
|
||||||
# Start server
|
nohup python3 server.py --port "$PORT" --host 0.0.0.0 > /tmp/agentic-os.log 2>&1 &
|
||||||
python3 server.py --port "${PORT}"
|
echo $! > "$PID_FILE"
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
# ── Open browser ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
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 ""
|
||||||
|
echo " Logs: tail -f /tmp/agentic-os.log"
|
||||||
|
echo " Stop: ./start.sh --stop"
|
||||||
|
echo ""
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue