Mac-native deployment control plane for AI-generated code. — v0.1.0
Shared business logic: project model, runtime detection, process executor, SQLite store.
Tauri 2.0 desktop app. Rust backend with 18 commands + React frontend with 5 pages. Agent-based execution via UDS.
MCP server for AI agent control. 17 tools via JSON-RPC 2.0 stdio. E2E tested.
Persistent execution agent. 14 gRPC RPCs, SQLite store (~/.berth/agent.db), agent-side scheduler, store-and-forward events, remote upgrade. Deployed on Linux via systemd.
CLI interface. 12 commands + schedule + targets subcommands. Full feature parity with MCP.
pub struct Project {
pub id: Uuid,
pub name: String,
pub path: String,
pub runtime: Runtime,
pub entrypoint: Option<String>,
pub status: ProjectStatus,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
pub enum ProjectStatus {
Idle, // Not running
Running, // Process active
Stopped, // Manually stopped
Failed, // Non-zero exit code
}
impl Project {
pub fn new(name: String, path: String, runtime: Runtime) -> Self
// Generates UUID v4, sets status to Idle, timestamps to now
}
pub enum Runtime {
Python, Node, Go, Rust, Shell, Unknown,
}
pub struct RuntimeInfo {
pub runtime: Runtime,
pub version_file: Option<String>,
pub entrypoint: Option<String>,
pub confidence: f32, // 0.0 - 1.0
pub dependencies: Vec<String>,
pub scripts: HashMap<String, String>,
}
pub fn detect_runtime(path: &Path) -> RuntimeInfo
| Marker File | Runtime | Entrypoints Checked |
|---|---|---|
requirements.txt, pyproject.toml, setup.py | Python | main.py, app.py, run.py, __main__.py |
package.json | Node | index.js, index.ts, main.js, app.js |
go.mod | Go | main.go, cmd/main.go |
Cargo.toml | Rust | src/main.rs |
*.sh, *.bash | Shell | run.sh, start.sh, main.sh |
pub struct LogLine {
pub stream: LogStream, // Stdout or Stderr
pub text: String,
pub timestamp: DateTime<Utc>,
}
pub enum LogStream { Stdout, Stderr }
pub async fn spawn_and_stream(
runtime: Runtime,
entrypoint: &str,
working_dir: &str,
) -> anyhow::Result<(Child, mpsc::Receiver<LogLine>)>
| Runtime | Command |
|---|---|
| Python | python3 -u <entrypoint> |
| Node | node <entrypoint> |
| Go | go run <entrypoint> |
| Rust | cargo run |
| Shell / Unknown | sh <entrypoint> |
All commands run with kill_on_drop(true), piped stdout/stderr, and two background tokio tasks reading into a 256-capacity mpsc channel.
pub struct ProjectStore { conn: Connection }
impl ProjectStore {
pub fn open(path: &str) -> Result<Self>
pub fn open_in_memory() -> Result<Self>
pub fn list(&self) -> Result<Vec<Project>>
pub fn insert(&self, project: &Project) -> Result<()>
pub fn get(&self, id: Uuid) -> Result<Option<Project>>
pub fn update_status(&self, id: Uuid, status: ProjectStatus) -> Result<()>
pub fn record_run_start(&self, id: Uuid) -> Result<()>
pub fn record_run_end(&self, id: Uuid, exit_code: Option<i32>) -> Result<()>
pub fn delete(&self, id: Uuid) -> Result<()>
// Schedule CRUD
pub fn insert_schedule(&self, schedule: &Schedule) -> Result<()>
pub fn list_schedules(&self) -> Result<Vec<Schedule>>
pub fn get_schedules_for_project(&self, project_id: Uuid) -> Result<Vec<Schedule>>
pub fn update_schedule_after_run(&self, ...) -> Result<()>
pub fn set_schedule_enabled(&self, id: Uuid, enabled: bool) -> Result<()>
pub fn delete_schedule(&self, id: Uuid) -> Result<()>
}
~/Library/Application Support/com.berth.app/berth.db~/.berth/agent.db (remote agent only)| Column | Format | Example |
|---|---|---|
id | UUID v4 string | 8bda6c73-3ead-47f5-a64d-8d8b48f428d3 |
runtime | lowercase enum | python, node, go, rust, shell, unknown |
status | lowercase enum | idle, running, stopped, failed |
created_at | RFC 3339 | 2026-03-06T08:30:00+00:00 |
updated_at | RFC 3339 | 2026-03-06T09:15:42+00:00 |
| Command | Params | Returns | Async |
|---|---|---|---|
list_projects | — | { projects: Project[] } | No |
create_project | name: String, path: String | Project | No |
detect_runtime | path: String | RuntimeInfo | No |
update_project | id: String, ... | Project | No |
delete_project | id: String | () | No |
save_paste_code | name: String, code: String | String (path) | No |
run_project | id: String, target: Option<String> | () | Yes |
stop_project | id: String, target: Option<String> | () | Yes |
list_targets | — | Vec<Target> | No |
add_target | name, host, port | Target | No |
remove_target | id: String | () | No |
ping_target | id: String | HealthResponse | Yes |
get_agent_stats | id: String | AgentStats | Yes |
set_project_target | id: String, targetId: Option<String> | () | No |
set_project_notify | id: String, enabled: bool | () | No |
read_project_file | id: String | String | No |
write_project_file | id: String, content: String | () | No |
import_file | filePath: String | Project | No |
get_settings | — | Record<String, String> | No |
update_setting | key: String, value: String | () | No |
list_schedules | projectId: String | Vec<Schedule> | No |
add_schedule | projectId, cronExpr | Schedule | No |
remove_schedule | id: String | () | No |
toggle_schedule | id: String, enabled: bool | () | No |
list_execution_logs | projectId, limit | Vec<ExecutionLog> | No |
publish_project | id, port, provider?, target? | PublishResult | No |
unpublish_project | id, target? | UnpublishResult | No |
// Embedded local agent (in-process, UDS transport)
// AgentClient connects via ~/.berth/agent.sock
// Lockfile coordination via ~/.berth/agent.lock
// No ProcessRegistry — agent owns all process lifecycle
pub struct AppState {
pub store: ProjectStore,
// Agent started on app init, shared across commands
}
| Event Name | Payload | Emitted By |
|---|---|---|
project-log | LogEvent | Background log task in run_project |
project-status-change | StatusEvent | run_project, stop_project, log task cleanup |
// LogEvent (Rust -> Frontend)
{
"project_id": "8bda6c73-...",
"stream": "stdout", // "stdout" | "stderr"
"text": "[1/10] AAPL: $215.13 (+4.64%)",
"timestamp": "2026-03-06T09:15:42.123+00:00"
}
// StatusEvent (Rust -> Frontend)
{
"project_id": "8bda6c73-...",
"status": "running" // "idle" | "running" | "stopped" | "failed"
}
interface Project {
id: string;
name: string;
path: string;
runtime: string;
entrypoint: string | null;
status: "idle" | "running" | "stopped" | "failed";
created_at: string;
updated_at: string;
}
interface RuntimeInfo {
runtime: string;
version_file: string | null;
entrypoint: string | null;
confidence: number; // 0.0 - 1.0
}
interface LogEvent {
project_id: string;
stream: "stdout" | "stderr";
text: string;
timestamp: string;
}
interface StatusEvent {
project_id: string;
status: "idle" | "running" | "stopped" | "failed";
}
listProjects(): Promise<Project[]>
createProject(name: string, path: string): Promise<Project>
detectRuntime(path: string): Promise<RuntimeInfo>
deleteProject(id: string): Promise<void>
runProject(id: string): Promise<void>
stopProject(id: string): Promise<void>
| Token | Value | Usage |
|---|---|---|
--berth-bg | #1c1c1e | Page background |
--berth-surface | #2c2c2e | Cards, inputs, log viewer |
--berth-border | #3a3a3c | Borders, dividers |
--berth-text | #f5f5f7 | Primary text |
--berth-muted | #8e8e93 | Secondary text, labels |
--berth-accent | #0a84ff | Buttons, links, active states |
--berth-success | #30d158 | Running status |
--berth-error | #ff453a | Failed status, stderr |
| Token | Value |
|---|---|
--berth-bg | #f2f2f7 |
--berth-surface | #ffffff |
--berth-border | #d1d1d6 |
--berth-text | #1c1c1e |
--berth-accent | #007aff |
--berth-success | #34c759 |
--berth-error | #ff3b30 |
font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text",
"Helvetica Neue", Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
user-select: none;
syntax = "proto3";
package berth;
service AgentService {
// Core RPCs (Phase 1-3)
rpc Execute(ExecuteRequest) returns (stream ExecuteResponse);
rpc Status(StatusRequest) returns (StatusResponse);
rpc StreamLogs(LogStreamRequest) returns (stream LogStreamResponse);
rpc Stop(StopRequest) returns (StopResponse);
rpc Health(HealthRequest) returns (HealthResponse);
rpc Deploy(DeployRequest) returns (DeployResponse);
// Persistent Agent RPCs (Phase 3.5)
rpc GetExecutions(GetExecutionsRequest) returns (GetExecutionsResponse);
rpc GetExecutionLogs(GetExecutionLogsRequest) returns (stream LogStreamResponse);
rpc GetEvents(GetEventsRequest) returns (GetEventsResponse);
rpc AckEvents(AckEventsRequest) returns (AckEventsResponse);
rpc AddSchedule(AddScheduleRequest) returns (AddScheduleResponse);
rpc RemoveSchedule(RemoveScheduleRequest) returns (RemoveScheduleResponse);
rpc ListSchedules(ListSchedulesRequest) returns (ListSchedulesResponse);
rpc Upgrade(stream UpgradeChunk) returns (UpgradeResponse);
}
| Message | Key Fields | Notes |
|---|---|---|
ExecuteRequest | project_id, runtime, entrypoint, code (bytes), working_dir | code field for paste-and-deploy |
ExecuteResponse | stream, text, timestamp, exit_code, is_final | Server-streaming, final message has exit code |
HealthResponse | agent_version, status, uptime_seconds, os, arch | os/arch from std::env::consts |
StatusResponse | agent_id, status, cpu_usage, memory_bytes, projects[] | |
DeployRequest | project_id, runtime, entrypoint, tarball (bytes) | Multi-file deploy via tarball |
GetExecutionsRequest | project_id, limit | Query persistent execution history |
ExecutionInfo | id, project_id, started_at, finished_at, exit_code, trigger, status | Nested in GetExecutionsResponse |
GetEventsRequest | since_id, limit | Poll for store-and-forward events |
AgentEvent | id, event_type, project_id, execution_id, data, created_at | Nested in GetEventsResponse |
AgentScheduleInfo | id, project_id, cron_expr, enabled, last_triggered_at, next_run_at | Agent-side schedule |
UpgradeChunk | data (bytes), chunk_index, total_chunks | Client-streaming binary upload |
UpgradeResponse | success, new_version, message | Sent before systemd restart |
17 tools implemented via JSON-RPC 2.0 stdio transport (MCP protocol version 2024-11-05). E2E tested: deploy inline code, run, capture output, delete.
| Tool | Purpose | Status |
|---|---|---|
berth_list_projects | List all projects with status, runtime, run history | Done |
berth_project_status | Detailed status of one project (by UUID or name) | Done |
berth_deploy | Create project + run (inline code or path) | Done |
berth_run | Run existing project, capture output with timeout | Done |
berth_stop | Stop a running project | Done |
berth_logs | Fetch logs (pending log store) | Partial |
berth_import_code | Import code from path or inline, create project | Done |
berth_detect_runtime | Auto-detect language, entrypoint, deps, scripts | Done |
berth_delete | Delete a project | Done |
berth_health | System health (version, projects, schedules, platform) | Done |
berth_schedule_add | Add a cron-like schedule to a project | Done |
berth_schedule_list | List all schedules | Done |
berth_schedule_remove | Remove a schedule by UUID | Done |
berth_publish | Publish a running project to a public URL via tunnel (cloudflared) | Done |
berth_unpublish | Stop the public URL tunnel for a project | Done |
Transport: stdio (primary, for Claude Code). HTTP via axum planned for Phase 3.
Claude Code integration: .mcp.json config included in repo root.
crates/berth-mcp/
src/
main.rs -- Entry point, tokio::main, calls server::run_stdio()
lib.rs -- Module exports (protocol, server, tools)
protocol.rs -- JSON-RPC 2.0 types, MCP types (InitializeResult, Tool, CallToolResult)
server.rs -- Stdio loop: read line -> parse JSON-RPC -> dispatch -> write response
tools.rs -- Tool definitions (list_tools) + handlers (call_tool)
Berth can publish any running project to a public URL via pluggable tunnel providers. Zero Berth-side infrastructure — tunnels run on the user's machine.
| File | Role |
|---|---|
crates/berth-core/src/tunnel.rs | TunnelManager, TunnelProvider enum, cloudflared spawn + URL parsing, available_providers() |
crates/berth-core/src/agent_service.rs | Local agent publish/unpublish gRPC handlers |
crates/berth-agent/src/persistent_service.rs | Remote agent do_publish/do_unpublish + tunnel lifecycle |
crates/berth-core/src/agent_transport.rs | publish()/unpublish() trait methods |
crates/berth-core/src/nats_relay.rs | Publish/Unpublish NatsCommandKind + NatsResponseBody variants |
crates/berth-agent/src/nats_cmd_handler.rs | NATS Publish/Unpublish command dispatch |
proto/berth.proto | Publish/Unpublish RPCs + messages, tunnel_providers in HealthResponse |
src-tauri/src/commands.rs | publish_project/unpublish_project Tauri commands |
src/pages/ProjectDetail.tsx | PublishPanel component (port input, publish/unpublish, URL display) |
crates/berth-mcp/src/tools.rs | berth_publish, berth_unpublish MCP tools |
crates/berth-cli/src/main.rs | berth publish/unpublish CLI commands |
Adding a new provider (e.g. ngrok) requires changes to one file only: tunnel.rs.
TunnelProvider enumTunnelManager::start()start_ngrok() with provider-specific URL parsingavailable_providers()No proto changes, no transport changes, no new Tauri commands needed.
rpc Publish(PublishRequest) returns (PublishResponse);
rpc Unpublish(UnpublishRequest) returns (UnpublishResponse);
message PublishRequest {
string project_id = 1;
uint32 port = 2;
string provider = 3; // "cloudflared" (default)
string provider_config = 4; // reserved for future provider options
}
message PublishResponse {
bool success = 1;
string url = 2; // e.g. "https://random-words.trycloudflare.com"
string message = 3;
string provider = 4;
}
// HealthResponse includes: repeated string tunnel_providers = 9;
-- Added columns to projects table:
ALTER TABLE projects ADD COLUMN tunnel_url TEXT;
ALTER TABLE projects ADD COLUMN tunnel_provider TEXT;
-- Methods: store.set_tunnel_url(), store.clear_tunnel_url()
berth list # List all projects (table format)
berth deploy <path> [--name N] [--target T] # Create project + run
berth run <project> # Run by name or UUID
berth stop <project> # Stop a project
berth logs <project> [-f|--follow] # Run and capture output
berth status <project> # Detailed project info
berth import <path> [--name N] # Import code as project
berth detect <path> # Detect runtime + deps + scripts
berth delete <project> # Delete a project
berth health # System health check
berth targets list # List deploy targets
berth targets add <name> --host <host> # Add target (Phase 3)
berth schedule add <project> --cron <expr> # Add schedule (@every 5m, @hourly, etc.)
berth schedule list # List all schedules
berth schedule remove <id> # Remove a schedule
berth schedule enable <id> # Enable a schedule
berth schedule disable <id> # Disable a schedule
berth schedule tick # Run one scheduler tick
berth publish <project> --port 8080 # Publish via cloudflared tunnel
berth unpublish <project> # Stop the public URL tunnel
| Item | Status |
|---|---|
| Tauri app scaffold (project list, detail, code import) | Done |
| berth-core: project model, runtime detection | Done |
| Run/Stop execution (agent-based via UDS, log streaming via events) | Done |
| Paste & Deploy end-to-end (save to disk, smart quote normalization) | Done |
| xterm.js log viewer (ANSI colors, auto-fit, 10k scrollback) | Done |
| Basic monitoring (run count, last run, exit codes, live uptime) | Done |
| Local agent gRPC (tonic on localhost:50051) | Done |
| Menu bar tray icon (TrayIconBuilder + managed state) | Done |
| UI polish (toasts, skeletons, runtime badges, live status) | Done |
| Item | Status |
|---|---|
| MCP server (stdio, 13 tools, JSON-RPC 2.0) | Done |
| CLI tool (12 commands + schedule subcommands) | Done |
| .mcp.json config for Claude Code integration | Done |
| E2E MCP test (deploy inline code, run, capture output, delete) | Done |
| Scheduling (@every, @hourly, @daily, @weekly, M H * * *) | Done |
| Auto-detect: parse requirements.txt, package.json, go.mod, Cargo.toml | Done |
Persistent remote agent with SQLite store, 14 gRPC RPCs, agent-side scheduler, store-and-forward events, remote upgrade, dependency install during deployment. Agent install script, gRPC communication, remote deploy, agent health monitoring, mDNS LAN discovery, agent-only execution architecture (UDS for local, TCP gRPC for remote). Deployed and verified on remote Linux server.
AWS Lambda, Cloudflare Workers, deployment history, code signing, Homebrew cask.
Pro tier, team features, web dashboard, Windows/Linux builds.
| Layer | Technology | Version |
|---|---|---|
| Desktop Framework | Tauri | 2.10 |
| Frontend | React + TypeScript | 19 / 5.6 |
| Build Tool | Vite | 6.0 |
| Styling | Tailwind CSS | 3.4 |
| Rust Runtime | tokio | 1.x (full) |
| Database | SQLite via rusqlite | 0.32 (bundled) |
| Serialization | serde + serde_json | 1.x |
| gRPC (Phase 3) | tonic + prost | 0.12 / 0.13 |
| CLI Parsing | clap | 4.x |
| Logging | tracing + tracing-subscriber | 0.1 / 0.3 |
Berth v0.1.0 — Documentation generated March 2026
github.com/berth-app/berth