Open Source · MIT Licensed

OpenDray

Run AI coding agents on your server.
Control them from your phone, browser, or Telegram.

opendray
$ ./opendray
INFO database connected and migrated
INFO providers loaded count=17
INFO telegram bot online @your_bot
INFO OpenDray starting addr=127.0.0.1:8640

Sound familiar?

frustration.sh
You SSH into your server to check on Claude.
You attach to tmux. Wrong pane.
Claude asked a question 20 minutes ago. It's been waiting.
You wanted to approve it from the bus. But there was no way to.
You close the laptop. The session dies.
There has to be a better way.

What OpenDray actually does

Manages persistent terminal sessions for AI coding agents. Controls them from a mobile app, web browser, or Telegram.

📱

Persistent Cloud Sessions

Sessions run on your server as PTY processes. Close the app, leave for hours — when you come back, the full history replays instantly via a 4 MB ring buffer. Claude sessions resume exactly where they left off.

💬

Telegram Bridge

Not just notifications — a true two-way terminal over Telegram. When Claude goes idle, it reads the structured JSONL output and renders multi-select prompts as inline keyboards. Reply to approve, reject, or type a response.

🧩

Open Plugin Standard

An open manifest.json spec for wrapping any CLI tool. Declare the command, config schema with typed fields, capabilities — OpenDray generates the UI form, maps config to CLI flags and env vars automatically.

🔀

LLM Provider Routing

Register Ollama, Groq, LM Studio, or any OpenAI-compatible endpoint. When spawning an OpenCode session, pick a provider — OpenDray generates a per-session config and injects it at launch time.

🔌

MCP Injection

Register MCP servers once in the UI. At session spawn, OpenDray renders per-session temp config files and injects them via CLI args. No global config pollution. Temp files cleaned on session exit.

🏠

Single Binary, Self-Hosted

Go backend with the Flutter web build embedded via go:embed. One binary + PostgreSQL. All config lives in the database — editable from the app. Your code and credentials never leave your infrastructure.

Ships with six. Add more via manifest.

Each agent is a plugin with a manifest.json declaring its CLI command, available models, and capabilities. The UI adapts: resume buttons for Claude, model pickers for OpenCode, approval-mode selectors for Codex.

🟣 Claude Code
🤖 Codex
Gemini CLI
🤖 OpenCode
🐉 Qwen Code
Terminal
+ Add yours

Three steps. Zero friction.

01

Pick an agent, set a working directory, launch

Choose from Claude Code, Codex, Gemini, OpenCode, Qwen, or a plain shell. Select a project directory and model. If using Claude, pick which OAuth account. For OpenCode, pick an LLM provider endpoint. Hit Create & Start.

02

The agent runs as a real PTY process on your server

OpenDray spawns the CLI in a pseudo-terminal, injects MCP server configs and LLM provider credentials as temp files, and starts capturing output in a ring buffer. Idle detection fires after 8 seconds of silence — triggering Telegram notifications.

03

Come back any time — or control it from Telegram

Reopen the app and the WebSocket reconnects, replaying full session history. Or stay away and let the Telegram bridge handle it: idle notifications arrive with the latest output, structured prompts render as inline keyboards, your replies go straight to the agent.

Declarative, not imperative.

Every provider in OpenDray — agents, panels, tools — is defined by a manifest.json. The manifest declares the CLI command, available models, and a configSchema that drives the settings form in the app.

Config → CLI args + env vars — each config field has a type (string, secret, select, number, boolean, args) and optional cliFlag or envVar. When a session launches, OpenDray reads the user's config and builds the final command line and environment automatically. Boolean fields append flags. Select fields append flag + value. Secrets inject as env vars.

Conditional fieldsdependsOn and dependsVal let fields appear only when relevant. For example, an API key field only shows when the auth type is set to "custom" instead of "env" or "oauth".

Two plugin types: cli spawns a PTY process and manages its lifecycle. panel is a stateless HTTP API backed by config (file browser, database viewer, log tailer). Both are stored in PostgreSQL, toggleable at runtime, and health-checked in the background.

plugins/agents/my-agent/manifest.json
{
  "name": "my-agent",
  "displayName": "My Custom Agent",
  "type": "cli",
  "icon": "🤖",
  "cli": {
    "command": "my-agent-cli",
    "defaultArgs": ["--no-color"],
    "detectCmd": "which my-agent-cli"
  },
  "capabilities": {
    "models": [
      { "id": "fast", "name": "Fast Mode" },
      { "id": "deep", "name": "Deep Analysis" }
    ],
    "supportsResume": false,
    "supportsStream": true,
    "supportsMcp": true,
    "supportsImages": false,
    "dynamicModels": false
  },
  "configSchema": [
    {
      "key": "apiKey",
      "label": "API Key",
      "type": "secret",
      "envVar": "MY_AGENT_API_KEY",
      "required": true,
      "group": "auth"
    },
    {
      "key": "model",
      "label": "Model",
      "type": "select",
      "options": ["fast", "deep"],
      "cliFlag": "--model",
      "cliValue": true,
      "default": "fast"
    },
    {
      "key": "verbose",
      "label": "Verbose output",
      "type": "boolean",
      "cliFlag": "--verbose"
    }
  ]
}

A real two-way terminal over Telegram.

When a session goes idle (agent waiting for input), OpenDray reads the structured output — for Claude, it parses the JSONL response log directly — and sends the latest content to linked Telegram chats. Reply to the notification and your text goes straight to the agent's stdin.

Structured prompts — if Claude shows a permission dialog or multi-choice question, the bridge parses the prompt type and renders it as inline Telegram keyboards. Tap a button to approve, or check multiple boxes and hit Submit. The response is formatted exactly as the CLI expects it.

Event-driven, not polling — the bridge hooks into the session idle detector. No busy loops. Notifications fire within seconds of the agent going quiet. Output is diffed against the last sent snapshot so you only see genuinely new content.

Works on any device with Telegram. Set up with a BotFather token and an allowed chat ID list. Multiple chats can link to the same session.

OpenDray Telegram Bridge
Read
/sessions        — list running sessions
/screen [1]      — current screen snapshot
/tail 1 [n]      — last N lines of output
/whoami          — show your chat ID

Control
/link 1          — bind this chat to session #1
/unlink          — remove the binding
/links           — list all active bindings
/send 1 <text>   — one-shot send without /link
/stop 1          — stop a running session

Quick keys (sent to linked session)
/cc  /cd  /tab  /enter  /yes  /no

One binary. One database.

Go backend embeds the Flutter web build via go:embed. PostgreSQL stores sessions, plugin configs, Claude accounts, and LLM provider endpoints. Agents are spawned as real PTY subprocesses with injected credentials and MCP configs.

Clients
Flutter App
Telegram Bot
Web UI
WebSocket / HTTP
OpenDray Server
Session Manager
Plugin Loader
LLM Router
MCP Gateway
PTY / Subprocess
Agents
Claude Code
Codex CLI
Gemini CLI
+ Plugins

Up and running in 60 seconds

terminal
$ git clone https://github.com/opendray/opendray.git
$ cd opendray && cp .env.example .env
$ make dev

Boring technology, exciting product

GoFlutterPostgreSQLWebSocketxterm.js

Ready to take control?

Stop SSH-ing into tmux sessions. Start piloting your AI agents from anywhere.