From 3d0b4d19b7ee5d772de63fa51744825a39b3e92e Mon Sep 17 00:00:00 2001 From: Thomas Marchand Date: Mon, 12 Jan 2026 22:45:05 +0000 Subject: [PATCH] Th0rgal/update branding (#32) * feat: chroots * wip * Update workspace templates and Playwright tests * Fix thinking panel close button not working during active thinking The auto-show useEffect was including showThinkingPanel in its dependency array, causing the panel to immediately reopen when closed since the state change would trigger the effect while hasActiveThinking was still true. Changed to use a ref to track previous state and only auto-show on transition from inactive to active thinking. * wip * wip * wip * Cleanup web search tool and remove hardcoded OAuth credentials * Ralph iteration 1: work in progress * Ralph iteration 2: work in progress * Ralph iteration 3: work in progress * Ralph iteration 4: work in progress * Ralph iteration 5: work in progress * Ralph iteration 6: work in progress * Ralph iteration 1: work in progress * Ralph iteration 2: work in progress * Ralph iteration 3: work in progress * Ralph iteration 4: work in progress * Ralph iteration 5: work in progress * Ralph iteration 6: work in progress * Ralph iteration 7: work in progress * Ralph iteration 1: work in progress * Ralph iteration 2: work in progress * improve readme * fix: remove unused file * feat: hero screenshot * Update README with cleaner vision and hero screenshot Simplified the vision section with "what if" framing, removed architecture diagram, added hero screenshot showing mission view. --- .claude/CLAUDE.md | 51 +- .env.example | 71 +- .gitignore | 1 + .opencode/skill/library-management/SKILL.md | 141 + .opencode/tool/library-agents.ts | 102 + .opencode/tool/library-commands.ts | 209 ++ .opencode/tool/library-git.ts | 140 + .opencode/tool/library-skills.ts | 102 + AGENTS.md | 82 +- CLAUDE.md | 76 +- Cargo.toml | 4 + INSTALL.md | 517 +++ README.md | 129 +- dashboard/bun.lock | 42 + dashboard/package.json | 4 + dashboard/playwright-report/index.html | 2 +- dashboard/src/app/agents/page.tsx | 452 --- dashboard/src/app/analytics/page.tsx | 2 +- dashboard/src/app/config/commands/page.tsx | 37 +- dashboard/src/app/config/rules/page.tsx | 32 +- dashboard/src/app/config/settings/page.tsx | 401 +++ dashboard/src/app/config/skills/page.tsx | 37 +- .../app/config/workspace-templates/page.tsx | 948 ++++++ dashboard/src/app/console/console-client.tsx | 544 ++- dashboard/src/app/control/control-client.tsx | 2989 +++++++++++++---- dashboard/src/app/extensions/mcps/page.tsx | 301 +- dashboard/src/app/extensions/plugins/page.tsx | 14 +- dashboard/src/app/extensions/tools/page.tsx | 20 +- dashboard/src/app/globals.css | 235 +- dashboard/src/app/history/page.tsx | 2 +- dashboard/src/app/layout.tsx | 23 +- dashboard/src/app/modules/page.tsx | 2 +- dashboard/src/app/page.tsx | 100 +- dashboard/src/app/settings/page.tsx | 17 +- dashboard/src/app/settings/secrets/page.tsx | 44 +- dashboard/src/app/workspaces/page.tsx | 888 ++++- .../src/components/config-code-editor.tsx | 86 + dashboard/src/components/enhanced-input.tsx | 431 +++ dashboard/src/components/markdown-content.tsx | 331 +- dashboard/src/components/mission-switcher.tsx | 426 +++ .../src/components/new-mission-dialog.tsx | 291 ++ dashboard/src/components/recent-tasks.tsx | 3 +- dashboard/src/components/sidebar.tsx | 16 +- dashboard/src/components/system-monitor.tsx | 670 ++++ dashboard/src/components/toast.tsx | 416 +++ .../src/components/ui/add-provider-modal.tsx | 31 +- dashboard/src/components/ui/copy-button.tsx | 2 +- dashboard/src/contexts/library-context.tsx | 31 +- dashboard/src/lib/api.ts | 430 ++- dashboard/src/lib/settings.ts | 12 +- dashboard/tests/agents.spec.ts | 35 +- dashboard/tests/ai-providers.spec.ts | 261 +- dashboard/tests/desktop-sessions.spec.ts | 198 ++ dashboard/tests/library.spec.ts | 85 +- dashboard/tests/minecraft-workspace.spec.ts | 144 + dashboard/tests/navigation.spec.ts | 36 +- dashboard/tests/overview.spec.ts | 16 +- dashboard/tests/secrets.spec.ts | 56 +- dashboard/tests/workspace-templates.spec.ts | 124 + dashboard/tests/workspaces.spec.ts | 24 +- .../OpenAgentDashboard/Models/Workspace.swift | 2 +- .../Views/Components/MarkdownView.swift | 329 ++ .../Views/Control/ControlView.swift | 25 +- .../Views/Control/NewMissionSheet.swift | 2 +- ios_dashboard/README.md | 6 +- screenshots/dashboard-overview.webp | Bin 0 -> 122652 bytes screenshots/hero.webp | Bin 0 -> 53322 bytes screenshots/library-skills.webp | Bin 0 -> 191296 bytes screenshots/mcp-servers.webp | Bin 0 -> 76072 bytes scripts/README.md | 3 + scripts/install_desktop.sh | 153 +- scripts/validate_skill_isolation.sh | 55 + secrets.json.example | 56 - src/agents/opencode.rs | 270 +- src/ai_providers.rs | 44 +- src/api/ai_providers.rs | 720 +++- src/api/console.rs | 740 +++- src/api/control.rs | 945 ++++-- src/api/desktop.rs | 533 +++ src/api/fs.rs | 425 +-- src/api/library.rs | 631 +++- src/api/mission_runner.rs | 93 +- src/api/mission_store/file.rs | 272 ++ src/api/mission_store/memory.rs | 215 ++ src/api/mission_store/mod.rs | 230 ++ src/api/mission_store/sqlite.rs | 901 +++++ src/api/mod.rs | 4 +- src/api/monitoring.rs | 372 ++ src/api/opencode.rs | 329 +- src/api/providers.rs | 218 +- src/api/routes.rs | 152 +- src/api/secrets.rs | 105 +- src/api/ssh_util.rs | 175 - src/api/workspaces.rs | 394 ++- src/bin/desktop_mcp.rs | 89 +- src/bin/host_mcp.rs | 288 +- src/chroot.rs | 224 -- src/config.rs | 215 +- src/lib.rs | 2 +- src/library/git.rs | 145 +- src/library/mod.rs | 458 ++- src/library/types.rs | 132 +- src/main.rs | 25 +- src/mcp/config.rs | 10 - src/mcp/registry.rs | 241 +- src/mcp/types.rs | 69 +- src/nspawn.rs | 488 +++ src/opencode/mod.rs | 487 ++- src/opencode_config.rs | 227 +- src/secrets/crypto.rs | 6 +- src/secrets/store.rs | 24 +- src/tools/browser.rs | 1220 ------- src/tools/git.rs | 259 -- src/tools/github.rs | 639 ---- src/tools/mod.rs | 31 +- src/tools/storage.rs | 101 +- src/tools/terminal.rs | 639 +++- src/tools/web.rs | 254 +- src/workspace.rs | 1286 ++++++- workspaces/mission-547aefd0/opencode.json | 39 - 120 files changed, 22049 insertions(+), 6864 deletions(-) create mode 100644 .opencode/skill/library-management/SKILL.md create mode 100644 .opencode/tool/library-agents.ts create mode 100644 .opencode/tool/library-commands.ts create mode 100644 .opencode/tool/library-git.ts create mode 100644 .opencode/tool/library-skills.ts create mode 100644 INSTALL.md delete mode 100644 dashboard/src/app/agents/page.tsx create mode 100644 dashboard/src/app/config/settings/page.tsx create mode 100644 dashboard/src/app/config/workspace-templates/page.tsx create mode 100644 dashboard/src/components/config-code-editor.tsx create mode 100644 dashboard/src/components/enhanced-input.tsx create mode 100644 dashboard/src/components/mission-switcher.tsx create mode 100644 dashboard/src/components/new-mission-dialog.tsx create mode 100644 dashboard/src/components/system-monitor.tsx create mode 100644 dashboard/src/components/toast.tsx create mode 100644 dashboard/tests/desktop-sessions.spec.ts create mode 100644 dashboard/tests/minecraft-workspace.spec.ts create mode 100644 dashboard/tests/workspace-templates.spec.ts create mode 100644 ios_dashboard/OpenAgentDashboard/Views/Components/MarkdownView.swift create mode 100644 screenshots/dashboard-overview.webp create mode 100644 screenshots/hero.webp create mode 100644 screenshots/library-skills.webp create mode 100644 screenshots/mcp-servers.webp create mode 100755 scripts/validate_skill_isolation.sh delete mode 100644 secrets.json.example create mode 100644 src/api/desktop.rs create mode 100644 src/api/mission_store/file.rs create mode 100644 src/api/mission_store/memory.rs create mode 100644 src/api/mission_store/mod.rs create mode 100644 src/api/mission_store/sqlite.rs create mode 100644 src/api/monitoring.rs delete mode 100644 src/api/ssh_util.rs delete mode 100644 src/chroot.rs create mode 100644 src/nspawn.rs delete mode 100644 src/tools/browser.rs delete mode 100644 src/tools/git.rs delete mode 100644 src/tools/github.rs delete mode 100644 workspaces/mission-547aefd0/opencode.json diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index d422bf3..5a253bc 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -1,27 +1,27 @@ -# Open Agent Panel – Project Guide +# Open Agent – Project Guide Open Agent is a managed control plane for OpenCode-based agents. The backend **does not** run model inference or autonomous logic; it delegates execution to an OpenCode server and focuses on orchestration, telemetry, and workspace/library management. ## Architecture Summary -- **Backend (Rust/Axum)**: mission orchestration, workspace/chroot management, MCP registry, Library sync. +- **Backend (Rust/Axum)**: mission orchestration, workspace/container management, MCP registry, Library sync. - **OpenCode Client**: `src/opencode/` and `src/agents/opencode.rs` (thin wrapper). - **Dashboards**: `dashboard/` (Next.js) and `ios_dashboard/` (SwiftUI). ## Core Concepts -- **Library**: Git-backed config repo (skills, commands, agents, MCPs). `src/library/`. -- **Workspaces**: Host or chroot environments with their own skills and plugins. `src/workspace.rs` manages workspace lifecycle and syncs skills to `.opencode/skill/`. +- **Library**: Git-backed config repo (skills, commands, agents, tools, rules, MCPs). `src/library/`. The default template is at [github.com/Th0rgal/openagent-library-template](https://github.com/Th0rgal/openagent-library-template). +- **Workspaces**: Host or container environments with their own skills, tools, and plugins. `src/workspace.rs` manages workspace lifecycle and syncs skills/tools to `.opencode/`. - **Missions**: Agent selection + workspace + conversation. Execution is delegated to OpenCode and streamed to the UI. ## Scoping Model -- **Global**: Auth, providers, MCPs (run on HOST machine), agents, commands -- **Per-Workspace**: Skills, plugins/hooks, installed software (chroot only), file isolation +- **Global**: Auth, providers, MCPs (run on HOST machine), agents, commands, rules +- **Per-Workspace**: Skills, tools, plugins/hooks, installed software (container only), file isolation - **Per-Mission**: Agent selection, workspace selection, conversation history -MCPs are global because they run as child processes on the host, not inside chroots. -Skills and plugins are synced to workspace `.opencode/` directories. +MCPs are global because they run as child processes on the host, not inside containers. +Skills and tools are synced to workspace `.opencode/skill/` and `.opencode/tool/` directories. ## Design Guardrails @@ -50,7 +50,40 @@ bun install bun dev ``` +## Debugging Missions + +Missions are persisted in a **SQLite database** with full event logging, enabling detailed post-mortem analysis. + +**Database location**: `~/.openagent/missions/missions.db` (or `missions-dev.db` in dev mode) + +**Retrieve events via API**: +```bash +GET /api/control/missions/{mission_id}/events +``` + +**Query parameters**: +- `types=,` – filter by event type +- `limit=` – max events to return +- `offset=` – pagination offset + +**Event types captured**: +- `user_message` – user inputs +- `thinking` – agent reasoning tokens +- `tool_call` – tool invocations (name + input) +- `tool_result` – tool outputs +- `assistant_message` – agent responses +- `mission_status_changed` – status transitions +- `error` – execution errors + +**Example**: Retrieve tool calls for a mission: +```bash +curl "http://localhost:3000/api/control/missions//events?types=tool_call,tool_result" \ + -H "Authorization: Bearer " +``` + +**Code entry points**: `src/api/mission_store/` handles persistence; `src/api/control.rs` exposes the events endpoint. + ## Notes - OpenCode config files are generated per workspace; do not keep static `opencode.json` in the repo. -- Chroot workspaces require root and Ubuntu/Debian tooling. +- Container workspaces require root and Ubuntu/Debian tooling (systemd-nspawn). diff --git a/.env.example b/.env.example index e235286..b19bf8e 100644 --- a/.env.example +++ b/.env.example @@ -2,81 +2,72 @@ # Copy this file to .env and fill in your values. # ============================================================================= -# OpenCode Backend (Required) +# OpenCode Backend # ============================================================================= OPENCODE_BASE_URL=http://127.0.0.1:4096 -# Optional OpenCode agent name (build/plan/etc) -# OPENCODE_AGENT=build -# Auto-allow all OpenCode permissions (default: true) OPENCODE_PERMISSIVE=true -# Auto-abort stuck tools after N seconds (0 = disabled) -TOOL_STUCK_ABORT_TIMEOUT_SECS=0 +# Agent/model defaults are configured in OpenCode / oh-my-opencode. +# Avoid overriding them here unless you explicitly need to. +# +# Optional: set this to the same config directory used by the OpenCode service +# when running in strong skill isolation mode (see INSTALL.md). +# OPENCODE_CONFIG_DIR=/var/lib/opencode/.config/opencode -# Default model label for telemetry / UI (optional) -DEFAULT_MODEL=claude-opus-4-5-20251101 +# Optional: abort stuck tools after N seconds (0 = disabled) +# TOOL_STUCK_ABORT_TIMEOUT_SECS=0 # ============================================================================= # Workspace + Library # ============================================================================= -# Default working directory for relative paths. -# In production this is typically `/root`. WORKING_DIR=/root -# Local library path (defaults to {WORKING_DIR}/.openagent/library) LIBRARY_PATH=/root/.openagent/library -# Remote Git URL for the library (optional) # LIBRARY_REMOTE=git@github.com:your-org/agent-library.git # ============================================================================= -# Server settings +# Server # ============================================================================= -HOST=127.0.0.1 +HOST=0.0.0.0 PORT=3000 MAX_ITERATIONS=50 STALE_MISSION_HOURS=24 MAX_PARALLEL_MISSIONS=1 # ============================================================================= -# Dashboard / API Auth (JWT) +# Auth (JWT) # ============================================================================= -# For local debugging, set DEV_MODE=true to disable auth entirely. +# Set DEV_MODE=false in production DEV_MODE=true - -# Password the dashboard submits to obtain a JWT. -# Choose something strong in real deployments. DASHBOARD_PASSWORD=change-me - -# HMAC secret used to sign/verify JWTs. Use a strong random value in production. JWT_SECRET=change-me-to-a-long-random-string - -# JWT validity in days (default: 30) JWT_TTL_DAYS=30 - -# Optional multi-user auth (JSON array) +# Multi-user auth (optional, overrides DASHBOARD_PASSWORD) # OPEN_AGENT_USERS='[{"username":"admin","password":"change-me","id":"admin"}]' # ============================================================================= -# Supabase (Optional: file sharing / screenshots) +# Dashboard Console (local shell) +# ============================================================================= +# No SSH configuration required. + +# ============================================================================= +# Optional: File Sharing / Screenshots (Supabase) # ============================================================================= # SUPABASE_URL=https://your-project.supabase.co # SUPABASE_SERVICE_ROLE_KEY=eyJ... # ============================================================================= -# Tool APIs (Optional) +# Optional: Web Search (Tavily) # ============================================================================= -# Used by the host MCP web search tool for higher quality results. +# If not set, falls back to DuckDuckGo HTML (may be blocked by CAPTCHA) # TAVILY_API_KEY=tvly-... # ============================================================================= -# Dashboard Console / File Explorer (SSH) +# Optional: Desktop Automation # ============================================================================= -# These are used by the dashboard "Console" page to: -# - open an interactive root shell (WebSocket -> PTY -> ssh) -# - list/upload/download files (SFTP) -CONSOLE_SSH_HOST=127.0.0.1 -CONSOLE_SSH_PORT=22 -CONSOLE_SSH_USER=root -# Recommended: point to a key file on disk (avoid embedding secrets in env). -CONSOLE_SSH_PRIVATE_KEY_PATH= -# base64(OpenSSH private key). Example: -# base64 -i ~/.ssh/agent.thomas.md | tr -d '\n' -CONSOLE_SSH_PRIVATE_KEY_B64= +# DESKTOP_ENABLED=true +# DESKTOP_RESOLUTION=1920x1080 +# DESKTOP_DISPLAY=:101 + +# ============================================================================= +# Optional: Secrets encryption (for stored secrets) +# ============================================================================= +# OPENAGENT_SECRET_PASSPHRASE=change-me diff --git a/.gitignore b/.gitignore index 2989a56..5c2db29 100644 --- a/.gitignore +++ b/.gitignore @@ -43,3 +43,4 @@ Thumbs.db # npm lockfile (we use bun) dashboard/package-lock.json .openagent/ +library-template/ diff --git a/.opencode/skill/library-management/SKILL.md b/.opencode/skill/library-management/SKILL.md new file mode 100644 index 0000000..3073e14 --- /dev/null +++ b/.opencode/skill/library-management/SKILL.md @@ -0,0 +1,141 @@ +--- +name: library-management +description: > + Manage the Open Agent library (skills, agents, commands, tools, rules, MCPs) via Library API tools. + Trigger terms: library, skill, agent, command, tool, rule, MCP, save skill, create skill. +--- + +# Open Agent Library Management + +The Open Agent Library is a Git-backed configuration repo that stores reusable skills, agents, +commands, tools, rules, MCP servers, and workspace templates. Use the `library-*` tools to +read and update that repo. + +## When to Use +- Creating or updating skills, agents, commands, tools, rules, or MCPs +- Syncing library git state (status/sync/commit/push) +- Updating workspace templates or plugins in the library + +## When NOT to Use +- Local file operations unrelated to the library +- Running missions or managing workspace lifecycle + +## Tool Map (file name + export) +Tool names follow the pattern `_`. + +### Skills (`library-skills.ts`) +- `library-skills_list_skills` +- `library-skills_get_skill` +- `library-skills_save_skill` +- `library-skills_delete_skill` + +### Agents (`library-agents.ts`) +- `library-agents_list_agents` +- `library-agents_get_agent` +- `library-agents_save_agent` +- `library-agents_delete_agent` + +### Commands / Tools / Rules (`library-commands.ts`) +- Commands: `library-commands_list_commands`, `library-commands_get_command`, `library-commands_save_command`, `library-commands_delete_command` +- Tools: `library-commands_list_tools`, `library-commands_get_tool`, `library-commands_save_tool`, `library-commands_delete_tool` +- Rules: `library-commands_list_rules`, `library-commands_get_rule`, `library-commands_save_rule`, `library-commands_delete_rule` + +### MCPs + Git (`library-git.ts`) +- MCPs: `library-git_get_mcps`, `library-git_save_mcps` +- Git: `library-git_status`, `library-git_sync`, `library-git_commit`, `library-git_push` + +## Procedure +1. **List** existing items +2. **Get** current content before editing +3. **Save** the full updated content (frontmatter + body) +4. **Commit** with a clear message +5. **Push** to sync the library remote + +## File Formats + +### Skill (`skill//SKILL.md`) +```yaml +--- +name: skill-name +description: What this skill does +--- +Instructions for using this skill... +``` + +### Agent (`agent/.md`) +```yaml +--- +description: Agent description +mode: primary | subagent +model: provider/model-id +hidden: true | false +color: "#44BA81" +tools: + "*": false + "read": true + "write": true +permission: + edit: ask | allow | deny + bash: + "*": ask +rules: + - rule-name +--- +Agent system prompt... +``` + +### Command (`command/.md`) +```yaml +--- +description: Command description +model: provider/model-id +subtask: true | false +agent: agent-name +--- +Command prompt template. Use $ARGUMENTS for user input. +``` + +### Tool (`tool/.ts`) +```typescript +import { tool } from "@opencode-ai/plugin" + +export const my_tool = tool({ + description: "What it does", + args: { param: tool.schema.string().describe("Param description") }, + async execute(args) { + return "result" + }, +}) +``` + +### Rule (`rule/.md`) +```yaml +--- +description: Rule description +--- +Rule instructions applied to agents referencing this rule. +``` + +### MCPs (`mcp/servers.json`) +```json +{ + "server-name": { + "type": "local", + "command": ["npx", "package-name"], + "env": { "KEY": "value" }, + "enabled": true + }, + "remote-server": { + "type": "remote", + "url": "https://mcp.example.com", + "headers": { "Authorization": "Bearer token" }, + "enabled": true + } +} +``` + +## Guardrails +- Always read before updating to avoid overwrites +- Keep names lowercase (hyphens allowed) and within 1-64 chars +- Use descriptive commit messages +- Check `library-git_status` before pushing diff --git a/.opencode/tool/library-agents.ts b/.opencode/tool/library-agents.ts new file mode 100644 index 0000000..9a022dd --- /dev/null +++ b/.opencode/tool/library-agents.ts @@ -0,0 +1,102 @@ +import { tool } from "@opencode-ai/plugin" + +// The Open Agent API URL - the backend handles library configuration internally +const API_BASE = "http://127.0.0.1:3000" + +async function apiRequest(endpoint: string, options: RequestInit = {}) { + const url = `${API_BASE}/api/library${endpoint}` + const response = await fetch(url, { + ...options, + headers: { + "Content-Type": "application/json", + ...options.headers, + }, + }) + + if (!response.ok) { + const text = await response.text() + throw new Error(`API error ${response.status}: ${text}`) + } + + const contentType = response.headers.get("content-type") + if (contentType?.includes("application/json")) { + return response.json() + } + return response.text() +} + +// ───────────────────────────────────────────────────────────────────────────── +// Library Agents +// ───────────────────────────────────────────────────────────────────────────── + +export const list_agents = tool({ + description: "List all agents in the library with their names, descriptions, modes, and models", + args: {}, + async execute() { + const agents = await apiRequest("/agent") + if (!agents || agents.length === 0) { + return "No agents found in the library." + } + return agents.map((a: { name: string; description?: string; model?: string }) => { + let line = `- ${a.name}` + if (a.description) line += `: ${a.description}` + if (a.model) line += ` (model: ${a.model})` + return line + }).join("\n") + }, +}) + +export const get_agent = tool({ + description: "Get the full content of a library agent by name, including frontmatter and system prompt", + args: { + name: tool.schema.string().describe("The agent name"), + }, + async execute(args) { + const agent = await apiRequest(`/agent/${encodeURIComponent(args.name)}`) + let result = `# Agent: ${agent.name}\n\n` + result += `**Path:** ${agent.path}\n` + if (agent.description) result += `**Description:** ${agent.description}\n` + if (agent.model) result += `**Model:** ${agent.model}\n` + + if (agent.tools && Object.keys(agent.tools).length > 0) { + result += `**Tools:** ${JSON.stringify(agent.tools)}\n` + } + if (agent.permissions && Object.keys(agent.permissions).length > 0) { + result += `**Permissions:** ${JSON.stringify(agent.permissions)}\n` + } + if (agent.rules && agent.rules.length > 0) { + result += `**Rules:** ${agent.rules.join(", ")}\n` + } + + result += `\n## Full Content (markdown file)\n\n${agent.content}` + return result + }, +}) + +export const save_agent = tool({ + description: "Create or update a library agent. Provide the full markdown content including YAML frontmatter.", + args: { + name: tool.schema.string().describe("The agent name"), + content: tool.schema.string().describe("Full markdown content with YAML frontmatter (description, mode, model, tools, permissions, etc.)"), + }, + async execute(args) { + await apiRequest(`/agent/${encodeURIComponent(args.name)}`, { + method: "PUT", + body: JSON.stringify({ content: args.content }), + }) + return `Agent '${args.name}' saved successfully. Remember to commit and push your changes.` + }, +}) + +export const delete_agent = tool({ + description: "Delete a library agent", + args: { + name: tool.schema.string().describe("The agent name to delete"), + }, + async execute(args) { + await apiRequest(`/agent/${encodeURIComponent(args.name)}`, { + method: "DELETE", + }) + return `Agent '${args.name}' deleted. Remember to commit and push your changes.` + }, +}) diff --git a/.opencode/tool/library-commands.ts b/.opencode/tool/library-commands.ts new file mode 100644 index 0000000..e316351 --- /dev/null +++ b/.opencode/tool/library-commands.ts @@ -0,0 +1,209 @@ +import { tool } from "@opencode-ai/plugin" + +// The Open Agent API URL - the backend handles library configuration internally +const API_BASE = "http://127.0.0.1:3000" + +async function apiRequest(endpoint: string, options: RequestInit = {}) { + const url = `${API_BASE}/api/library${endpoint}` + const response = await fetch(url, { + ...options, + headers: { + "Content-Type": "application/json", + ...options.headers, + }, + }) + + if (!response.ok) { + const text = await response.text() + throw new Error(`API error ${response.status}: ${text}`) + } + + const contentType = response.headers.get("content-type") + if (contentType?.includes("application/json")) { + return response.json() + } + return response.text() +} + +// ───────────────────────────────────────────────────────────────────────────── +// Commands +// ───────────────────────────────────────────────────────────────────────────── + +export const list_commands = tool({ + description: "List all commands in the library (slash commands like /commit, /test)", + args: {}, + async execute() { + const commands = await apiRequest("/command") + if (!commands || commands.length === 0) { + return "No commands found in the library." + } + return commands.map((c: { name: string; description?: string }) => + `- /${c.name}: ${c.description || "(no description)"}` + ).join("\n") + }, +}) + +export const get_command = tool({ + description: "Get the full content of a command by name", + args: { + name: tool.schema.string().describe("The command name (without the leading /)"), + }, + async execute(args) { + const command = await apiRequest(`/command/${encodeURIComponent(args.name)}`) + let result = `# Command: /${command.name}\n\n` + result += `**Path:** ${command.path}\n` + if (command.description) result += `**Description:** ${command.description}\n` + result += `\n## Content\n\n${command.content}` + return result + }, +}) + +export const save_command = tool({ + description: "Create or update a command. Provide the full markdown content including YAML frontmatter.", + args: { + name: tool.schema.string().describe("The command name (without the leading /)"), + content: tool.schema.string().describe("Full markdown content with YAML frontmatter (description, model, subtask, agent)"), + }, + async execute(args) { + await apiRequest(`/command/${encodeURIComponent(args.name)}`, { + method: "PUT", + body: JSON.stringify({ content: args.content }), + }) + return `Command '/${args.name}' saved successfully. Remember to commit and push your changes.` + }, +}) + +export const delete_command = tool({ + description: "Delete a command from the library", + args: { + name: tool.schema.string().describe("The command name to delete (without the leading /)"), + }, + async execute(args) { + await apiRequest(`/command/${encodeURIComponent(args.name)}`, { + method: "DELETE", + }) + return `Command '/${args.name}' deleted. Remember to commit and push your changes.` + }, +}) + +// ───────────────────────────────────────────────────────────────────────────── +// Library Tools +// ───────────────────────────────────────────────────────────────────────────── + +export const list_tools = tool({ + description: "List all custom tools in the library (TypeScript tool definitions)", + args: {}, + async execute() { + const tools = await apiRequest("/tool") + if (!tools || tools.length === 0) { + return "No custom tools found in the library." + } + return tools.map((t: { name: string; description?: string }) => + `- ${t.name}: ${t.description || "(no description)"}` + ).join("\n") + }, +}) + +export const get_tool = tool({ + description: "Get the full TypeScript code of a custom tool by name", + args: { + name: tool.schema.string().describe("The tool name"), + }, + async execute(args) { + const t = await apiRequest(`/tool/${encodeURIComponent(args.name)}`) + let result = `# Tool: ${t.name}\n\n` + result += `**Path:** ${t.path}\n` + if (t.description) result += `**Description:** ${t.description}\n` + result += `\n## Code\n\n\`\`\`typescript\n${t.content}\n\`\`\`` + return result + }, +}) + +export const save_tool = tool({ + description: "Create or update a custom tool in the library. Provide TypeScript code using the @opencode-ai/plugin tool() helper.", + args: { + name: tool.schema.string().describe("The tool name"), + content: tool.schema.string().describe("Full TypeScript code for the tool"), + }, + async execute(args) { + await apiRequest(`/tool/${encodeURIComponent(args.name)}`, { + method: "PUT", + body: JSON.stringify({ content: args.content }), + }) + return `Tool '${args.name}' saved successfully. Remember to commit and push your changes.` + }, +}) + +export const delete_tool = tool({ + description: "Delete a custom tool from the library", + args: { + name: tool.schema.string().describe("The tool name to delete"), + }, + async execute(args) { + await apiRequest(`/tool/${encodeURIComponent(args.name)}`, { + method: "DELETE", + }) + return `Tool '${args.name}' deleted. Remember to commit and push your changes.` + }, +}) + +// ───────────────────────────────────────────────────────────────────────────── +// Rules +// ───────────────────────────────────────────────────────────────────────────── + +export const list_rules = tool({ + description: "List all rules in the library (reusable instruction sets for agents)", + args: {}, + async execute() { + const rules = await apiRequest("/rule") + if (!rules || rules.length === 0) { + return "No rules found in the library." + } + return rules.map((r: { name: string; description?: string }) => + `- ${r.name}: ${r.description || "(no description)"}` + ).join("\n") + }, +}) + +export const get_rule = tool({ + description: "Get the full content of a rule by name", + args: { + name: tool.schema.string().describe("The rule name"), + }, + async execute(args) { + const rule = await apiRequest(`/rule/${encodeURIComponent(args.name)}`) + let result = `# Rule: ${rule.name}\n\n` + result += `**Path:** ${rule.path}\n` + if (rule.description) result += `**Description:** ${rule.description}\n` + result += `\n## Content\n\n${rule.content}` + return result + }, +}) + +export const save_rule = tool({ + description: "Create or update a rule in the library. Provide markdown content with optional YAML frontmatter.", + args: { + name: tool.schema.string().describe("The rule name"), + content: tool.schema.string().describe("Full markdown content, optionally with YAML frontmatter (description)"), + }, + async execute(args) { + await apiRequest(`/rule/${encodeURIComponent(args.name)}`, { + method: "PUT", + body: JSON.stringify({ content: args.content }), + }) + return `Rule '${args.name}' saved successfully. Remember to commit and push your changes.` + }, +}) + +export const delete_rule = tool({ + description: "Delete a rule from the library", + args: { + name: tool.schema.string().describe("The rule name to delete"), + }, + async execute(args) { + await apiRequest(`/rule/${encodeURIComponent(args.name)}`, { + method: "DELETE", + }) + return `Rule '${args.name}' deleted. Remember to commit and push your changes.` + }, +}) diff --git a/.opencode/tool/library-git.ts b/.opencode/tool/library-git.ts new file mode 100644 index 0000000..af07221 --- /dev/null +++ b/.opencode/tool/library-git.ts @@ -0,0 +1,140 @@ +import { tool } from "@opencode-ai/plugin" + +// The Open Agent API URL - the backend handles library configuration internally +const API_BASE = "http://127.0.0.1:3000" + +async function apiRequest(endpoint: string, options: RequestInit = {}) { + const url = `${API_BASE}/api/library${endpoint}` + const response = await fetch(url, { + ...options, + headers: { + "Content-Type": "application/json", + ...options.headers, + }, + }) + + if (!response.ok) { + const text = await response.text() + throw new Error(`API error ${response.status}: ${text}`) + } + + const contentType = response.headers.get("content-type") + if (contentType?.includes("application/json")) { + return response.json() + } + return response.text() +} + +// ───────────────────────────────────────────────────────────────────────────── +// Git Operations +// ───────────────────────────────────────────────────────────────────────────── + +export const status = tool({ + description: "Get the git status of the library: current branch, commits ahead/behind, and modified files", + args: {}, + async execute() { + const status = await apiRequest("/status") + let result = `# Library Git Status\n\n` + result += `**Branch:** ${status.branch || "unknown"}\n` + result += `**Remote:** ${status.remote || "not configured"}\n` + + if (status.commits_ahead !== undefined) { + result += `**Commits ahead:** ${status.commits_ahead}\n` + } + if (status.commits_behind !== undefined) { + result += `**Commits behind:** ${status.commits_behind}\n` + } + + if (status.modified_files && status.modified_files.length > 0) { + result += `\n## Modified Files\n` + result += status.modified_files.map((f: string) => `- ${f}`).join("\n") + } else { + result += `\nNo uncommitted changes.` + } + + return result + }, +}) + +export const sync = tool({ + description: "Pull latest changes from the library remote (git pull)", + args: {}, + async execute() { + await apiRequest("/sync", { method: "POST" }) + return "Library synced successfully. Latest changes pulled from remote." + }, +}) + +export const commit = tool({ + description: "Commit all changes in the library with a message", + args: { + message: tool.schema.string().describe("Commit message describing what changed"), + }, + async execute(args) { + await apiRequest("/commit", { + method: "POST", + body: JSON.stringify({ message: args.message }), + }) + return `Changes committed with message: "${args.message}"\n\nUse library-git_push to push to remote.` + }, +}) + +export const push = tool({ + description: "Push committed changes to the library remote (git push)", + args: {}, + async execute() { + await apiRequest("/push", { method: "POST" }) + return "Changes pushed to remote successfully." + }, +}) + +// ───────────────────────────────────────────────────────────────────────────── +// MCP Servers +// ───────────────────────────────────────────────────────────────────────────── + +export const get_mcps = tool({ + description: "Get all MCP server configurations from the library", + args: {}, + async execute() { + const mcps = await apiRequest("/mcps") + if (!mcps || Object.keys(mcps).length === 0) { + return "No MCP servers configured in the library." + } + + let result = "# MCP Servers\n\n" + for (const [name, config] of Object.entries(mcps)) { + const c = config as { type: string; command?: string[]; url?: string; enabled?: boolean } + result += `## ${name}\n` + result += `- Type: ${c.type}\n` + if (c.type === "local" && c.command) { + result += `- Command: \`${c.command.join(" ")}\`\n` + } + if (c.type === "remote" && c.url) { + result += `- URL: ${c.url}\n` + } + result += `- Enabled: ${c.enabled !== false}\n\n` + } + return result + }, +}) + +export const save_mcps = tool({ + description: "Save MCP server configurations to the library. Provide the full JSON object with all servers.", + args: { + servers: tool.schema.string().describe("JSON object with MCP server configurations. Each server has type (local/remote), command/url, env/headers, and enabled fields."), + }, + async execute(args) { + let parsed: Record + try { + parsed = JSON.parse(args.servers) + } catch (e) { + throw new Error(`Invalid JSON: ${e}`) + } + + await apiRequest("/mcps", { + method: "PUT", + body: JSON.stringify(parsed), + }) + return "MCP server configurations saved successfully. Remember to commit and push your changes." + }, +}) diff --git a/.opencode/tool/library-skills.ts b/.opencode/tool/library-skills.ts new file mode 100644 index 0000000..1276fac --- /dev/null +++ b/.opencode/tool/library-skills.ts @@ -0,0 +1,102 @@ +import { tool } from "@opencode-ai/plugin" + +// The Open Agent API URL - the backend handles library configuration internally +const API_BASE = "http://127.0.0.1:3000" + +async function apiRequest(endpoint: string, options: RequestInit = {}) { + const url = `${API_BASE}/api/library${endpoint}` + const response = await fetch(url, { + ...options, + headers: { + "Content-Type": "application/json", + ...options.headers, + }, + }) + + if (!response.ok) { + const text = await response.text() + throw new Error(`API error ${response.status}: ${text}`) + } + + const contentType = response.headers.get("content-type") + if (contentType?.includes("application/json")) { + return response.json() + } + return response.text() +} + +// ───────────────────────────────────────────────────────────────────────────── +// Skills +// ───────────────────────────────────────────────────────────────────────────── + +export const list_skills = tool({ + description: "List all skills in the library with their names and descriptions", + args: {}, + async execute() { + const skills = await apiRequest("/skill") + if (!skills || skills.length === 0) { + return "No skills found in the library." + } + return skills.map((s: { name: string; description?: string }) => + `- ${s.name}: ${s.description || "(no description)"}` + ).join("\n") + }, +}) + +export const get_skill = tool({ + description: "Get the full content of a skill by name, including SKILL.md and any additional files", + args: { + name: tool.schema.string().describe("The skill name (e.g., 'git-release')"), + }, + async execute(args) { + const skill = await apiRequest(`/skill/${encodeURIComponent(args.name)}`) + let result = `# Skill: ${skill.name}\n\n` + result += `**Path:** ${skill.path}\n` + if (skill.description) { + result += `**Description:** ${skill.description}\n` + } + result += `\n## SKILL.md Content\n\n${skill.content}` + + if (skill.files && skill.files.length > 0) { + result += "\n\n## Additional Files\n" + for (const file of skill.files) { + result += `\n### ${file.path}\n\n${file.content}` + } + } + + if (skill.references && skill.references.length > 0) { + result += "\n\n## Reference Files\n" + result += skill.references.map((r: string) => `- ${r}`).join("\n") + } + + return result + }, +}) + +export const save_skill = tool({ + description: "Create or update a skill in the library. Provide the full SKILL.md content including YAML frontmatter.", + args: { + name: tool.schema.string().describe("The skill name (lowercase, hyphens allowed, 1-64 chars)"), + content: tool.schema.string().describe("Full SKILL.md content including YAML frontmatter with name and description"), + }, + async execute(args) { + await apiRequest(`/skill/${encodeURIComponent(args.name)}`, { + method: "PUT", + body: JSON.stringify({ content: args.content }), + }) + return `Skill '${args.name}' saved successfully. Remember to commit and push your changes.` + }, +}) + +export const delete_skill = tool({ + description: "Delete a skill from the library", + args: { + name: tool.schema.string().describe("The skill name to delete"), + }, + async execute(args) { + await apiRequest(`/skill/${encodeURIComponent(args.name)}`, { + method: "DELETE", + }) + return `Skill '${args.name}' deleted. Remember to commit and push your changes.` + }, +}) diff --git a/AGENTS.md b/AGENTS.md index d422bf3..316cd55 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,27 +1,27 @@ -# Open Agent Panel – Project Guide +# Open Agent – Project Guide Open Agent is a managed control plane for OpenCode-based agents. The backend **does not** run model inference or autonomous logic; it delegates execution to an OpenCode server and focuses on orchestration, telemetry, and workspace/library management. ## Architecture Summary -- **Backend (Rust/Axum)**: mission orchestration, workspace/chroot management, MCP registry, Library sync. +- **Backend (Rust/Axum)**: mission orchestration, workspace/container management, MCP registry, Library sync. - **OpenCode Client**: `src/opencode/` and `src/agents/opencode.rs` (thin wrapper). - **Dashboards**: `dashboard/` (Next.js) and `ios_dashboard/` (SwiftUI). ## Core Concepts -- **Library**: Git-backed config repo (skills, commands, agents, MCPs). `src/library/`. -- **Workspaces**: Host or chroot environments with their own skills and plugins. `src/workspace.rs` manages workspace lifecycle and syncs skills to `.opencode/skill/`. +- **Library**: Git-backed config repo (skills, commands, agents, tools, rules, MCPs). `src/library/`. The default template is at [github.com/Th0rgal/openagent-library-template](https://github.com/Th0rgal/openagent-library-template). +- **Workspaces**: Host or container environments with their own skills, tools, and plugins. `src/workspace.rs` manages workspace lifecycle and syncs skills/tools to `.opencode/`. - **Missions**: Agent selection + workspace + conversation. Execution is delegated to OpenCode and streamed to the UI. ## Scoping Model -- **Global**: Auth, providers, MCPs (run on HOST machine), agents, commands -- **Per-Workspace**: Skills, plugins/hooks, installed software (chroot only), file isolation +- **Global**: Auth, providers, MCPs (run on HOST machine), agents, commands, rules +- **Per-Workspace**: Skills, tools, plugins/hooks, installed software (container only), file isolation - **Per-Mission**: Agent selection, workspace selection, conversation history -MCPs are global because they run as child processes on the host, not inside chroots. -Skills and plugins are synced to workspace `.opencode/` directories. +MCPs are global because they run as child processes on the host, not inside containers. +Skills and tools are synced to workspace `.opencode/skill/` and `.opencode/tool/` directories. ## Design Guardrails @@ -37,20 +37,68 @@ Skills and plugins are synced to workspace `.opencode/` directories. - `src/workspace.rs` – workspace lifecycle + OpenCode config generation. - `src/opencode/` – OpenCode HTTP + SSE client. -## Local Dev +## Testing + +Testing of the backend cannot be done locally as it requires Linux-specific tools (desktop MCP). Deploy as root on `95.216.112.253` (use local SSH key `cursor`). Always prefer debug builds for speed. + +Frontend workflow: the Next.js dashboard is run locally (no remote deploy). Point the local dashboard at the remote backend in Settings. + +Fast deploy loop (sync source only, build on host): ```bash -# Backend -export OPENCODE_BASE_URL="http://127.0.0.1:4096" -cargo run --release +# from macOS +rsync -az --delete \ + --exclude target --exclude .git --exclude dashboard/node_modules \ + /Users/thomas/conductor/workspaces/open_agent/vaduz-v1/ \ + root@95.216.112.253:/opt/open_agent/vaduz-v1/ -# Dashboard -cd dashboard -bun install -bun dev +# on host +cd /opt/open_agent/vaduz-v1 +cargo build --bin open_agent --bin host-mcp --bin desktop-mcp +# restart services when needed: +# - OpenCode server: `opencode.service` +# - Open Agent backend: `open_agent.service` ``` +Notes to avoid common deploy pitfalls: +- Always include the SSH key in rsync: `-e "ssh -i ~/.ssh/cursor"` (otherwise auth will fail in non-interactive shells). +- Build `host-mcp` and `desktop-mcp` too so chroot builds can copy the binaries from PATH. +- The host uses rustup; build with `source /root/.cargo/env` so the newer toolchain is on PATH. + +## Debugging Missions + +Missions are persisted in a **SQLite database** with full event logging, enabling detailed post-mortem analysis. + +**Database location**: `~/.openagent/missions/missions.db` (or `missions-dev.db` in dev mode) + +**Retrieve events via API**: +```bash +GET /api/control/missions/{mission_id}/events +``` + +**Query parameters**: +- `types=,` – filter by event type +- `limit=` – max events to return +- `offset=` – pagination offset + +**Event types captured**: +- `user_message` – user inputs +- `thinking` – agent reasoning tokens +- `tool_call` – tool invocations (name + input) +- `tool_result` – tool outputs +- `assistant_message` – agent responses +- `mission_status_changed` – status transitions +- `error` – execution errors + +**Example**: Retrieve tool calls for a mission: +```bash +curl "http://localhost:3000/api/control/missions//events?types=tool_call,tool_result" \ + -H "Authorization: Bearer " +``` + +**Code entry points**: `src/api/mission_store/` handles persistence; `src/api/control.rs` exposes the events endpoint. + ## Notes - OpenCode config files are generated per workspace; do not keep static `opencode.json` in the repo. -- Chroot workspaces require root and Ubuntu/Debian tooling. +- Container workspaces require root and Ubuntu/Debian tooling (systemd-nspawn). diff --git a/CLAUDE.md b/CLAUDE.md index d422bf3..6ae40bf 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,27 +1,27 @@ -# Open Agent Panel – Project Guide +# Open Agent – Project Guide Open Agent is a managed control plane for OpenCode-based agents. The backend **does not** run model inference or autonomous logic; it delegates execution to an OpenCode server and focuses on orchestration, telemetry, and workspace/library management. ## Architecture Summary -- **Backend (Rust/Axum)**: mission orchestration, workspace/chroot management, MCP registry, Library sync. +- **Backend (Rust/Axum)**: mission orchestration, workspace/container management, MCP registry, Library sync. - **OpenCode Client**: `src/opencode/` and `src/agents/opencode.rs` (thin wrapper). - **Dashboards**: `dashboard/` (Next.js) and `ios_dashboard/` (SwiftUI). ## Core Concepts -- **Library**: Git-backed config repo (skills, commands, agents, MCPs). `src/library/`. -- **Workspaces**: Host or chroot environments with their own skills and plugins. `src/workspace.rs` manages workspace lifecycle and syncs skills to `.opencode/skill/`. +- **Library**: Git-backed config repo (skills, commands, agents, tools, rules, MCPs). `src/library/`. The default template is at [github.com/Th0rgal/openagent-library-template](https://github.com/Th0rgal/openagent-library-template). +- **Workspaces**: Host or container environments with their own skills, tools, and plugins. `src/workspace.rs` manages workspace lifecycle and syncs skills/tools to `.opencode/`. - **Missions**: Agent selection + workspace + conversation. Execution is delegated to OpenCode and streamed to the UI. ## Scoping Model -- **Global**: Auth, providers, MCPs (run on HOST machine), agents, commands -- **Per-Workspace**: Skills, plugins/hooks, installed software (chroot only), file isolation +- **Global**: Auth, providers, MCPs (run on HOST machine), agents, commands, rules +- **Per-Workspace**: Skills, tools, plugins/hooks, installed software (container only), file isolation - **Per-Mission**: Agent selection, workspace selection, conversation history -MCPs are global because they run as child processes on the host, not inside chroots. -Skills and plugins are synced to workspace `.opencode/` directories. +MCPs are global because they run as child processes on the host, not inside containers. +Skills and tools are synced to workspace `.opencode/skill/` and `.opencode/tool/` directories. ## Design Guardrails @@ -50,7 +50,65 @@ bun install bun dev ``` +Frontend workflow: the Next.js dashboard runs locally and points at the remote backend in Settings. No remote deploy needed for frontend changes. + +## Testing + +Backend testing must happen on Linux (desktop MCP). Deploy as root on `95.216.112.253` with SSH key `cursor`. Always use debug builds for speed (never release). Frontend stays local. + +```bash +# from macOS +rsync -az --delete \ + --exclude target --exclude .git --exclude dashboard/node_modules \ + /Users/thomas/conductor/workspaces/open_agent/vaduz-v1/ \ + root@95.216.112.253:/opt/open_agent/vaduz-v1/ + +# on host +cd /opt/open_agent/vaduz-v1 +cargo build --bin open_agent +# restart services when needed: +# - OpenCode server: `opencode.service` +# - Open Agent backend: `open_agent.service` +``` + +Notes to avoid common deploy pitfalls: +- Always include the SSH key in rsync: `-e "ssh -i ~/.ssh/cursor"` (otherwise auth will fail in non-interactive shells). +- The host uses rustup; build with `source /root/.cargo/env` so the newer toolchain is on PATH. + +## Debugging Missions + +Missions are persisted in a **SQLite database** with full event logging, enabling detailed post-mortem analysis. + +**Database location**: `~/.openagent/missions/missions.db` (or `missions-dev.db` in dev mode) + +**Retrieve events via API**: +```bash +GET /api/control/missions/{mission_id}/events +``` + +**Query parameters**: +- `types=,` – filter by event type +- `limit=` – max events to return +- `offset=` – pagination offset + +**Event types captured**: +- `user_message` – user inputs +- `thinking` – agent reasoning tokens +- `tool_call` – tool invocations (name + input) +- `tool_result` – tool outputs +- `assistant_message` – agent responses +- `mission_status_changed` – status transitions +- `error` – execution errors + +**Example**: Retrieve tool calls for a mission: +```bash +curl "http://localhost:3000/api/control/missions//events?types=tool_call,tool_result" \ + -H "Authorization: Bearer " +``` + +**Code entry points**: `src/api/mission_store/` handles persistence; `src/api/control.rs` exposes the events endpoint. + ## Notes - OpenCode config files are generated per workspace; do not keep static `opencode.json` in the repo. -- Chroot workspaces require root and Ubuntu/Debian tooling. +- Container workspaces require root and Ubuntu/Debian tooling (systemd-nspawn). diff --git a/Cargo.toml b/Cargo.toml index 1dbdf80..485bfa6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,6 +43,7 @@ async-recursion = "1" # For memory/storage chrono = { version = "0.4", features = ["serde"] } +rusqlite = { version = "0.31", features = ["bundled"] } # For desktop tools (process management on Unix) libc = "0.2" @@ -58,6 +59,9 @@ rand = "0.8" # Remote console / file manager base64 = "0.22" + +# System monitoring +sysinfo = "0.32" bytes = "1" portable-pty = "0.9" md5 = "0.7" diff --git a/INSTALL.md b/INSTALL.md new file mode 100644 index 0000000..b5f50d4 --- /dev/null +++ b/INSTALL.md @@ -0,0 +1,517 @@ +# Installing Open Agent (Ubuntu 24.04, dedicated server) + +This is the installation approach currently used on a **dedicated Ubuntu 24.04 server** (OpenCode + Open Agent running on the same machine, managed by `systemd`). + +Open Agent is the orchestrator/UI backend. **It does not run model inference**; it delegates execution to an **OpenCode server** running locally (default `http://127.0.0.1:4096`). + +--- + +## 0) Assumptions + +- Ubuntu 24.04 LTS, root SSH access +- A dedicated server (not shared hosting) +- You want: + - OpenCode server bound to localhost: `127.0.0.1:4096` + - Open Agent bound to: `0.0.0.0:3000` +- You have a Git repo for your **Library** (skills/tools/agents/rules/MCP configs) + +--- + +## 1) Install base OS dependencies + +```bash +apt update +apt install -y \ + ca-certificates curl git jq unzip tar \ + build-essential pkg-config libssl-dev +``` + +If you plan to use container workspaces (systemd-nspawn), also install: + +```bash +apt install -y systemd-container debootstrap +``` + +If you plan to use **desktop automation** tools (Xvfb/i3/Chromium screenshots/OCR), install: + +```bash +apt install -y xvfb i3 x11-utils xdotool scrot imagemagick chromium chromium-sandbox tesseract-ocr +``` + +See `docs/DESKTOP_SETUP.md` for a full checklist and i3 config recommendations. + +--- + +## 2) Install Bun (for bunx + Playwright MCP) + +OpenCode is distributed as a binary, but: +- OpenCode plugins are installed internally via Bun +- Open Agent’s default Playwright MCP runner prefers `bunx` + +Install Bun: + +```bash +curl -fsSL https://bun.sh/install | bash + +# Make bun/bunx available to systemd services +install -m 0755 /root/.bun/bin/bun /usr/local/bin/bun +install -m 0755 /root/.bun/bin/bunx /usr/local/bin/bunx + +bun --version +bunx --version +``` + +--- + +## 3) Install OpenCode (server backend) + +### 3.1 Install/Update the OpenCode binary + +This installs the latest release into `~/.opencode/bin/opencode`: + +```bash +curl -fsSL https://opencode.ai/install | bash -s -- --no-modify-path +``` + +Optional: pin a version (recommended for servers): + +```bash +curl -fsSL https://opencode.ai/install | bash -s -- --version 1.1.8 --no-modify-path +``` + +Copy the binary into a stable system location used by `systemd`: + +```bash +install -m 0755 /root/.opencode/bin/opencode /usr/local/bin/opencode +opencode --version +``` + +### 3.2 Create `systemd` unit for OpenCode + +Create `/etc/systemd/system/opencode.service`: + +```ini +[Unit] +Description=OpenCode Server +After=network.target + +[Service] +Type=simple +ExecStart=/usr/local/bin/opencode serve --port 4096 --hostname 127.0.0.1 +WorkingDirectory=/root +Restart=always +RestartSec=10 +Environment=HOME=/root + +[Install] +WantedBy=multi-user.target +``` + +Enable + start: + +```bash +systemctl daemon-reload +systemctl enable --now opencode.service +``` + +Test: + +```bash +curl -fsSL http://127.0.0.1:4096/global/health | jq . +``` + +Note: Open Agent will also keep OpenCode's global config updated (MCP + tool allowlist) in: +`~/.config/opencode/opencode.json`. + +### 3.2.1 Strong workspace skill isolation (recommended) + +OpenCode discovers skills from global locations (e.g. `~/.opencode/skill`, `~/.config/opencode/skill`) +*and* from the project/mission directory `.opencode/skill`. To guarantee **per‑workspace** skill usage, +run OpenCode with an isolated HOME and keep global skill dirs empty. + +1) Create an isolated OpenCode home: + +```bash +mkdir -p /var/lib/opencode +``` + +2) Update `opencode.service` to use the isolated home: + +```ini +Environment=HOME=/var/lib/opencode +Environment=XDG_CONFIG_HOME=/var/lib/opencode/.config +Environment=XDG_DATA_HOME=/var/lib/opencode/.local/share +Environment=XDG_CACHE_HOME=/var/lib/opencode/.cache +``` + +3) Point Open Agent at the same OpenCode config dir (see section 6): + +``` +OPENCODE_CONFIG_DIR=/var/lib/opencode/.config/opencode +``` + +4) Move any old global skills out of the way (optional but recommended): + +```bash +mv /root/.opencode/skill /root/.opencode/skill.bak-$(date +%F) 2>/dev/null || true +mv /root/.config/opencode/skill /root/.config/opencode/skill.bak-$(date +%F) 2>/dev/null || true +``` + +5) Reload services: + +```bash +systemctl daemon-reload +systemctl restart opencode.service +systemctl restart open_agent.service +``` + +Validation (on the server, from the repo root): + +```bash +scripts/validate_skill_isolation.sh +``` + +### 3.3 Install oh-my-opencode (agent pack) + +Install the default agent pack as root: + +```bash +bunx oh-my-opencode install --no-tui +``` + +This installs the **Sisyphus** default agent (plus other personalities). To preserve plugin defaults: +Leave the Open Agent agent/model overrides unset to use the OpenCode / oh-my-opencode defaults. + +Update strategy: +- Pin a version in your Library `plugins.json` (e.g., `oh-my-opencode@1.2.3`) to lock updates. +- Otherwise, the plugin can auto-update via OpenCode's install hook and Open Agent sync. + +### 3.4 Install opencode-gemini-auth (optional, for Google OAuth) + +If you want to authenticate with Google accounts (Gemini plans/quotas including free tier) via OAuth instead of API keys: + +```bash +bunx opencode-gemini-auth install +``` + +This enables OAuth-based Google authentication, allowing users to leverage their existing Gemini plan directly within OpenCode. Features include: +- OAuth flow with Google accounts +- Automatic Cloud project provisioning +- Support for thinking capabilities (Gemini 2.5/3) + +To authenticate via CLI (useful for testing): + +```bash +opencode auth login +# Select Google provider, then "OAuth with Google (Gemini CLI)" +``` + +For dashboard OAuth integration, see the Settings page which handles this flow via the API. + +--- + +## 4) Install Open Agent (Rust backend) + +### 4.1 Install Rust toolchain + +```bash +curl -fsSL https://sh.rustup.rs | sh -s -- -y +source /root/.cargo/env +rustc --version +cargo --version +``` + +### 4.2 Deploy the repository + +On the server we keep the repo under: + +```bash +mkdir -p /opt/open_agent +cd /opt/open_agent +git clone vaduz-v1 +``` + +If you develop locally, a fast deploy loop is to `rsync` source to the server and build on the server (debug builds are much faster): + +```bash +rsync -az --delete \ + --exclude target --exclude .git --exclude dashboard/node_modules \ + -e "ssh -i ~/.ssh/cursor" \ + /path/to/vaduz-v1/ \ + root@:/opt/open_agent/vaduz-v1/ +``` + +### 4.3 Build and install binaries + +```bash +cd /opt/open_agent/vaduz-v1 +source /root/.cargo/env + +# Debug build (fast) - recommended for rapid iteration +cargo build --bin open_agent --bin host-mcp --bin desktop-mcp +install -m 0755 target/debug/open_agent /usr/local/bin/open_agent +install -m 0755 target/debug/host-mcp /usr/local/bin/host-mcp +install -m 0755 target/debug/desktop-mcp /usr/local/bin/desktop-mcp + +# Or: Release build (slower compile, faster runtime) +# cargo build --release --bin open_agent --bin host-mcp --bin desktop-mcp +# install -m 0755 target/release/open_agent /usr/local/bin/open_agent +# install -m 0755 target/release/host-mcp /usr/local/bin/host-mcp +# install -m 0755 target/release/desktop-mcp /usr/local/bin/desktop-mcp +``` + +--- + +## 5) Bootstrap the Library (config repo) + +Open Agent expects a git-backed **Library** repo. At runtime it will: +- clone it into `LIBRARY_PATH` (default: `{WORKING_DIR}/.openagent/library`) +- ensure the `origin` remote matches `LIBRARY_REMOTE` +- pull/sync as needed + +### 5.1 Create your own library repo from the template + +Template: +- https://github.com/Th0rgal/openagent-library-template + +One way to bootstrap: + +```bash +# On your machine +git clone git@github.com:Th0rgal/openagent-library-template.git openagent-library +cd openagent-library + +# Point it at your own repo +git remote set-url origin git@github.com:/.git + +# Push to your remote (choose main/master as you prefer) +git push -u origin HEAD:main +``` + +### 5.2 Configure Open Agent to use it + +Set in `/etc/open_agent/open_agent.env`: +- `LIBRARY_REMOTE=git@github.com:/.git` +- optional: `LIBRARY_PATH=/root/.openagent/library` + +--- + +## 6) Configure Open Agent (env file) + +Create `/etc/open_agent/open_agent.env`: + +```bash +mkdir -p /etc/open_agent +chmod 700 /etc/open_agent +``` + +Example (fill in your real values): + +```bash +cat > /etc/open_agent/open_agent.env <<'EOF' +# OpenCode backend (must match opencode.service) +OPENCODE_BASE_URL=http://127.0.0.1:4096 +OPENCODE_PERMISSIVE=true +# Optional: keep Open Agent writing OpenCode global config into the isolated home +# (recommended if you enabled strong workspace skill isolation in section 3.2.1). +# OPENCODE_CONFIG_DIR=/var/lib/opencode/.config/opencode + +# Server bind +HOST=0.0.0.0 +PORT=3000 + +# Default filesystem root for Open Agent (agent still has full system access) +WORKING_DIR=/root +LIBRARY_PATH=/root/.openagent/library +LIBRARY_REMOTE=git@github.com:/.git + +# Auth (set DEV_MODE=false on real deployments) +DEV_MODE=false +DASHBOARD_PASSWORD=change-me +JWT_SECRET=change-me-to-a-long-random-string +JWT_TTL_DAYS=30 + +# Dashboard Console (local shell) +# No SSH configuration required. + +# Default model (provider/model). If omitted or not in provider/model format, +# Open Agent won’t force a model and OpenCode will use its own defaults. + +# Desktop tools (optional) +DESKTOP_ENABLED=true +DESKTOP_RESOLUTION=1920x1080 +EOF +``` + +--- + +## 7) Create `systemd` unit for Open Agent + +Create `/etc/systemd/system/open_agent.service`: + +```ini +[Unit] +Description=OpenAgent (managed control plane) +After=network-online.target +Wants=network-online.target + +[Service] +Type=simple +User=root +Group=root +EnvironmentFile=/etc/open_agent/open_agent.env +WorkingDirectory=/root +ExecStart=/usr/local/bin/open_agent +Restart=on-failure +RestartSec=2 + +# Agent needs full system access, minimal hardening +NoNewPrivileges=false +PrivateTmp=false +ProtectHome=false + +[Install] +WantedBy=multi-user.target +``` + +--- + +## 8) Optional: Tailscale exit-node workspaces (residential IP) + +If you want a **workspace** to egress via a residential IP, the recommended pattern is: +1) Run a Tailscale **exit node** at home. +2) Use a workspace template that installs and starts Tailscale inside the container. + +### 8.1 Enable the exit node at home +On the home server: + +```bash +tailscale up --advertise-exit-node +``` + +Approve it in the Tailscale admin console (Machines → your node → “Approve exit node”). + +### 8.2 Use the `residential` workspace template +This repo ships a sample template at: + +``` +library-template/workspace-template/residential.json +``` + +It installs Tailscale and adds helper scripts: +- `openagent-network-up` (brings up host0 veth + DHCP + DNS) +- `openagent-tailscale-up` (starts tailscaled + sets exit node) +- `openagent-tailscale-check` (prints Tailscale status + public IP) + +Set these **workspace env vars** (not global env): +- `TS_AUTHKEY` (auth key for that workspace) +- `TS_EXIT_NODE` (node name like `umbrel` or its 100.x IP) +- Optional: `TS_ACCEPT_DNS=true|false`, `TS_EXIT_NODE_ALLOW_LAN=false`, `TS_STATE_DIR=/var/lib/tailscale` + +Then inside the workspace: + +```bash +openagent-tailscale-up +openagent-tailscale-check +``` + +If the public IP matches your home ISP, the exit node is working. + +### 8.3 Host NAT for veth networking (required) +`systemd-nspawn --network-veth` needs DHCP + NAT on the host. Without this, containers +won’t reach the internet or Tailscale control plane. + +Create an override for `ve-*` interfaces: + +```bash +cat >/etc/systemd/network/80-container-ve.network <<'EOF' +[Match] +Name=ve-* + +[Network] +Address=10.88.0.1/24 +DHCPServer=yes +EOF + +systemctl restart systemd-networkd +``` + +Enable forwarding + NAT (replace `` with your public interface, e.g. `enp0s31f6`): + +```bash +sysctl -w net.ipv4.ip_forward=1 + +iptables -t nat -A POSTROUTING -s 10.88.0.0/24 -o -j MASQUERADE +iptables -A FORWARD -s 10.88.0.0/24 -o -j ACCEPT +iptables -A FORWARD -d 10.88.0.0/24 -m state --state ESTABLISHED,RELATED -i -j ACCEPT +``` + +Persist the iptables rules using `iptables-persistent` (or migrate to nftables). + +### 8.4 Notes for container workspaces +Tailscale inside a container requires: +- `/dev/net/tun` bound into the container +- `CAP_NET_ADMIN` +- A private network namespace (not host network) + +If those aren’t enabled, Tailscale will fail or affect the host instead of the workspace. + +Enable + start: + +```bash +systemctl daemon-reload +systemctl enable --now open_agent.service +``` + +Test: + +```bash +curl -fsSL http://127.0.0.1:3000/api/health | jq . +``` + +--- + +## 8) Optional: Desktop automation dependencies + +If you want browser/desktop automation on Ubuntu, run: + +```bash +cd /opt/open_agent/vaduz-v1 +bash scripts/install_desktop.sh +``` + +Or follow `docs/DESKTOP_SETUP.md`. + +--- + +## 9) Updating + +### 9.1 Update Open Agent (build on server, restart service) + +```bash +cd /opt/open_agent/vaduz-v1 +git pull +source /root/.cargo/env +cargo build --bin open_agent --bin host-mcp --bin desktop-mcp +install -m 0755 target/debug/open_agent /usr/local/bin/open_agent +install -m 0755 target/debug/host-mcp /usr/local/bin/host-mcp +install -m 0755 target/debug/desktop-mcp /usr/local/bin/desktop-mcp +systemctl restart open_agent.service +``` + +### 9.2 Update OpenCode (replace binary, restart service) + +```bash +# Optionally pin a version +curl -fsSL https://opencode.ai/install | bash -s -- --version 1.1.8 --no-modify-path +install -m 0755 /root/.opencode/bin/opencode /usr/local/bin/opencode +systemctl restart opencode.service +curl -fsSL http://127.0.0.1:4096/global/health | jq . +``` + +## Suggested improvements + +- Put Open Agent behind a reverse proxy (Caddy/Nginx) with TLS and restrict who can reach `:3000`. +- Set `DEV_MODE=false` in production and use strong JWT secrets / multi-user auth. +- Run OpenCode on localhost only (already recommended) and keep it firewalled. +- Pin OpenCode/plugin versions for reproducible deployments. diff --git a/README.md b/README.md index e446460..abcaaa2 100644 --- a/README.md +++ b/README.md @@ -1,79 +1,112 @@ -# Open Agent Panel +

+ Open Agent +

-A managed control panel for OpenCode-based agents. Install it on your server to run missions in isolated workspaces, stream live telemetry to the dashboards, and keep all agent configs synced through a Git-backed Library. +

Open Agent

-## What it does +

+ Self-hosted control plane for AI autonomous agents
+ Isolated Linux workspaces and git-backed Library configuration +

-- **Mission control**: start, stop, and monitor missions on a remote machine. -- **Workspace isolation**: host or chroot workspaces with per-mission directories. -- **Library sync**: Git-backed configs for skills, commands, agents, and MCPs. -- **Provider management**: manage OpenCode auth/providers from the dashboard. -- **Live telemetry**: stream thinking/tool events to web and iOS clients. +

+ Vision · + Features · + Ecosystem · + Screenshots · + Getting Started +

-## Architecture +
-1. **Backend (Rust/Axum)** - - Manages workspaces + chroot lifecycle. - - Syncs skills and plugins to workspace `.opencode/` directories. - - Writes OpenCode workspace config (per-mission `opencode.json`). - - Delegates execution to an OpenCode server and streams events. - - Syncs the Library repo. +

+ Open Agent Dashboard +

-2. **Web dashboard (Next.js)** - - Mission timeline, logs, and controls. - - Library editor and MCP management. - - Workspace and agent configuration. +--- -3. **iOS dashboard (SwiftUI)** - - Mission monitoring on the go. - - Picture-in-Picture for desktop automation. +## Vision -## Key concepts +What if you could: -- **Library**: Git repo containing agent configs (skills, commands, MCPs, tools). -- **Workspaces**: Execution environments (host or chroot) with their own skills and plugins. Skills are synced to `.opencode/skill/` for OpenCode to discover. -- **Agents**: Library-defined capabilities (model, permissions, rules). Selected per-mission. -- **Missions**: Agent selection + workspace + conversation with streaming telemetry. -- **MCPs**: Global MCP servers run on the host machine (not inside chroots). +**Hand off entire dev cycles.** Point an agent at a GitHub issue, let it write code, test by launching a Minecraft server, and open a PR when tests pass. You review the diff, not the process. -## Quick start +**Run multi-day operations unattended.** Give an agent SSH access to your home GPU through a VPN. It reads Nvidia docs, sets up training, fine-tunes models while you sleep. + +**Keep sensitive data local.** Analyze your sequenced DNA against scientific literature. Local inference, isolated containers, nothing leaves your machines. + +--- + +## Features + +- **Mission Control**: Start, stop, and monitor agents remotely with real-time streaming +- **Isolated Workspaces**: Containerized Linux environments (systemd-nspawn) with per-mission directories +- **Git-backed Library**: Skills, tools, rules, agents, and MCPs versioned in a single repo +- **MCP Registry**: Global MCP servers running on the host, available to all workspaces +- **Multi-platform**: Web dashboard (Next.js) and iOS app (SwiftUI) with Picture-in-Picture + +--- + +## Ecosystem + +Open Agent is a control plane for [**OpenCode**](https://github.com/anomalyco/opencode), the open-source AI coding agent. It delegates all model inference and autonomous execution to OpenCode while handling orchestration, workspace isolation, and configuration management. + +Works great with [**oh-my-opencode**](https://github.com/code-yeongyu/oh-my-opencode) for enhanced agent capabilities and prebuilt skill packs. + +--- + +## Screenshots + +

+ Dashboard Overview +

+

Real-time monitoring with CPU, memory, network graphs and mission timeline

+ +
+ +

+ Library Skills Editor +

+

Git-backed Library with skills, commands, rules, and inline editing

+ +
+ +

+ MCP Servers +

+

MCP server management with runtime status and Library integration

+ +--- + +## Getting Started ### Prerequisites - Rust 1.75+ -- Bun 1.0+ (dashboard) -- An OpenCode server reachable from the backend -- Ubuntu/Debian recommended if you need chroot workspaces +- Bun 1.0+ +- [OpenCode](https://github.com/anomalyco/opencode) server +- Linux host (Ubuntu/Debian for container workspaces) ### Backend ```bash -# Required: OpenCode endpoint export OPENCODE_BASE_URL="http://127.0.0.1:4096" - -# Optional defaults -export DEFAULT_MODEL="claude-opus-4-5-20251101" -export WORKING_DIR="/root" -export LIBRARY_REMOTE="git@github.com:your-org/agent-library.git" - cargo run --release ``` -### Web dashboard +### Dashboard ```bash cd dashboard bun install bun dev ``` -Open `http://localhost:3001`. -### iOS app -Open `ios_dashboard` in Xcode and run on a device or simulator. +Open `http://localhost:3001` -## Repository layout +--- -- `src/` — Rust backend -- `dashboard/` — Next.js web app -- `ios_dashboard/` — SwiftUI iOS app -- `docs/` — ops + setup docs +## Status + +**Work in Progress** — This project is under active development. Contributions and feedback welcome. ## License + MIT diff --git a/dashboard/bun.lock b/dashboard/bun.lock index 190727c..cda5498 100644 --- a/dashboard/bun.lock +++ b/dashboard/bun.lock @@ -6,17 +6,21 @@ "name": "dashboard", "dependencies": { "@radix-ui/react-slot": "^1.2.4", + "@types/prismjs": "^1.26.5", "@types/react-syntax-highlighter": "^15.5.13", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "framer-motion": "^12.23.26", "lucide-react": "^0.561.0", "next": "16.0.10", + "prismjs": "^1.30.0", "react": "19.2.1", "react-dom": "19.2.1", "react-markdown": "^10.1.0", + "react-simple-code-editor": "^0.14.1", "react-syntax-highlighter": "^16.1.0", "recharts": "^3.6.0", + "remark-gfm": "^4.0.1", "sonner": "^2.0.7", "tailwind-merge": "^3.4.0", "xterm": "^5.3.0", @@ -802,10 +806,26 @@ "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + "markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="], + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], + "mdast-util-find-and-replace": ["mdast-util-find-and-replace@3.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "escape-string-regexp": "^5.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg=="], + "mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA=="], + "mdast-util-gfm": ["mdast-util-gfm@3.1.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-gfm-autolink-literal": "^2.0.0", "mdast-util-gfm-footnote": "^2.0.0", "mdast-util-gfm-strikethrough": "^2.0.0", "mdast-util-gfm-table": "^2.0.0", "mdast-util-gfm-task-list-item": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ=="], + + "mdast-util-gfm-autolink-literal": ["mdast-util-gfm-autolink-literal@2.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "ccount": "^2.0.0", "devlop": "^1.0.0", "mdast-util-find-and-replace": "^3.0.0", "micromark-util-character": "^2.0.0" } }, "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ=="], + + "mdast-util-gfm-footnote": ["mdast-util-gfm-footnote@2.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0" } }, "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ=="], + + "mdast-util-gfm-strikethrough": ["mdast-util-gfm-strikethrough@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg=="], + + "mdast-util-gfm-table": ["mdast-util-gfm-table@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "markdown-table": "^3.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg=="], + + "mdast-util-gfm-task-list-item": ["mdast-util-gfm-task-list-item@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ=="], + "mdast-util-mdx-expression": ["mdast-util-mdx-expression@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ=="], "mdast-util-mdx-jsx": ["mdast-util-mdx-jsx@3.2.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "parse-entities": "^4.0.0", "stringify-entities": "^4.0.0", "unist-util-stringify-position": "^4.0.0", "vfile-message": "^4.0.0" } }, "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q=="], @@ -826,6 +846,20 @@ "micromark-core-commonmark": ["micromark-core-commonmark@2.0.3", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-destination": "^2.0.0", "micromark-factory-label": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-title": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-html-tag-name": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg=="], + "micromark-extension-gfm": ["micromark-extension-gfm@3.0.0", "", { "dependencies": { "micromark-extension-gfm-autolink-literal": "^2.0.0", "micromark-extension-gfm-footnote": "^2.0.0", "micromark-extension-gfm-strikethrough": "^2.0.0", "micromark-extension-gfm-table": "^2.0.0", "micromark-extension-gfm-tagfilter": "^2.0.0", "micromark-extension-gfm-task-list-item": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w=="], + + "micromark-extension-gfm-autolink-literal": ["micromark-extension-gfm-autolink-literal@2.1.0", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw=="], + + "micromark-extension-gfm-footnote": ["micromark-extension-gfm-footnote@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw=="], + + "micromark-extension-gfm-strikethrough": ["micromark-extension-gfm-strikethrough@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw=="], + + "micromark-extension-gfm-table": ["micromark-extension-gfm-table@2.1.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg=="], + + "micromark-extension-gfm-tagfilter": ["micromark-extension-gfm-tagfilter@2.0.0", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg=="], + + "micromark-extension-gfm-task-list-item": ["micromark-extension-gfm-task-list-item@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw=="], + "micromark-factory-destination": ["micromark-factory-destination@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA=="], "micromark-factory-label": ["micromark-factory-label@2.0.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg=="], @@ -954,6 +988,8 @@ "react-redux": ["react-redux@9.2.0", "", { "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" }, "peerDependencies": { "@types/react": "^18.2.25 || ^19", "react": "^18.0 || ^19", "redux": "^5.0.0" } }, "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g=="], + "react-simple-code-editor": ["react-simple-code-editor@0.14.1", "", { "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-BR5DtNRy+AswWJECyA17qhUDvrrCZ6zXOCfkQY5zSmb96BVUbpVAv03WpcjcwtCwiLbIANx3gebHOcXYn1EHow=="], + "react-syntax-highlighter": ["react-syntax-highlighter@16.1.0", "", { "dependencies": { "@babel/runtime": "^7.28.4", "highlight.js": "^10.4.1", "highlightjs-vue": "^1.0.0", "lowlight": "^1.17.0", "prismjs": "^1.30.0", "refractor": "^5.0.0" }, "peerDependencies": { "react": ">= 0.14.0" } }, "sha512-E40/hBiP5rCNwkeBN1vRP+xow1X0pndinO+z3h7HLsHyjztbyjfzNWNKuAsJj+7DLam9iT4AaaOZnueCU+Nplg=="], "recharts": ["recharts@3.6.0", "", { "dependencies": { "@reduxjs/toolkit": "1.x.x || 2.x.x", "clsx": "^2.1.1", "decimal.js-light": "^2.5.1", "es-toolkit": "^1.39.3", "eventemitter3": "^5.0.1", "immer": "^10.1.1", "react-redux": "8.x.x || 9.x.x", "reselect": "5.1.1", "tiny-invariant": "^1.3.3", "use-sync-external-store": "^1.2.2", "victory-vendor": "^37.0.2" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-L5bjxvQRAe26RlToBAziKUB7whaGKEwD3znoM6fz3DrTowCIC/FnJYnuq1GEzB8Zv2kdTfaxQfi5GoH0tBinyg=="], @@ -968,10 +1004,14 @@ "regexp.prototype.flags": ["regexp.prototype.flags@1.5.4", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", "get-proto": "^1.0.1", "gopd": "^1.2.0", "set-function-name": "^2.0.2" } }, "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA=="], + "remark-gfm": ["remark-gfm@4.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-gfm": "^3.0.0", "micromark-extension-gfm": "^3.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "unified": "^11.0.0" } }, "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg=="], + "remark-parse": ["remark-parse@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "micromark-util-types": "^2.0.0", "unified": "^11.0.0" } }, "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA=="], "remark-rehype": ["remark-rehype@11.1.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "mdast-util-to-hast": "^13.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw=="], + "remark-stringify": ["remark-stringify@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-to-markdown": "^2.0.0", "unified": "^11.0.0" } }, "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw=="], + "reselect": ["reselect@5.1.1", "", {}, "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w=="], "resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": "bin/resolve" }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="], @@ -1174,6 +1214,8 @@ "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + "mdast-util-find-and-replace/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], + "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], "next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="], diff --git a/dashboard/package.json b/dashboard/package.json index fcc2f86..8aeadd5 100644 --- a/dashboard/package.json +++ b/dashboard/package.json @@ -13,17 +13,21 @@ }, "dependencies": { "@radix-ui/react-slot": "^1.2.4", + "@types/prismjs": "^1.26.5", "@types/react-syntax-highlighter": "^15.5.13", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "framer-motion": "^12.23.26", "lucide-react": "^0.561.0", "next": "16.0.10", + "prismjs": "^1.30.0", "react": "19.2.1", "react-dom": "19.2.1", "react-markdown": "^10.1.0", + "react-simple-code-editor": "^0.14.1", "react-syntax-highlighter": "^16.1.0", "recharts": "^3.6.0", + "remark-gfm": "^4.0.1", "sonner": "^2.0.7", "tailwind-merge": "^3.4.0", "xterm": "^5.3.0", diff --git a/dashboard/playwright-report/index.html b/dashboard/playwright-report/index.html index f3d0014..44c52a0 100644 --- a/dashboard/playwright-report/index.html +++ b/dashboard/playwright-report/index.html @@ -82,4 +82,4 @@ Error generating stack: `+n.message+`
- \ No newline at end of file + \ No newline at end of file diff --git a/dashboard/src/app/agents/page.tsx b/dashboard/src/app/agents/page.tsx deleted file mode 100644 index 97679c7..0000000 --- a/dashboard/src/app/agents/page.tsx +++ /dev/null @@ -1,452 +0,0 @@ -'use client'; - -import { useState } from 'react'; -import { - Plus, - Save, - Trash2, - X, - Loader, - AlertCircle, - Users, - GitBranch, - RefreshCw, - Check, - Upload, -} from 'lucide-react'; -import { cn } from '@/lib/utils'; -import { LibraryUnavailable } from '@/components/library-unavailable'; -import { useLibrary } from '@/contexts/library-context'; - -export default function AgentsPage() { - const { - status, - libraryAgents, - loading, - error, - libraryUnavailable, - libraryUnavailableMessage, - refresh, - clearError, - getLibraryAgent, - saveLibraryAgent, - removeLibraryAgent, - sync, - commit, - push, - } = useLibrary(); - - const [selectedAgent, setSelectedAgent] = useState(null); - const [agentContent, setAgentContent] = useState(''); - const [loadingAgent, setLoadingAgent] = useState(false); - const [saving, setSaving] = useState(false); - const [dirty, setDirty] = useState(false); - const [showNewAgentDialog, setShowNewAgentDialog] = useState(false); - const [newAgentName, setNewAgentName] = useState(''); - const [newAgentError, setNewAgentError] = useState(null); - - // Git operations state - const [syncing, setSyncing] = useState(false); - const [committing, setCommitting] = useState(false); - const [pushing, setPushing] = useState(false); - const [showCommitDialog, setShowCommitDialog] = useState(false); - const [commitMessage, setCommitMessage] = useState(''); - - const loadAgent = async (name: string) => { - try { - setLoadingAgent(true); - const agent = await getLibraryAgent(name); - setSelectedAgent(name); - setAgentContent(agent.content); - setDirty(false); - } catch (err) { - console.error('Failed to load agent:', err); - } finally { - setLoadingAgent(false); - } - }; - - const handleSaveAgent = async () => { - if (!selectedAgent) return; - setSaving(true); - try { - await saveLibraryAgent(selectedAgent, agentContent); - setDirty(false); - } catch (err) { - console.error('Failed to save agent:', err); - } finally { - setSaving(false); - } - }; - - const handleCreateAgent = async () => { - const name = newAgentName.trim(); - if (!name) { - setNewAgentError('Please enter a name'); - return; - } - if (!/^[a-z0-9-]+$/.test(name)) { - setNewAgentError('Name must be lowercase alphanumeric with hyphens'); - return; - } - - const template = `--- -model: claude-sonnet-4-20250514 -tools: - - Read - - Edit - - Bash ---- - -# ${name} - -Agent instructions here. -`; - try { - setSaving(true); - await saveLibraryAgent(name, template); - setShowNewAgentDialog(false); - setNewAgentName(''); - setNewAgentError(null); - await loadAgent(name); - } catch (err) { - setNewAgentError(err instanceof Error ? err.message : 'Failed to create agent'); - } finally { - setSaving(false); - } - }; - - const handleDeleteAgent = async () => { - if (!selectedAgent) return; - if (!confirm(`Delete agent "${selectedAgent}"?`)) return; - - try { - await removeLibraryAgent(selectedAgent); - setSelectedAgent(null); - setAgentContent(''); - } catch (err) { - console.error('Failed to delete agent:', err); - } - }; - - const handleSync = async () => { - setSyncing(true); - try { - await sync(); - } finally { - setSyncing(false); - } - }; - - const handleCommit = async () => { - if (!commitMessage.trim()) return; - setCommitting(true); - try { - await commit(commitMessage); - setCommitMessage(''); - setShowCommitDialog(false); - } finally { - setCommitting(false); - } - }; - - const handlePush = async () => { - setPushing(true); - try { - await push(); - } finally { - setPushing(false); - } - }; - - // Handle Escape key - const handleKeyDown = (e: React.KeyboardEvent) => { - if (e.key === 'Escape') { - if (showNewAgentDialog) setShowNewAgentDialog(false); - if (showCommitDialog) setShowCommitDialog(false); - } - }; - - if (loading) { - return ( -
- -
- ); - } - - if (libraryUnavailable) { - return ( -
- -
- ); - } - - return ( -
- {error && ( -
- - {error} - -
- )} - - {/* Git Status Bar */} - {status && ( -
-
-
-
- - {status.branch} -
-
- {status.clean ? ( - - - Clean - - ) : ( - - - {status.modified_files.length} modified - - )} -
- {(status.ahead > 0 || status.behind > 0) && ( -
- {status.ahead > 0 && +{status.ahead}} - {status.ahead > 0 && status.behind > 0 && ' / '} - {status.behind > 0 && -{status.behind}} -
- )} -
-
- - {!status.clean && ( - - )} - {status.ahead > 0 && ( - - )} -
-
-
- )} - -
- {/* Agent List */} -
-
- - Agents{libraryAgents.length ? ` (${libraryAgents.length})` : ''} - - -
-
- {libraryAgents.length === 0 ? ( -
- -

No agents yet

- -
- ) : ( - libraryAgents.map((agent) => ( - - )) - )} -
-
- - {/* Agent Editor */} -
- {selectedAgent ? ( - <> -
-
-

{selectedAgent}

-

agent/{selectedAgent}.md

-
-
- {dirty && Unsaved} - - -
-
- -
- {loadingAgent ? ( -
- -
- ) : ( -