Berth

Mac-native deployment control plane for AI-generated code. — v0.1.0

System Architecture

+----------------------------------------------------------+ | Tauri Desktop App | | | | React 19 Frontend Rust Backend | | +--------------------+ +-------------------------+ | | | ProjectList.tsx | | commands.rs | | | | ProjectDetail.tsx |<-->| (18 Tauri commands) | | | | PasteAndDeploy.tsx | | + Embedded Agent (UDS)| | | | Targets.tsx | | | | | | Settings.tsx | | | | | +--------------------+ +------------+------------+ | | invoke() / listen() | | +------------------------------------------+----------------+ | berth-core +------------+------------+ | | +--------+--------+ +-----------+---------+ | executor.rs | | store.rs | | spawn_and_stream| | SQLite (rusqlite) | | LogLine channel | | projects table | +-----------------+ +---------------------+ | +---------------+---------------+ | | | Local Mac Linux VPS Serverless (Phase 1) (Phase 3) (Phase 4)

Workspace Crates

berth-core Done

Shared business logic: project model, runtime detection, process executor, SQLite store.

berth-app (src-tauri) Done

Tauri 2.0 desktop app. Rust backend with 18 commands + React frontend with 5 pages. Agent-based execution via UDS.

berth-mcp Done

MCP server for AI agent control. 17 tools via JSON-RPC 2.0 stdio. E2E tested.

berth-agent Done

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.

berth-cli Done

CLI interface. 12 commands + schedule + targets subcommands. Full feature parity with MCP.

Data Flow

Project Execution Flow

User clicks "Run" | v ProjectDetail.tsx --invoke("run_project")--> commands.rs::run_project() | +--------+--------+ | | store.get(uuid) target check | (local or remote?) v AgentClient::connect_uds() or connect(tcp) | AgentClient::execute_streaming() | AgentService::execute() | executor::spawn_and_stream() | Streams ExecuteResponse (log lines) | tokio::spawn(log_task) | app.emit("project-log", LogEvent) | v ProjectDetail.tsx listen("project-log") appends to logs state renders in log viewer When process exits: Stream ends with is_final=true + exit_code --> store.update_status(Idle or Failed) --> app.emit("project-status-change")

Project Creation Flow

PasteAndDeploy.tsx | +--> invoke("detect_runtime", { path }) | | | runtime::detect_runtime() | | | Scans for marker files: | requirements.txt -> Python | package.json -> Node | go.mod -> Go | Cargo.toml -> Rust | | | Returns RuntimeInfo { runtime, entrypoint, confidence } | +--> invoke("create_project", { name, path }) | commands.rs::create_project() | detect_runtime + Project::new + store.insert | Returns Project (saved to SQLite)

Rust Crates Detail

berth-core::project

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
}

berth-core::runtime

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

Detection Matrix

Marker FileRuntimeEntrypoints Checked
requirements.txt, pyproject.toml, setup.pyPythonmain.py, app.py, run.py, __main__.py
package.jsonNodeindex.js, index.ts, main.js, app.js
go.modGomain.go, cmd/main.go
Cargo.tomlRustsrc/main.rs
*.sh, *.bashShellrun.sh, start.sh, main.sh

berth-core::executor

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>)>

Command Mapping

RuntimeCommand
Pythonpython3 -u <entrypoint>
Nodenode <entrypoint>
Gogo run <entrypoint>
Rustcargo run
Shell / Unknownsh <entrypoint>

All commands run with kill_on_drop(true), piped stdout/stderr, and two background tokio tasks reading into a 256-capacity mpsc channel.

berth-core::store

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<()>
}

Database Schema

SQLite — ~/Library/Application Support/com.berth.app/berth.db

+-------------------------------------------------------------------+ | projects | +-------------------------------------------------------------------+ | id TEXT PRIMARY KEY UUID v4 as string | | name TEXT NOT NULL Human-readable project name | | path TEXT NOT NULL Absolute filesystem path | | runtime TEXT NOT NULL python|node|go|rust|shell|unknown | | entrypoint TEXT main.py, index.js, etc. (nullable) | | status TEXT NOT NULL idle|running|stopped|failed | | DEFAULT 'idle' | | created_at TEXT NOT NULL RFC 3339 timestamp | | updated_at TEXT NOT NULL RFC 3339 timestamp | | last_run_at TEXT RFC 3339 (nullable) | | last_exit_code INTEGER Process exit code (nullable) | | run_count INTEGER NOT NULL Total execution count | | DEFAULT 0 | +-------------------------------------------------------------------+ +-------------------------------------------------------------------+ | schedules | +-------------------------------------------------------------------+ | id TEXT PRIMARY KEY UUID v4 as string | | project_id TEXT NOT NULL FK -> projects(id) ON DELETE CASCADE | | cron_expr TEXT NOT NULL @every 5m, @hourly, 30 9 * * * | | enabled INTEGER NOT NULL 1 = enabled, 0 = disabled | | DEFAULT 1 | | created_at TEXT NOT NULL RFC 3339 timestamp | | last_triggered_at TEXT RFC 3339 (nullable) | | next_run_at TEXT RFC 3339 (nullable) | +-------------------------------------------------------------------+

Status State Machine

create_project() | v +----------+ +--------+ Idle +<-------+ | +----+-----+ | | | | run_project() run_project() exit code 0 | | | v v | +----------+ +-----------+ | | Failed | | Running +-------+ | (no | +-----+-----+ | entrypoint) | +----------+ stop_project() | v +-----------+ | Stopped | +-----------+ Failed also occurs on non-zero exit code

Agent SQLite — ~/.berth/agent.db (remote agent only)

+-------------------------------------------------------------------+ | deployments | +-------------------------------------------------------------------+ | id TEXT PRIMARY KEY UUID v4 | | project_id TEXT NOT NULL | | version INTEGER NOT NULL | | runtime TEXT NOT NULL python|node|go|rust|shell | | entrypoint TEXT NOT NULL | | working_dir TEXT NOT NULL /home/user/.berth/deploys/... | | image_tag TEXT container image (nullable) | | status TEXT NOT NULL deployed|running|stopped | | deployed_at TEXT NOT NULL RFC 3339 | +-------------------------------------------------------------------+ +-------------------------------------------------------------------+ | executions | +-------------------------------------------------------------------+ | id TEXT PRIMARY KEY UUID v4 | | project_id TEXT NOT NULL | | deployment_id TEXT | | started_at TEXT NOT NULL RFC 3339 | | finished_at TEXT RFC 3339 (nullable) | | exit_code INTEGER | | trigger TEXT NOT NULL manual|schedule | | status TEXT NOT NULL running|completed|failed|stopped | +-------------------------------------------------------------------+ +-------------------------------------------------------------------+ | execution_logs | +-------------------------------------------------------------------+ | id INTEGER PRIMARY KEY AUTOINCREMENT | | execution_id TEXT NOT NULL | | seq INTEGER NOT NULL | | stream TEXT NOT NULL stdout|stderr | | text TEXT NOT NULL | | timestamp TEXT NOT NULL RFC 3339 | +-------------------------------------------------------------------+ +-------------------------------------------------------------------+ | events | +-------------------------------------------------------------------+ | id INTEGER PRIMARY KEY AUTOINCREMENT | | event_type TEXT NOT NULL deploy_completed, execution_*, | | schedule_triggered, agent_upgraded| | project_id TEXT | | execution_id TEXT | | data TEXT NOT NULL JSON payload | | created_at TEXT NOT NULL RFC 3339 | +-------------------------------------------------------------------+ +-------------------------------------------------------------------+ | schedules (agent-side) | +-------------------------------------------------------------------+ | id TEXT PRIMARY KEY UUID v4 | | project_id TEXT NOT NULL | | cron_expr TEXT NOT NULL @every 5m, @hourly, etc. | | enabled INTEGER NOT NULL 1=enabled, 0=disabled | | created_at TEXT NOT NULL RFC 3339 | | last_triggered_at TEXT RFC 3339 (nullable) | | next_run_at TEXT RFC 3339 (nullable) | +-------------------------------------------------------------------+

Column Formats

ColumnFormatExample
idUUID v4 string8bda6c73-3ead-47f5-a64d-8d8b48f428d3
runtimelowercase enumpython, node, go, rust, shell, unknown
statuslowercase enumidle, running, stopped, failed
created_atRFC 33392026-03-06T08:30:00+00:00
updated_atRFC 33392026-03-06T09:15:42+00:00

Tauri Commands

CommandParamsReturnsAsync
list_projects{ projects: Project[] }No
create_projectname: String, path: StringProjectNo
detect_runtimepath: StringRuntimeInfoNo
update_projectid: String, ...ProjectNo
delete_projectid: String()No
save_paste_codename: String, code: StringString (path)No
run_projectid: String, target: Option<String>()Yes
stop_projectid: String, target: Option<String>()Yes
list_targetsVec<Target>No
add_targetname, host, portTargetNo
remove_targetid: String()No
ping_targetid: StringHealthResponseYes
get_agent_statsid: StringAgentStatsYes
set_project_targetid: String, targetId: Option<String>()No
set_project_notifyid: String, enabled: bool()No
read_project_fileid: StringStringNo
write_project_fileid: String, content: String()No
import_filefilePath: StringProjectNo
get_settingsRecord<String, String>No
update_settingkey: String, value: String()No
list_schedulesprojectId: StringVec<Schedule>No
add_scheduleprojectId, cronExprScheduleNo
remove_scheduleid: String()No
toggle_scheduleid: String, enabled: bool()No
list_execution_logsprojectId, limitVec<ExecutionLog>No
publish_projectid, port, provider?, target?PublishResultNo
unpublish_projectid, target?UnpublishResultNo

Managed State

// 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
}

Tauri Events

Event NamePayloadEmitted By
project-logLogEventBackground log task in run_project
project-status-changeStatusEventrun_project, stop_project, log task cleanup

Event Payloads

// 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"
}

Frontend Components

Component Tree

App.tsx (root, view router, titlebar) | +-- view === "list" | ProjectList.tsx | Props: onSelect(id), onNewProject() | State: projects[], loading | +-- view === "detail" | ProjectDetail.tsx | Props: projectId, onBack() | State: project, status, logs[], error | Listens: project-log, project-status-change | +-- view === "paste" | PasteAndDeploy.tsx | Props: onBack(), onCreated(id) | State: name, code, path, runtimeInfo, mode, creating, target | +-- view === "targets" | Targets.tsx | Props: onBack() | State: targets[], showAdd, stats{}, expandedId | +-- view === "settings" Settings.tsx Props: onBack() State: settings{}, theme

TypeScript Interfaces

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";
}

Invoke Functions

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>

Design Tokens

Dark Mode (default)

TokenValueUsage
--berth-bg#1c1c1ePage background
--berth-surface#2c2c2eCards, inputs, log viewer
--berth-border#3a3a3cBorders, dividers
--berth-text#f5f5f7Primary text
--berth-muted#8e8e93Secondary text, labels
--berth-accent#0a84ffButtons, links, active states
--berth-success#30d158Running status
--berth-error#ff453aFailed status, stderr

Light Mode

TokenValue
--berth-bg#f2f2f7
--berth-surface#ffffff
--berth-border#d1d1d6
--berth-text#1c1c1e
--berth-accent#007aff
--berth-success#34c759
--berth-error#ff3b30

Typography

font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text",
             "Helvetica Neue", Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
user-select: none;

Protobuf / gRPC

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 Types

MessageKey FieldsNotes
ExecuteRequestproject_id, runtime, entrypoint, code (bytes), working_dircode field for paste-and-deploy
ExecuteResponsestream, text, timestamp, exit_code, is_finalServer-streaming, final message has exit code
HealthResponseagent_version, status, uptime_seconds, os, archos/arch from std::env::consts
StatusResponseagent_id, status, cpu_usage, memory_bytes, projects[]
DeployRequestproject_id, runtime, entrypoint, tarball (bytes)Multi-file deploy via tarball
GetExecutionsRequestproject_id, limitQuery persistent execution history
ExecutionInfoid, project_id, started_at, finished_at, exit_code, trigger, statusNested in GetExecutionsResponse
GetEventsRequestsince_id, limitPoll for store-and-forward events
AgentEventid, event_type, project_id, execution_id, data, created_atNested in GetEventsResponse
AgentScheduleInfoid, project_id, cron_expr, enabled, last_triggered_at, next_run_atAgent-side schedule
UpgradeChunkdata (bytes), chunk_index, total_chunksClient-streaming binary upload
UpgradeResponsesuccess, new_version, messageSent before systemd restart

MCP Server Done

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.

ToolPurposeStatus
berth_list_projectsList all projects with status, runtime, run historyDone
berth_project_statusDetailed status of one project (by UUID or name)Done
berth_deployCreate project + run (inline code or path)Done
berth_runRun existing project, capture output with timeoutDone
berth_stopStop a running projectDone
berth_logsFetch logs (pending log store)Partial
berth_import_codeImport code from path or inline, create projectDone
berth_detect_runtimeAuto-detect language, entrypoint, deps, scriptsDone
berth_deleteDelete a projectDone
berth_healthSystem health (version, projects, schedules, platform)Done
berth_schedule_addAdd a cron-like schedule to a projectDone
berth_schedule_listList all schedulesDone
berth_schedule_removeRemove a schedule by UUIDDone
berth_publishPublish a running project to a public URL via tunnel (cloudflared)Done
berth_unpublishStop the public URL tunnel for a projectDone

Transport: stdio (primary, for Claude Code). HTTP via axum planned for Phase 3.

Claude Code integration: .mcp.json config included in repo root.

MCP Architecture

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)

Public URL Publishing Done

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.

Architecture

User clicks "Publish" (port 8080) | Tauri command: publish_project | AgentTransport::publish() | | [gRPC] [NATS] | | AgentService / PersistentAgentService | TunnelManager::start() | cloudflared tunnel --url http://127.0.0.1:8080 | Parse stderr for trycloudflare.com URL | Return URL to UI → copy to clipboard

Key Components

FileRole
crates/berth-core/src/tunnel.rsTunnelManager, TunnelProvider enum, cloudflared spawn + URL parsing, available_providers()
crates/berth-core/src/agent_service.rsLocal agent publish/unpublish gRPC handlers
crates/berth-agent/src/persistent_service.rsRemote agent do_publish/do_unpublish + tunnel lifecycle
crates/berth-core/src/agent_transport.rspublish()/unpublish() trait methods
crates/berth-core/src/nats_relay.rsPublish/Unpublish NatsCommandKind + NatsResponseBody variants
crates/berth-agent/src/nats_cmd_handler.rsNATS Publish/Unpublish command dispatch
proto/berth.protoPublish/Unpublish RPCs + messages, tunnel_providers in HealthResponse
src-tauri/src/commands.rspublish_project/unpublish_project Tauri commands
src/pages/ProjectDetail.tsxPublishPanel component (port input, publish/unpublish, URL display)
crates/berth-mcp/src/tools.rsberth_publish, berth_unpublish MCP tools
crates/berth-cli/src/main.rsberth publish/unpublish CLI commands

Provider Design (Pluggable)

Adding a new provider (e.g. ngrok) requires changes to one file only: tunnel.rs.

  1. Add variant to TunnelProvider enum
  2. Add match arm in TunnelManager::start()
  3. Implement start_ngrok() with provider-specific URL parsing
  4. Add detection in available_providers()

No proto changes, no transport changes, no new Tauri commands needed.

Proto Messages

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;

Database

-- 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()

CLI Done

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

File Map

mac-berth/ +-- Cargo.toml Workspace manifest +-- Cargo.lock +-- package.json React frontend deps +-- tsconfig.json +-- vite.config.ts +-- tailwind.config.ts +-- postcss.config.js +-- index.html Vite entry +-- CLAUDE.md Project instructions +-- PRODUCT_VISION.md Business & market | +-- src/ React frontend | +-- main.tsx React entry | +-- App.tsx Root component, view router | +-- lib/ | | +-- invoke.ts Typed Tauri invoke functions (targets, schedules, settings, etc.) | +-- pages/ | | +-- ProjectList.tsx Project list with status | | +-- ProjectDetail.tsx Run/Stop, log viewer, code editor, schedules, history | | +-- PasteAndDeploy.tsx Code import wizard with target selector | | +-- Targets.tsx Remote target management (add/remove/ping/stats) | | +-- Settings.tsx App settings (theme, default target, auto-run) | +-- styles/ | +-- globals.css macOS design tokens | +-- src-tauri/ Tauri Rust backend | +-- Cargo.toml | +-- tauri.conf.json Window, tray, plugins | +-- build.rs | +-- capabilities/ | | +-- default.json Permissions | +-- icons/ | | +-- icon.png 32x32 tray icon | +-- src/ | +-- main.rs Desktop entry | +-- lib.rs Tauri builder, state, plugins | +-- commands.rs 18 commands + agent-based execution + monitoring | +-- crates/ | +-- berth-core/ Done | | +-- src/ | | +-- lib.rs Re-exports | | +-- project.rs Project, ProjectStatus + monitoring fields | | +-- runtime.rs Runtime detection + dep parsing (req.txt, package.json, go.mod, Cargo.toml) | | +-- executor.rs Process spawn + log stream (mpsc channel) | | +-- store.rs SQLite CRUD (projects + schedules + targets) + migrations | | +-- scheduler.rs Cron-like scheduler (parse, tick, execute due jobs) | | +-- agent_service.rs AgentServiceImpl (shared across all crates) | | +-- agent_client.rs gRPC client (TCP + UDS connect, streaming execute) | | +-- uds.rs Unix Domain Socket transport helpers (serve + connect) | | +-- local_agent.rs Embedded agent bootstrap + lockfile coordination | | +-- tls.rs mTLS certificate generation (rcgen) | | +-- credentials.rs Keychain credential storage (security-framework) | | +-- tunnel.rs Pluggable tunnel manager (cloudflared, TunnelProvider trait) | | +-- nats_relay.rs NATS message types (commands, responses, events) | | +-- nats_cmd_client.rs Desktop-side NATS command client | | +-- agent_transport.rs AgentTransport trait (gRPC + NATS unified) | | | +-- berth-mcp/ Done | | +-- src/ | | +-- main.rs Entry point (stdio) | | +-- lib.rs Module exports | | +-- protocol.rs JSON-RPC 2.0 + MCP types | | +-- server.rs Stdio server loop | | +-- tools.rs 19 tool definitions + handlers (incl. publish/unpublish) | | | +-- berth-agent/ Done | | +-- src/ | | | +-- main.rs Agent startup: SQLite init, gRPC server, scheduler loop | | | +-- service.rs Legacy re-export (AgentServiceImpl from berth-core) | | | +-- persistent_service.rs PersistentAgentService (14 RPCs, SQLite persistence) | | | +-- agent_store.rs Agent SQLite store (~/.berth/agent.db, 5 tables) | | | +-- agent_scheduler.rs Agent-side cron scheduler (tick every 30s) | | +-- build.rs tonic-build proto compilation | | | +-- berth-cli/ Done | +-- src/main.rs 12 commands + schedule subcommands | +-- proto/ | +-- berth.proto gRPC service (Phase 3) | +-- reasoning/ Decision records (DRF/CRF) | +-- crf/berth-project.yaml | +-- drf/001-framework-selection.yaml | +-- drf/002-agent-language.yaml | +-- drf/003-product-positioning.yaml | +-- drf/004-business-model.yaml | +-- drf/005-mcp-as-core.yaml | +-- drf/006-backend-storage.yaml | +-- drf/007-agent-only-execution.yaml | +-- docs/ This documentation +-- index.html +-- remote-agent.html

Roadmap & Status

Phase 1: Foundation Complete

ItemStatus
Tauri app scaffold (project list, detail, code import)Done
berth-core: project model, runtime detectionDone
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

Phase 2: MCP + CLI Complete

ItemStatus
MCP server (stdio, 13 tools, JSON-RPC 2.0)Done
CLI tool (12 commands + schedule subcommands)Done
.mcp.json config for Claude Code integrationDone
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.tomlDone

Phase 3: Remote Agents Complete

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.

Phase 4: Cloud Targets Not Started

AWS Lambda, Cloudflare Workers, deployment history, code signing, Homebrew cask.

Phase 5: Growth Not Started

Pro tier, team features, web dashboard, Windows/Linux builds.

Tech Stack Summary

LayerTechnologyVersion
Desktop FrameworkTauri2.10
FrontendReact + TypeScript19 / 5.6
Build ToolVite6.0
StylingTailwind CSS3.4
Rust Runtimetokio1.x (full)
DatabaseSQLite via rusqlite0.32 (bundled)
Serializationserde + serde_json1.x
gRPC (Phase 3)tonic + prost0.12 / 0.13
CLI Parsingclap4.x
Loggingtracing + tracing-subscriber0.1 / 0.3

Berth v0.1.0 — Documentation generated March 2026
github.com/berth-app/berth