Skip to main content
Glama
README.md
# freebuff-bridge

> A communication server for **Freebuff Desktop** — a bridge between agents/scripts and Desktop sessions.

**[English](README.md) · עברית ([README.he.md](README.he.md))**

Any tool or script (any language, any platform) can open a new session or talk to an existing agent session, watch live activity, and manage work — all over **HTTP**, **MCP**, or a **Web UI**.

```
   ┌─────────────────┐    HTTP + SSE     ┌────────────────────┐
   │  bridge (7777)  ├──────────────────►│ orchestrator (dynamic) │  ← Freebuff Desktop
   │  bun + Hono     │  /api/threads      └─────────┬──────────┘
   │  reads DB       │  /api/thread/:id/message
   │  writes via API │                                │
   │  streams SSE    │                                ▼
   └────────┬────────┘                  ┌────────────────────┐
            │ reads                     │   SQLite (.freebuff)│
            ▼                           └────────────────────┘
   ┌─────────────────┐
   │  <project>/     │
   │  desktop-v2.db  │  ← Freebuff's own DB per project
   └─────────────────┘
```

The bridge talks to both the orchestrator (live) and the SQLite audit log (offline), and keeps working even if one of them is unavailable (falls back to **DB-only mode** with a `fallback` flag).

---

## Features

| Area | What you get |
|---|---|
| **REST API** (`/v1/*`) | Threads CRUD, send prompts, stop/resume, switch model/harness live, queue control, SSE streams, wake |
| **MCP server** (stdio) | **14 tools** (`list_projects`, `send_prompt`, `resume_thread`, `set_agent`, `wake_freebuff`, queue tools…) for Claude Code / Cline / any MCP agent |
| **Web UI** (port 7778) | RTL Hebrew dashboard — projects, all threads, single-thread view with state + queue + action buttons |
| **Wake on demand** | `POST /v1/wake` launches `Freebuff.exe` if it's down; `send_prompt`/`enqueue_prompt` auto-wake the app instead of failing |
| **Self-healing** | Discovers the orchestrator's **dynamic port** from its log; re-resolves on port change mid-run; SSE streams reconnect automatically |
| **Version compatibility** | Reads the installed Freebuff version, checks it against a verified table, warns in banner + `/v1/info` + UI |
| **Shape probe** | Validates the live `/api/projects` schema at startup against the verified contract — catches drift even inside a "verified" version |
| **Windows services** | Runs 24/7 via nssm (`FreebuffBridge` on 7777, `FreebuffBridgeUi` on 7778) |
| **Examples** | Copy-paste recipes in 4 languages (Python, bash, PowerShell, make) |

---

## Quick install

```bash
# Prerequisite: Bun (https://bun.sh)
bun install
bun run init                 # creates ~/.config/freebuff-bridge/instances.json
bun run serve                # listens on 127.0.0.1:7777 (localhost-noauth)

# checks
bun run typecheck            # tsc --noEmit
bun run test                 # 175 tests across 10 files
```

That's it — the bridge is live. Open the Web UI:

```bash
bun run ui                   # Hono JSX + RTL Hebrew on 127.0.0.1:7778
# → open http://127.0.0.1:7778/
```

### One-liner smoke test

```bash
curl -s http://127.0.0.1:7777/v1/info | jq .
```

---

## curl examples

### Reads

```bash
# health + self info (includes orchestrator, freebuff version, shape probe)
curl http://127.0.0.1:7777/v1/info

# all configured machines
curl http://127.0.0.1:7777/v1/instances

# all projects the orchestrator sees
curl http://127.0.0.1:7777/v1/projects

# threads of one project
curl "http://127.0.0.1:7777/v1/projects/C:/path/to/project/threads"

# a specific session (from the DB)
curl http://127.0.0.1:7777/v1/threads/<THREAD_ID>

# recent messages of a session
curl "http://127.0.0.1:7777/v1/threads/<THREAD_ID>/messages?limit=20"
```

### Writes (go through the live orchestrator)

```bash
# open a new session in a chosen project
curl -X POST http://127.0.0.1:7777/v1/threads \
  -H 'content-type: application/json' \
  --data-raw '{"title":"[bridge] test","projectPath":"C:/my-project","harnessId":"codebuff","model":"deepseek/deepseek-v4-flash"}'

# send a prompt to a session (endpoint is /message, not /prompt)
curl -X POST http://127.0.0.1:7777/v1/threads/<THREAD_ID>/message \
  -H 'content-type: application/json' \
  --data-raw '{"text":"Check my email"}'

# stop a running turn
curl -X POST http://127.0.0.1:7777/v1/threads/<THREAD_ID>/stop
```

### Resume a stuck session & switch model in real time

```bash
# bring a session back from paused → running
curl -X POST http://127.0.0.1:7777/v1/threads/<THREAD_ID>/resume
# → {"ok":true,"thread":{...}}

# switch only the model (harnessId is auto-filled from current state)
curl -X PATCH http://127.0.0.1:7777/v1/threads/<THREAD_ID> \
  -H 'content-type: application/json' \
  --data-raw '{"model":"openai/gpt-5.6-luna"}'

# switch model + harness together
curl -X PATCH http://127.0.0.1:7777/v1/threads/<THREAD_ID> \
  -H 'content-type: application/json' \
  --data-raw '{"harnessId":"codebuff","model":"deepseek/deepseek-v4-flash"}'

# un-pause via PATCH (alias)
curl -X PATCH http://127.0.0.1:7777/v1/threads/<THREAD_ID> \
  -H 'content-type: application/json' \
  --data-raw '{"queuePaused":false}'
```

⚠️ **PATCH gotchas:**

- When changing only `model`, the bridge reads current state first to fill in `harnessId` — the orchestrator requires **both together**.
- If the model is rejected by your tier (`rejected: true`), the thread gets `model: null` and the previous value is lost. Always check `thread.model` in the response.
- The orchestrator endpoint is `/api/thread/<id>/agent`, not `/model` (verified against the live bundle).

### Wake Freebuff Desktop on demand

```bash
curl -X POST http://127.0.0.1:7777/v1/wake
# → {"ok":true,"alreadyRunning":false,"exePath":"C:/.../Freebuff.exe","port":53810,"elapsedMs":4210}
```

When the orchestrator is down and `autoWake` is on (default), `send_prompt` / `enqueue_prompt` launch Freebuff automatically and retry once instead of returning 502.

### Queue control

```bash
# list a session's queue (pending + done, sorted by position)
curl http://127.0.0.1:7777/v1/threads/<THREAD_ID>/queue
# → {"thread":{...}, "items":[{id, state, prompt, position, ...}], "pending":N}

# enqueue a prompt (runs when the current turn finishes)
curl -X POST http://127.0.0.1:7777/v1/threads/<THREAD_ID>/queue \
  -H 'content-type: application/json' \
  --data-raw '{"text":"collect today's PRs","label":"pr-digest"}'

# reorder (lower position = runs earlier)
curl -X POST http://127.0.0.1:7777/v1/threads/<THREAD_ID>/reorder \
  -H 'content-type: application/json' \
  --data-raw '{"id":"<ITEM_ID>","position":-1}'

# edit a queued item's prompt before it runs
curl -X POST http://127.0.0.1:7777/v1/queue/<ITEM_ID>/edit \
  -H 'content-type: application/json' \
  --data-raw '{"prompt":"rewritten prompt text"}'

# delete from queue
curl -X POST http://127.0.0.1:7777/v1/queue/<ITEM_ID>/delete

# promote to the front (runs immediately)
curl -X POST http://127.0.0.1:7777/v1/queue/<ITEM_ID>/send-now
```

⚠️ **Queue rules:**

- If `queuePaused=true`, items accumulate and don't run until you call `resume_thread` (or `PATCH {"queuePaused":false}`).
- If `queuePaused=false`, items run as soon as the current turn finishes (auto-promote to running).
- `reorder` can fail with 409 "item is not queued" once the item is already running/done.

### Live events (SSE)

```bash
# all orchestrator events
curl -N -H 'accept: text/event-stream' http://127.0.0.1:7777/v1/events

# events for one session
curl -N -H 'accept: text/event-stream' http://127.0.0.1:7777/v1/threads/<THREAD_ID>/stream
```

---

## Running as Windows services (24/7)

The bridge and the Web UI can run as Windows services via **nssm** — auto-start with the machine, restart-on-crash, logs to `.freebuff/`, and wake Freebuff automatically when there is work.

```bash
# one-time: install nssm
winget install -e --id NSSM.NSSM

# install (elevated)
powershell -ExecutionPolicy Bypass -File .\install-service.ps1        # FreebuffBridge (API, 7777)
powershell -ExecutionPolicy Bypass -File .\install-ui-service.ps1     # FreebuffBridgeUi (UI, 7778)

# uninstall (elevated)
powershell -ExecutionPolicy Bypass -File .\uninstall-service.ps1
powershell -ExecutionPolicy Bypass -File .\uninstall-ui-service.ps1
```

**Why nssm?** `sc create` alone can't work here — bun.exe never registers with the Service Control Manager, so SCM kills the service after 30 seconds (error 1053). nssm is the standard wrapper that manages bun as a child process.

**LocalSystem + paths:** services run as SYSTEM, so env overrides (`FREEBUFF_BRIDGE_APPDATA`, `FREEBUFF_BRIDGE_LOCALAPPDATA`, `FREEBUFF_BRIDGE_WAKE_EXE`) are written to the service's `Environment` value (REG_MULTI_SZ — the official SCM mechanism). Note: do **not** use nssm `AppEnvironmentExtra` — any `nssm set` call replaces the whole list.

The tray icon (`bun run tray`) detects when the services already hold ports 7777/7778 and doesn't start duplicate copies.

---

## MCP server for external agents (Claude Code / Cline)

The bridge exposes an **MCP server** on stdio that talks directly to Claude Code / Cline / any MCP-aware agent. **14 tools** are defined:

**Session control (7):** `list_projects` · `list_threads` · `get_thread` · `send_prompt` · `stop_thread` · `resume_thread` · `set_agent`

**Wake (1):** `wake_freebuff`

**Queue control (6):** `list_queue` · `enqueue_prompt` · `reorder_queue_item` · `edit_queue_item` · `delete_queue_item` · `send_queue_item_now`

**Use it from Claude Code (`~/.claude/mcp.json`):**

```json
{
  "mcpServers": {
    "freebuff-bridge": {
      "command": "bun",
      "args": ["run", "C:/path/to/freebuff-bridge/src/bridge/mcp.ts"]
    }
  }
}
```

Make sure the bridge is running first: `bun run serve`. The MCP server connects automatically to `http://127.0.0.1:7777`.

---

## Security

- **Default bind: `127.0.0.1`** (loopback only). The bridge does not expose itself to the network.
- **`localhost-noauth` mode** (default): any local process can talk without a token.
- **`strict` mode**: requires `Authorization: Bearer <token>`. Every instance in `instances.json` gets a unique token.
- **Admin token** via env var (`FREEBUFF_BRIDGE_ADMIN_TOKEN`) grants access to every instance.
- **Per-alias env override**: `FREEBUFF_BRIDGE_TOKEN_<ALIAS_UPPER>` takes priority over the file.

```bash
# strict + admin token
FREEBUFF_BRIDGE_HOST=0.0.0.0 FREEBUFF_BRIDGE_BIND_MODE=strict \
FREEBUFF_BRIDGE_ADMIN_TOKEN=$(openssl rand -hex 32) \
  bun run serve
```

---

## Project structure

```
src/
  bridge/
    db.ts            # SQLite + WAL/NORMAL/busy_timeout + typed reads
    instances.ts     # config store (load/save/find/env-override)
    auth.ts          # bearer header regex + decideAuth()
    ratelimit.ts     # sliding-window in-memory limiter
    orchestrator.ts  # HTTP client to Freebuff's orchestrator (dynamic port discovery)
    projects.ts      # multi-project discovery (.../.freebuff scan)
    wake.ts          # find exe + discover port from log + spawn + poll
    compat.ts        # installed Freebuff version (asar/exe) + compat table
    shape.ts         # /api/projects schema probe (validate + HTTP)
    server.ts        # Hono REST API (/v1/*) — reads DB, writes via orchestrator
    mcp.ts           # MCP stdio server — 14 tools that proxy /v1/*
  web/
    ui.tsx           # Hono JSX server (7778) — SSR HTML + 9 POST actions + SSE
    views/           # layout, projects, threads, thread, queue views
  cli/
    serve.ts         # bun run serve (main entry — bridge on 7777)
    ui.ts            # bun run ui (Web UI on 7778)
    tray.ts          # bun run tray (Windows tray icon)
  tray/
    tray.ps1         # PowerShell NotifyIcon + context menu + health poller
tests/               # 10 files, 175 tests
examples/            # copy-paste integrations in 4 languages
```

---

## Windows notes

- `bun:sqlite` opens with `journal_mode=WAL`, `synchronous=NORMAL`, `busy_timeout=5000ms`, and `foreign_keys=ON` so it never collides with the Desktop running in parallel.
- Hebrew/UTF-8 paths are supported.
- The orchestrator port is **dynamic** in current builds — the bridge reads it from `%APPDATA%\Freebuff\logs\orchestrator-stderr.log` (line `listening on http://127.0.0.1:PORT`). Manual check: `curl http://127.0.0.1:<PORT>/api/projects` should return JSON, or just `GET /v1/info` and look at the `orchestrator` field.

## Git

- `bun.lockb` is **meant to be committed** — don't add it to `.gitignore`.