mirror of
https://github.com/runbear-io/beardrive.git
synced 2026-08-25 08:08:08 +02:00
* feat(templates): start a project from a structure, not an empty folder A new project was an empty folder with a .bdriveignore in it, so every agent session invented its own layout and the folder rotted into a pile. Both surfaces now offer the same three starting points — from a template, from scratch, from an existing folder (which is just a non-empty folder, and is never restructured). internal/templates holds the shipped set as literal go:embed'ed files: `docs` (docs/, decisions/) and `para` (projects/, areas/, resources/, archives/). cmd/bdrive is one binary for the CLI and the hub, so both read the identical set — no gallery, no drift. The AGENTS.md in each is the deliverable: where a new note goes, when something is archived, what a good filename looks like. Every directory holds a real file, because BearDrive syncs paths and an empty directory would never reach a teammate. The hub seeds at creation through the existing Upload+Commit path, journaled under its own device, and records the choice on the project record — so a user who picked PARA in a browser sees PARA in the browser, and a later init cannot seed a second copy. `bdrive init --template <name>` goes through the same endpoint, with a local-seed fallback for a hub too old to know the field, and seeds in place when re-run in an already-initialized folder (the agent's post-init path). Seeding never overwrites an existing path, which is what makes a double-seed a no-op rather than a divergence. Refusals cost nothing: an unknown name and --template with --only are both rejected before any network call or write. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * test(cli): joining a project that already has a template is refused by name The one acceptance case with no test behind it: connecting to an existing project with --template must say what the project was actually created from, and must not write the other skeleton on the way out. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(templates): name the docs template in plain English, not an acronym "Plain docs + ADRs" was the recommended, first, preselected-adjacent option in a picker that non-engineers see — and it's the label people accept without reading further, so half of it not parsing is the worst place for jargon. The title also disagreed with its own blurb: "ADRs" over "docs/, decisions/", two words for the same folder one line apart. Now "Docs + decision records", which says the same thing to everyone and matches the folder names. The term itself moves into decisions/0001-record-decisions.md, where the reader is already inside the structure and the file can teach it in passing. One line in the registry drives both the web dialog and the CLI menu; the rest is prose echoing it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * feat(templates): add the LLM wiki template The third starting point from the issue title, unblocked: the spec parked it because shipping an approximation under someone's name needed a source, and there is now one — Karpathy's LLM Wiki gist. Worth noting the issue's own one-line description of it ("few large, append-heavy topic pages") does not match the source, which is the opposite: many interlinked pages, where a single ingest touches 10-15 of them. The pattern is three layers and three operations, not a folder shape. sources/ is yours and immutable; wiki/ is the agent's and it owns every page; AGENTS.md is the schema layer — which is exactly the file this template system already treats as the deliverable, so the fit is direct. index.md and log.md ship as the two navigation files the pattern turns on. Three of the things the gist tells you to go set up, BearDrive already is: version history and collaboration (per-file history, bdrive log), an Obsidian- style reader for [[wikilinks]] (the hub viewer), and a surface for the lint pass (the dashboard is literally reads x staleness). Two rules in the AGENTS.md are load-bearing and deliberate. A page write that has not updated the index is an incomplete write — a stale index is worse than a missing page, because it is read first and believed. And with no sources yet, build nothing: the structure grows out of the material rather than ahead of it. Shipped second, not first: docs stays the recommendation because a default is the option chosen by people not reading closely, and this pattern degrades badly when half-followed. Promoting it later is one line in the registry. The shipped-template test now checks the "what happens when something stops being true" question through a set of alternatives — PARA archives, a wiki supersedes and revises — since the vocabulary honestly differs by structure. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * feat(web): "I already have a folder" as a starting point The third way to start from the spec — adopt what you already have — had no presence in the browser. Templates and "empty" were the only visible answers, so someone with a folder of notes either hesitated or picked a template and got four directories merged into their material. The constraint that shapes it: the browser cannot reach your disk, so this cannot change what is created. It creates the same empty project "Empty project" does; what it changes is the next screen. Create therefore stays enabled — disabling it would leave the dialog a dead end AND produce no project id, which is the one thing the paste prompt actually needs. Landing on the project home with the intent, three things differ: the guide says "in the folder you already have", a note states plainly that connecting never moves, renames or overwrites anything, and the paste prompt tells the agent a folder already exists. That last one is the part that isn't cosmetic — without it an agent reads an empty project and proposes creating shared/, the one recommendation that is wrong here. It still asks which folder: that is the runbook's hard gate and nothing here weakens it. The intent rides in the URL (?connect=existing) rather than onto the project record, the same way ?v= pins a file version. It belongs to whoever is connecting right now — a teammate who connects next week has their own answer and would be told the wrong thing by a persisted flag. Five rows made the dialog tall enough to push Create off a short viewport, so .modal scrolls internally. A hairline divider between the seeding and non-seeding rows was tried and removed: --border is 7% white, which at 1px in a gap renders as literally nothing. The gap is the cue that reads. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * feat(web): with no projects, open the create dialog and give the page a way in A signed-in account with no projects landed on a page whose only path forward was pasting a prompt into a coding agent. Now the create dialog opens itself — with nothing to browse there is nothing else on that page to do — and the page behind it leads with "Start a project" and a button, so closing the dialog is not a dead end. The dialog moves up to HubApp because three things ask for it now: the sidebar's +, the empty state's button, and the auto-open. ProjectNav keeps only an onNew callback; one owner beats three copies of the create handler. Two guards on the auto-open. It fires once per mount, keyed off a ref rather than the empty state, or closing it would immediately reopen it. And it never fires on a read-only hub, which refuses creation server-side with a 403 — opening a dialog that cannot succeed is worse than the page it covers. The agent paste-prompt stays, demoted to "Or let your agent do it": it is still the right path for someone who wants the folder connected in the same breath, and it is the only path on a hub where this account cannot create. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
644 lines
34 KiB
Markdown
644 lines
34 KiB
Markdown
# BearDrive — Google Drive for AI agents
|
||
|
||
**BearDrive** mounts any folder as a synced volume: its contents stay
|
||
synchronized across all your devices and teammates through a BearDrive
|
||
**hub**, every change is tracked (who, when, on which device), and
|
||
everything keeps working offline. The CLI is `bdrive`; a hub is a
|
||
`bdrive serve` server you (or we) run on an object store — clients sync
|
||
through it over HTTPS and never touch the storage directly.
|
||
|
||
What it's for, first and foremost: **sharing context across AI agents** —
|
||
give every agent on the team the same folder as memory, and your agent
|
||
knows what their agent knows. (People are covered too: any synced file
|
||
becomes a public URL that renders as a page.) Notes, plans, findings, and
|
||
artifacts follow the team everywhere — and unlike a memory API, they stay
|
||
**real files with provenance**: every change is attributed to the human,
|
||
agent, and device that made it, and the hub's Dashboard shows what your
|
||
agents actually read (and which hot-but-stale docs nobody maintains).
|
||
|
||
<p align="center">
|
||
<img src="docs/assets/insights.png" alt="Knowledge Insights — every file plotted by agent/human reads vs staleness; hot-but-stale docs are the danger zone" width="820">
|
||
</p>
|
||
|
||
| Browse with read heat | Public share pages |
|
||
|---|---|
|
||
|  |  |
|
||
|
||
```console
|
||
$ bdrive login https://your-hub # once per device — self-host a hub in ~10 min (docs/self-hosting.md)
|
||
$ cd ~/workspace && bdrive init
|
||
initialized /Users/snow/workspace
|
||
server: https://your-hub
|
||
project: workspace (7f3a2c91-4d5e-4b8a-9c17-2ad0f6b3e9c4)
|
||
claude hooks registered → /Users/snow/.claude/settings.json
|
||
login: autostart registered → ~/Library/LaunchAgents/ai.beardrive.daemon.plist
|
||
daemon: running (pid 55434, scan 3s, remote sync 10s)
|
||
```
|
||
|
||
> BearDrive Cloud — zero-setup, bare `bdrive login`, free personal
|
||
> workspace on signup — at [beardrive.ai](https://beardrive.ai). Or
|
||
> self-host your own hub.
|
||
|
||
On another machine:
|
||
|
||
```console
|
||
$ bdrive login https://your-hub && cd ~/workspace && bdrive init
|
||
# … connect the same project; the files appear and stay in sync
|
||
```
|
||
|
||
## What this looks like
|
||
|
||
Say your teammate Dana spent yesterday afternoon with her agent working out
|
||
why checkout fails for EU cards, and the agent wrote it down. Today you hit a
|
||
related bug. Here's the difference a shared folder makes.
|
||
|
||
**Without one**, your agent starts from nothing:
|
||
|
||
> **You:** why do EU checkouts fail?
|
||
>
|
||
> **Agent:** Let me investigate — searching the payment flow…
|
||
|
||
Some minutes and a few thousand tokens later it rediscovers what Dana's agent
|
||
already knew. Or it reaches a different conclusion and writes a second doc
|
||
that quietly contradicts hers.
|
||
|
||
**With BearDrive**, the folder is already fresh before the agent reads a
|
||
single file — the pull hook runs the moment you hit enter:
|
||
|
||
> **You:** why do EU checkouts fail?
|
||
>
|
||
> **Agent:** `shared/findings/eu-checkout.md` [🔗](https://your-hub/p/…)
|
||
> covers this. Dana's agent traced it to the 3-D Secure redirect dropping the
|
||
> session cookie on Safari, and the fix it proposes isn't applied yet. Want me
|
||
> to apply it?
|
||
|
||
When your agent writes its own findings down, the push hook puts them on the
|
||
hub seconds later, and Dana's next session starts from *your* work. Both
|
||
directions, no one exporting or pasting anything.
|
||
|
||
Because these are real files, the same folder is also a website: any file can
|
||
become a public link that renders as a page, the History view shows who
|
||
changed what from which device, and the Dashboard shows which docs your agents
|
||
actually read (and which hot ones nobody maintains).
|
||
|
||
## Features
|
||
|
||
- **Any folder is a project** — `bdrive init` turns any folder into a synced
|
||
project. Files are *real files on disk*: every tool, editor, and agent can
|
||
use them with zero integration work. Rename or move the folder freely —
|
||
state is keyed by a stable id, never the path.
|
||
- **Multi-device sync** — devices converge through a shared hub. Each
|
||
device only writes its own append-only journal, so no locking service is
|
||
needed; the hub can be backed by any object store.
|
||
- **Change tracking** — `bdrive log` and the web UI's History view show
|
||
which account changed which file, when, from which device (name, OS).
|
||
Content is stored content-addressed, so every version is retained — view
|
||
or download any point in a file's history.
|
||
- **Cloud-provider agnostic** — a hub can store on Amazon S3 (`s3://`),
|
||
Google Cloud Storage (`gs://`), any S3-compatible store (MinIO, Cloudflare
|
||
R2 via `AWS_ENDPOINT_URL`), or a plain shared directory (`file://`, e.g. a
|
||
NAS). Clients never see it.
|
||
- **Offline-first** — the working folder is always fully usable with no
|
||
network. Changes are journaled locally and pushed when the remote becomes
|
||
reachable again.
|
||
- **Conflict-safe** — concurrent edits resolve deterministically
|
||
(last-writer-wins), and the losing version is preserved as a
|
||
`name.bdrive-conflict-<device>-<time>` file. Nothing is silently dropped.
|
||
- **Selective sync** — a gitignore-style `.bdriveignore` opts files out, and
|
||
`bdrive init . --only wiki,docs` (or the interactive prompt) narrows a mount
|
||
to some of its subfolders by writing those same rules for you.
|
||
- **macOS & Linux.**
|
||
|
||
## Install
|
||
|
||
BearDrive is meant to be set up by the agent that will use it. The fastest
|
||
path is to have your agent do it; the CLI route below is the same
|
||
destination, by hand.
|
||
|
||
### Have your agent set it up (recommended)
|
||
|
||
No terminal needed: start any agent (Claude Code, Codex, Gemini CLI, Hermes)
|
||
in the folder you want synced and give it one paste:
|
||
|
||
```
|
||
Follow https://raw.githubusercontent.com/runbear-io/beardrive/main/INSTALL_FOR_AGENTS.md
|
||
to set up BearDrive project <project-id> on <hub-url>. Ask me which folder to
|
||
sync (the project is named "<project-name>").
|
||
```
|
||
|
||
Joining a teammate's project? They can copy that paste with the hub URL and
|
||
project id already filled in from the project's home page in the web UI.
|
||
Starting fresh, drop the trailing sentence — the agent recommends `shared/`
|
||
and names the new project `shared`.
|
||
|
||
The agent fetches [INSTALL_FOR_AGENTS.md](INSTALL_FOR_AGENTS.md) and follows
|
||
it: install the CLI, then one `bdrive init` — which signs in (an approval link
|
||
when there is no local browser), registers the sync hooks, and prints the
|
||
project link. The instructions live at that URL rather than inside the prompt
|
||
so they never go stale in someone's copy, and the agent handles every
|
||
deviation (already installed, no Homebrew, sign-in, wrong folder).
|
||
|
||
Those hooks are the whole integration, and `bdrive init` registers them in
|
||
each platform's user config (`~/.claude/settings.json` and friends), once per
|
||
machine, so every session in every folder is covered:
|
||
|
||
- a **blocking pull** when you send a message, so the agent always reads
|
||
fresh team files — it also injects the project's link convention, so the
|
||
agent appends a hub link to any synced path it mentions;
|
||
- an **async push** after every file edit, so artifacts are on the hub
|
||
seconds after the agent writes them;
|
||
- **read tracking**, so the hub's Dashboard can show what your agents
|
||
actually read.
|
||
|
||
Each hook no-ops instantly outside BearDrive projects, which is what makes a
|
||
machine-wide registration safe. `bdrive hooks` prints what's set up on this
|
||
machine; re-run it after a CLI upgrade.
|
||
|
||
### Install the CLI yourself
|
||
|
||
```sh
|
||
brew install runbear-io/tap/beardrive # macOS (and Linuxbrew); installs the `bdrive` CLI
|
||
```
|
||
|
||
or from source:
|
||
|
||
```sh
|
||
go install github.com/runbear-io/beardrive/cmd/bdrive@latest
|
||
```
|
||
|
||
## Quick start
|
||
|
||
```sh
|
||
# 1. Sign this device in against your hub (once per device).
|
||
# Self-host a hub in ~10 minutes (docs/self-hosting.md), then:
|
||
bdrive login
|
||
# (BearDrive Cloud: sign up in the browser, get a free personal
|
||
# workspace automatically. Self-hosting? bdrive login https://your-hub)
|
||
|
||
# 2. Start syncing a project — interactive: create or connect a project,
|
||
# sync the whole folder or just ./shared. Re-run any time to resume.
|
||
cd ~/my-project && bdrive init
|
||
|
||
# 3. Work normally — create, edit, delete files with any tool.
|
||
echo "remember this" > memory.md
|
||
|
||
# On every other device: `bdrive login https://your-hub` once, then bdrive init in a folder
|
||
# and connect the same project.
|
||
|
||
# See what changed, who changed it, and from which device
|
||
bdrive log
|
||
|
||
# An agent clobbered a file? Put the old version back (as a new change)
|
||
bdrive restore memory.md
|
||
|
||
# Check sync state and the daemon
|
||
bdrive status
|
||
|
||
# Stop syncing — pauses everything, including agent turn hooks
|
||
# (files stay on disk; bdrive init resumes any time)
|
||
bdrive stop
|
||
```
|
||
|
||
Renaming or moving a project folder is safe: state is keyed by a stable
|
||
project id, never the path. The daemon notices the move, steps aside, and
|
||
the next `bdrive init` (or any bdrive command) at the new location resumes
|
||
exactly where it left off — zero re-scan, zero spurious changes.
|
||
|
||
### Credentials
|
||
|
||
beardrive uses each provider's standard credential chain — nothing beardrive-specific.
|
||
Note: **client devices always use an `https://` hub remote** — the
|
||
`s3`/`gs`/`file` rows below are how the *hub operator* configures the
|
||
hub's own storage, never something a syncing client points at directly:
|
||
|
||
| Remote | Credentials |
|
||
|---|---|
|
||
| `s3://bucket/prefix` | `AWS_PROFILE`, `~/.aws/credentials`, env vars, IAM roles. S3-compatible stores via `AWS_ENDPOINT_URL`. |
|
||
| `gs://bucket/prefix` | Application Default Credentials (`gcloud auth application-default login`) or `GOOGLE_APPLICATION_CREDENTIALS`. |
|
||
| `file:///path` | none — any local or network-mounted directory |
|
||
| `https://host:port/p/<id>` | none — syncs through a BearDrive hub; only the server holds storage credentials (see [The sync hub and `bdrive init`](#the-sync-hub-and-bdrive-init)) |
|
||
|
||
## Commands
|
||
|
||
| Command | Description |
|
||
|---|---|
|
||
| `bdrive login [server-url]` | Sign this device in (browser flow — the page names the account this terminal would act as and lets you switch before approving; `--device` forces the approval-link flow, and shells without a TTY fall back to it automatically; default server beardrive.ai — the managed cloud, free personal workspace on signup; pass your hub URL to self-host). Switch hubs with `bdrive login <new-url>` |
|
||
| `bdrive logout` | Sign this device out — clear the saved token/account (`--forget` also drops the remembered server) |
|
||
| `bdrive init [folder]` | Create/connect a project and start syncing — the mount is always exactly the folder named. Interactive on a TTY, flags (`--name/--project/--server/--only/--template/--yes`) for scripts; `--template docs\|wiki\|para` starts the project from a structure (directories plus the `AGENTS.md` that explains them) instead of an empty folder; registers agent sync hooks and the login autostart in each platform's user config (`--no-hooks` skips the hooks), prints the project link; re-run to resume |
|
||
| `bdrive resume` | Restart the sync daemon for every project on this device that isn't paused — after a reboot, a crash, or a manual kill. Idempotent; this is what the login agent runs |
|
||
| `bdrive autostart [install\|uninstall]` | Show, add, or remove the login registration that runs `bdrive resume` after a reboot — a launchd user agent on macOS, a systemd user unit on Linux, an HKCU Run entry on Windows. `bdrive init` installs it; `--no-autostart` skips it |
|
||
| `bdrive stop [folder]` | Stop syncing, including agent sync hooks (files stay; `bdrive init` resumes) |
|
||
| `bdrive scope [add\|rm <dirs...>]` | Show or change which subfolders sync — edits the managed block of `.bdriveignore` rules that `init --only` writes, so no one hand-writes negation syntax. The daemon picks changes up in seconds; `rm` deletes nothing, locally or on the hub. `--explain` lists every path in the folder split into what syncs and what does not, so you can verify what leaves this machine (pure read — no daemon, no lock, no network) |
|
||
| `bdrive forget <path>...` | Stop syncing a path *and* remove it from the hub — adds the rule to `.bdriveignore` (which syncs) and prunes in one step. Local files are never touched, here or on teammates' devices |
|
||
| `bdrive url [path]` | Internal hub link for a file/folder (sign-in + membership required; `--sync` pushes first; no arg = project home). Computed locally |
|
||
| `bdrive share <file>` | Public URL for a synced file (`--list`, `--revoke`, `--expires`) |
|
||
| `bdrive sync [folder]` | Run one sync cycle now. `--note <text>` stamps session context (e.g. an agent session id) onto changes — shown in `bdrive log` and hub history; keeps applying to daemon-committed changes until `--note-ttl` (default 30m) expires. `--prune` also removes from the hub what `.bdriveignore` now excludes (files stay on disk everywhere). `--hook <label>` is agent-hook plumbing: event JSON on stdin, sync + note, gated-link formula (Claude Code hook JSON) on stdout |
|
||
| `bdrive hooks [install\|uninstall]` | Register turn-boundary sync hooks in each agent platform's user config (Claude Code, Codex, Gemini CLI, Hermes) — pull each turn, push after edits, session-note stamping, agent-read tracking. Once per machine, covering every session; run automatically by `bdrive init`; idempotent (`--agent` overrides detection) |
|
||
| `bdrive read-log [folder]` | Hook plumbing: queue agent file reads from a hook event (JSON on stdin) for the hub's read heatmap — native reads, grep matches, and files named in shell commands; drained on the next sync. Registered by `bdrive hooks install` |
|
||
| `bdrive status [folder]` | Projects, daemon state, pending changes |
|
||
| `bdrive log [folder] [-p path] [-n N]` | Change history: account, device, time, file — newest first by the time shown, which is when the file was written (ops recorded before this was tracked, and deletes, show their sync time instead) |
|
||
| `bdrive restore <file> [version]` | Put an earlier version of a file back, as a new change (`--list` shows the versions; no version = the previous one). Nothing is erased and it syncs everywhere like any edit. To un-create a file a run *created*, use **undo — remove file** on that row in the hub's History view |
|
||
| `bdrive export [folder]` | Export the whole project — every device's journal, all blobs, full history — from its hub to a portable `.tar.gz` (`-o` names the file) |
|
||
| `bdrive import <archive>` | Import an export archive as a new project on the hub you're logged into (`--name` overrides); history and authorship carry over. Move projects between hubs — cloud → self-hosted or back — with `export` + `login` + `import` |
|
||
| `bdrive serve [folder \| storage-root-url]` | Web server: viewer (rendered markdown, downloads, history), uploads, multi-project sync hub (`bdrive web` is a deprecated alias) |
|
||
| `bdrive whoami` | Signed-in account and device identity used in change tracking |
|
||
| `bdrive version` | Print the version (also `bdrive --version`) |
|
||
|
||
## Project files
|
||
|
||
Each mounted folder carries its own settings, so configuration travels with
|
||
the project:
|
||
|
||
- **`.bdrive/`** — the folder's settings directory: `config.json` holds the
|
||
**stable mount id** plus the project and remote (and, on older mounts, a
|
||
legacy `include` list — still honored, never written now). Written by
|
||
`bdrive init`, safe to hand-edit (a running daemon picks changes up
|
||
automatically). Never synced, and it holds no credentials (the session
|
||
token stays in `~/.bdrive`). Because everything is keyed by the mount id,
|
||
the folder can be renamed or moved freely; copy it to another machine and
|
||
`bdrive init` resumes the same project.
|
||
- **`.bdriveignore`** — gitignore-style opt-out list at the mount root. Syncs
|
||
like a normal file, so every device shares the same rules. Supports `#`
|
||
comments, `*`, `**`, `?`, trailing `/` for directories, leading `/` (or any
|
||
`/`) for root-anchoring, and `!` to re-include.
|
||
|
||
```jsonc
|
||
// .bdrive/config.json
|
||
{ "id": "m-5a10b713", "volume": "notes",
|
||
"remote": "https://drive.example.com/p/7f3a2c91-4d5e-4b8a-9c17-2ad0f6b3e9c4" }
|
||
```
|
||
|
||
Opting out is non-destructive: when a pattern starts matching an
|
||
already-synced file, the file stops syncing but is deleted nowhere — which
|
||
also means the hub keeps the copy it already has. `bdrive forget <path>` (or
|
||
`bdrive sync --prune` for rules you added by hand) takes it off the hub, and
|
||
still deletes nothing on disk: every device receives the rule alongside the
|
||
removal and simply stops tracking the path.
|
||
|
||
## Web server
|
||
|
||
`bdrive serve` serves a website — browse folders and files, read markdown
|
||
rendered Obsidian-style (including `[[wikilinks]]`, task lists, and
|
||
tables), download any file — and, pointed at a storage root, becomes a
|
||
**multi-project sync hub**. It is read-only unless started with `--upload`.
|
||
|
||
```sh
|
||
bdrive serve # serve the current directory (viewer)
|
||
bdrive serve ./notes # serve a folder from disk (viewer)
|
||
bdrive serve -c config.json # everything from a config file
|
||
bdrive serve s3://my-bucket/root --upload # multi-project sync hub
|
||
```
|
||
|
||
With a folder it serves files straight from disk — on a BearDrive mount the
|
||
daemon keeps them fresh, so this is the simplest read-only deployment (no
|
||
cloud credentials on the serving machine). With a storage root URL it runs
|
||
in hub mode, described below.
|
||
|
||
Flags: `--addr` (default `:4173`), `--volume` (display name), `--refresh`
|
||
(listing cache, default `10s`), `--dir` / `--remote` (explicit forms of
|
||
the positional argument), `--upload` (allow client writes, off by default),
|
||
`--upload-ttl` (presigned-URL lifetime, default `15m`), `--projects-db`
|
||
(hub project registry file, default `$BDRIVE_HOME/projects.json`),
|
||
`-c/--config` (read all of the above from a JSON file; explicit flags win):
|
||
|
||
```jsonc
|
||
// bdrive serve -c config.json
|
||
{
|
||
"remote": "s3://my-bucket/root", // storage root (hub) — or "dir": "./folder" (viewer)
|
||
"addr": ":4173",
|
||
"upload": true,
|
||
"upload_ttl": "15m",
|
||
"refresh": "10s",
|
||
"projects_db": "/var/lib/bdrive/projects.json",
|
||
"share_rpm": 120, // per-IP rate limit on public /s/* links
|
||
"auth": { // optional knobs; hub auth is always on
|
||
// Signup is invite-only by default. To allow self-service signup,
|
||
// open it WITH a gate (an ungated open hub is refused at startup):
|
||
"allow_signup": true,
|
||
"allowed_domains": ["example.com"], // only these domains may sign up
|
||
"require_approval": true, // …and an admin must approve each one
|
||
"users_db": "/var/lib/bdrive/auth.json",
|
||
"admins": ["admin@example.com"],
|
||
"smtp": { "host": "smtp.example.com", "port": 587,
|
||
"user": "drive@example.com", "pass": "…", "from": "drive@example.com" }
|
||
},
|
||
"reads": { // read heatmap telemetry (hub mode)
|
||
"enabled": true, // default true; aggregate counts only
|
||
"retention_days": 400 // daily buckets older than this fold into all-time totals
|
||
}
|
||
}
|
||
```
|
||
|
||
### The sync hub and `bdrive init`
|
||
|
||
In hub mode the server hosts many **projects** on one storage root — each
|
||
project's data lives under its own prefix (`<root>/<project-id>/`), and a
|
||
file-backed registry (`projects.json`, loaded at start, rewritten
|
||
atomically on every change) maps project ids to names. Client devices sync
|
||
whole folders through the hub without ever knowing where the storage is or
|
||
holding any cloud credentials; the server device is the only one configured
|
||
with the bucket.
|
||
|
||
Projects are walled by **organization**: every project belongs to one org
|
||
(file-backed `orgs.json`), and only that org's members — accounts with the
|
||
`owner` or `member` role — can see, browse, or sync it. Your first
|
||
`bdrive init` creates an org for you automatically; an owner invites
|
||
teammates from the web UI (the org name in the sidebar footer — Invite
|
||
mints an expiring join link, `/join/<token>`, that any signed-in account
|
||
can open to become a member). A hub upgraded from an earlier version
|
||
sweeps its existing projects into a `default` org that all existing
|
||
accounts join, so nothing breaks. Public share links stay outside the
|
||
wall on purpose.
|
||
|
||
Inside an org, each project carries its own **permissions** — four ordered
|
||
levels, edited under Project settings → People:
|
||
|
||
| Level | Can |
|
||
|---|---|
|
||
| `none` | nothing: the project is hidden — absent from the project list, every route denied |
|
||
| `read` | browse, view, download, history, read heat — and **pull**, so a device stays current |
|
||
| `write` | + upload, sync push, and minting/revoking share links |
|
||
| `admin` | + rename, delete, and edit this project's permissions |
|
||
|
||
The default is `write` for every org member, which is exactly the old
|
||
behavior — an upgraded hub changes nothing until someone edits
|
||
permissions. Setting the **default** to `No access` makes a project
|
||
invite-only: only explicit grants get in. Whoever creates a project becomes
|
||
its first admin, and **org owners are implicitly admin on every project in
|
||
their org**, so nobody can lock them out. Grants are org members only, and
|
||
a project always keeps at least one admin.
|
||
|
||
Two things follow on the **device** side, because a refusal is not the same
|
||
as being offline (see `bdrive status`):
|
||
|
||
- **read-only** — pushes are refused, so the daemon goes **pull-only**. Your
|
||
local edits stay journaled on the device, never pushed and never lost;
|
||
they go out if you're granted `write` again.
|
||
- **no access** — pulls are refused too, so sync **pauses**. Nothing is
|
||
pulled, pushed, or written: revoking access never deletes or reverts a
|
||
file on someone's disk. Re-granting resumes on the next tick.
|
||
|
||
Public `/s/<token>` share links are **unaffected** by any of this: they are
|
||
anonymous by design and keep serving until revoked, so cutting someone's
|
||
access does not kill links they already minted.
|
||
|
||
```sh
|
||
# On the server device (knows the storage)
|
||
bdrive serve -c config.json
|
||
|
||
# On each client device (knows only the server) — one command does it all:
|
||
bdrive login https://drive.example.com:4173 # once per device
|
||
cd ~/some-project && bdrive init # once per project
|
||
```
|
||
|
||
`bdrive login` signs the device in and remembers the server (`settings.json`
|
||
under the bdrive home; bare `bdrive login` defaults to beardrive.ai — the
|
||
managed cloud, where signup auto-creates a free personal workspace; pass
|
||
your hub's URL to use a self-hosted hub instead — `--status` shows the
|
||
current server and account). To move to a **different
|
||
hub**, run `bdrive login <new-url>` and then re-run `bdrive init` in each
|
||
folder to connect it to a project there; `bdrive logout` signs out entirely.
|
||
`bdrive init` then, per
|
||
project, walks you through it on a terminal: **create a new project or
|
||
connect an existing one** (picked from the server's list), **start from a
|
||
structure or from scratch**, and **sync the whole folder or only some of its
|
||
subfolders** (e.g. `./wiki`). Every question has a flag (`--name`,
|
||
`--project`, `--template`, `--only`, `--yes`), and without a TTY init never
|
||
prompts — it creates-or-joins a project named after the folder, empty, and
|
||
syncs everything.
|
||
|
||
`--template docs`, `wiki` or `para` starts a **new** project from a
|
||
structure rather than an empty folder: a small directory skeleton plus the
|
||
`AGENTS.md` that tells an agent where a new note goes, when something is
|
||
archived, and what a good filename looks like — which is the part that keeps
|
||
a shared folder from rotting into a pile. The hub seeds it at creation, so it
|
||
is already there for the browser and for every device that connects later;
|
||
joining a project that already exists never restructures it, and
|
||
`--template` is refused together with `--only` (scope rules live in the
|
||
synced `.bdriveignore`, so a scope that left out the template's folders would
|
||
hide them for the whole team). Creating a project in the web UI offers the
|
||
same three starting points. It writes `.bdrive/config.json`, seeds a starter
|
||
`.bdriveignore` (node_modules, build dirs, caches, `.env*`), and starts the
|
||
daemon — local changes are detected within seconds, and the agent sync
|
||
hooks sync at every turn boundary. Not signed in yet? init runs the login
|
||
flow first.
|
||
|
||
Under the hood the `https://` remote speaks the hub's per-project
|
||
`/api/p/<id>/store` API — journal reads/writes relay through the server,
|
||
blob uploads go direct to the object store via the same short-lived
|
||
presigned URLs browser uploads use (falling back to relaying when the
|
||
backend can't presign). Client pushes and project creation require the
|
||
server to run with `--upload`; against a read-only hub, clients still pull
|
||
and `bdrive status` reports `access: read-only (pull only)` rather than
|
||
pretending to be offline.
|
||
|
||
### Sharing files by URL
|
||
|
||
For teammates, every synced file already has an internal link — the hub
|
||
viewer URL, gated by sign-in and the project's org membership:
|
||
|
||
```console
|
||
$ bdrive url wiki/report.html
|
||
https://drive.example.com/1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d/wiki/report.html
|
||
```
|
||
|
||
It's computed locally (no network), always shows the latest synced
|
||
content, and is the link agents should drop in their replies when they
|
||
create an artifact in the shared folder (`--sync` pushes first so a
|
||
just-created file resolves immediately).
|
||
|
||
For people **outside** the hub, any synced file can instead be shared
|
||
with a public link — hand someone the URL and they see the file, no
|
||
account needed:
|
||
|
||
```console
|
||
$ bdrive share wiki/report.html
|
||
https://drive.example.com/s/eacc1df3ee6a6ebbdacc535c2796dc30
|
||
```
|
||
|
||
Links always serve the file's **latest** synced content (right for wiki
|
||
pages and living reports), and live until revoked — `bdrive share --list`
|
||
and `--revoke <token-or-url>` manage them, `--expires 24h` makes one
|
||
self-destruct. The web UI has a Share button on every file, and its
|
||
dialog can put an expiry on the link it just minted (24 hours, 7 days,
|
||
30 days) without changing the URL you already copied.
|
||
|
||
Shared HTML renders as a real page, markdown renders like the viewer
|
||
(with a small "Shared with BearDrive" footer; raw HTML is served
|
||
byte-for-byte), PDFs open inline. Rendering is sandboxed: `/s/*` responses
|
||
carry a strict CSP, never see auth cookies, and sit behind a generous
|
||
per-IP rate limit (`share_rpm`), so a malicious shared file's scripts
|
||
can't touch hub sessions and a scraper can't turn the hub into a CDN.
|
||
Any org member can mint links, and a link is public to whoever has the
|
||
URL — don't share folders that hold secrets, and note a LAN-bound hub
|
||
means LAN-only links.
|
||
|
||
### Agent integration
|
||
|
||
Setup is conversational — one paste and the agent does the rest, hooks
|
||
included ([Have your agent set it up](#have-your-agent-set-it-up-recommended)).
|
||
The payoff, once those hooks are in place: "write a report and share it"
|
||
becomes the agent generating `wiki/report.html` and replying with a link.
|
||
|
||
The web UI lists your orgs' projects in the sidebar (⌘K opens a command
|
||
palette: fuzzy file search, project switching, share/history/upload
|
||
actions); selecting one browses that project's files, and the **History**
|
||
view shows every change — which
|
||
account made it, when, from which device (name and OS — never the connecting
|
||
IP), with view/download of any past version (content is
|
||
content-addressed and retained forever; reverting to a version is the next
|
||
phase and the API is already shaped for it). Folder rows have a history
|
||
shortcut for a subtree feed; the topbar button shows the current file's
|
||
versions or the whole project feed.
|
||
|
||
Hubs also track **read heat**: viewer opens and downloads count as human
|
||
reads, share-link hits as share reads, and agent tool reads (reported by
|
||
the sync hooks via `bdrive read-log`) as agent reads — sync replication
|
||
never counts. Folder listings show heat dots and 30-day read counts to
|
||
every member, and every member gets the project **Dashboard**
|
||
(sidebar or ⋯ menu), four sections with an all/human/agent lens: a **treemap** of
|
||
every file (cell size = reads, color = staleness, ⚠ on hot+stale — click
|
||
through to any file), the **reads × freshness** scatter whose hot-but-stale
|
||
quadrant is the knowledge people rely on that nobody maintains, the
|
||
**hot path** (top files by reads, agent/human split — effectively the
|
||
team's agent context window), and an **agent coverage matrix** (which
|
||
agent devices read which folders). The API
|
||
(`GET /api/p/<id>/heat?prefix=&days=`) exposes only aggregate counts,
|
||
distinct-reader counts, and last-read times — never who read what;
|
||
`?by=device` adds the agent-only per-device folder breakdown (device
|
||
identity is already public via history; human emails never appear).
|
||
|
||
### Authentication & database
|
||
|
||
Hubs always require sign-in — every change is attributed to a real
|
||
account. **Signup is invite-only by default** (the safe posture for a
|
||
public URL); self-service signup opens only with a gate (admin approval,
|
||
or allowed domains + email verification). Hub metadata (accounts,
|
||
projects, orgs, shares) lives in a file-backed store by default, or
|
||
SQLite/Postgres (incl. Supabase) via the `database` config block.
|
||
|
||
Full reference — the three signup postures, SMTP, admins, CLI device
|
||
sign-in, and database selection: **[docs/self-hosting.md](docs/self-hosting.md)**.
|
||
|
||
|
||
### Uploads
|
||
|
||
The browser client is deliberately storage-blind: it never sees the remote
|
||
URL, bucket, or any credentials. On page load it fetches `/api/config` and
|
||
follows whatever the server allows.
|
||
|
||
With `--upload` set, the server decides per upload how the bytes travel:
|
||
|
||
- **Direct** — for backends that can presign (S3 and S3-compatible stores;
|
||
GCS when the server runs with credentials that can sign, e.g. a service
|
||
account): the server mints a short-lived presigned `PUT` URL for the
|
||
content-addressed blob (`blobs/<sha256>`), the browser uploads straight
|
||
to the object store, then asks the server to commit. The commit verifies
|
||
the blob actually exists and appends a `put` op to the *server's own*
|
||
journal — the blobs-before-journal ordering and the one-writer-per-journal
|
||
invariant both hold. Expired URLs are refused by the store; the client
|
||
just re-runs init. Direct uploads to a bucket also need a CORS rule on
|
||
the bucket allowing `PUT` from the viewer's origin.
|
||
- **Through the server** — `file://` remotes and plain-folder serving can't
|
||
presign, so the client sends content to the server, which stores it
|
||
(object store + journal, or straight to disk for a served folder, where
|
||
the daemon will pick it up like any local edit).
|
||
|
||
## How it works
|
||
|
||
```
|
||
working folder ←materialize/scan→ local volume store ←push/pull→ object store
|
||
(real files) ~/.bdrive/volumes/<vol> s3:// gs:// file://
|
||
├─ blobs/ content-addressed (sha256)
|
||
├─ journal/ one append-only op log per device
|
||
├─ state.json what's materialized
|
||
└─ sync.json lamport clock + push cursor
|
||
```
|
||
|
||
- Every change becomes an **op** (`put`/`delete`) in this device's
|
||
append-only journal, stamped with a lamport clock, wall-clock time, device
|
||
ID, and author. File content goes into a content-addressed blob store.
|
||
- A **sync** uploads new blobs, then the journal; it downloads other
|
||
devices' journals and any blobs it's missing. Since each device writes
|
||
only its own journal, there are no concurrent writers per object and any
|
||
dumb object store suffices.
|
||
- The folder's state is a deterministic **replay** of all journals ordered
|
||
by `(lamport, time, device)` — every device converges to the same view.
|
||
Concurrent edits keep the last writer at the path; the loser is preserved
|
||
as a conflict-copy file by the device that detects the overlap.
|
||
- A per-mount **daemon** scans the folder every few seconds (cheap
|
||
size+mtime check) and exchanges with the remote every ~10s — or
|
||
immediately after local edits. Tunable with --scan-interval and
|
||
--remote-interval on the daemon (defaults 3s / 10s).
|
||
|
||
### What beardrive does not sync
|
||
|
||
`.git` directories (per-file LWW would corrupt repositories), `.DS_Store`,
|
||
the `.bdrive` settings file, its own temp files, nested mounts (a
|
||
subdirectory with its own `.bdrive/config.json` syncs only through its own
|
||
project — the parent never scans into it, writes over it, or propagates
|
||
deletes for it), and anything excluded by `.bdriveignore` or omitted from an
|
||
`include` list. Empty directories are not tracked (like git).
|
||
|
||
## Roadmap
|
||
|
||
See [ROADMAP.md](ROADMAP.md) — the public, dated roadmap, including the
|
||
items we'd love help with. Highlights: `beardrive restore <path>@<time>`
|
||
(time travel — all content is already retained), FUSE/NFS mount mode,
|
||
journal compaction & blob GC, per-path access scopes for multi-agent
|
||
setups.
|
||
|
||
## Development
|
||
|
||
Contributions welcome — see [CONTRIBUTING.md](CONTRIBUTING.md) for the
|
||
build/test workflow and the rules that matter, [ROADMAP.md](ROADMAP.md)
|
||
for where help is wanted, and [CHANGELOG.md](CHANGELOG.md) for what
|
||
shipped when. Self-hosting a hub: [docs/self-hosting.md](docs/self-hosting.md).
|
||
|
||
```sh
|
||
go build ./...
|
||
go test ./...
|
||
```
|
||
|
||
The integration tests in `internal/syncer` simulate multiple devices syncing
|
||
through a `file://` remote, including offline operation and concurrent-edit
|
||
conflicts. Set `BDRIVE_HOME` to relocate all beardrive state (used heavily in tests).
|
||
|
||
### Web frontend
|
||
|
||
The hub's web UI is a React + TypeScript app in `internal/webapp/frontend`
|
||
(Vite + Tailwind v4 + shadcn/ui components owned in-repo; TanStack
|
||
query/table/virtual, react-hook-form + zod, cmdk, sonner, lucide-react —
|
||
routing stays a small in-repo history router). Its
|
||
**built output is committed** at `internal/webapp/static`, the `go:embed`
|
||
target, so building or `go install`-ing the binary never needs Node.
|
||
|
||
Only when changing `frontend/src`:
|
||
|
||
```sh
|
||
cd internal/webapp/frontend
|
||
npm install
|
||
npm run dev # hot-reload dev server, proxying /api to a local hub
|
||
# (BDRIVE_DEV_PROXY=http://localhost:8993 to point elsewhere)
|
||
npm run build # rebuild internal/webapp/static — commit the result
|
||
npm run e2e # Playwright suite; starts its own seeded hub on :8993
|
||
./check-dist.sh # verify the committed static/ is fresh (pre-release check)
|
||
```
|
||
|
||
## License
|
||
|
||
GNU AGPL-3.0 — Copyright 2026 Runbear, Inc. See [LICENSE](LICENSE).
|
||
|
||
We chose AGPL-3.0 deliberately: it keeps BearDrive fully open and
|
||
self-hostable forever while preventing a cloud provider from offering a
|
||
closed BearDrive-as-a-service. The managed service at beardrive.ai funds
|
||
the project; the code stays open.
|
||
|
||
Everything in this repo is open source and self-hostable: a complete BearDrive
|
||
server for one organization's deployment, teams included. The managed service
|
||
at beardrive.ai is the same core plus what only makes sense as an operated
|
||
service — hosting, PropelAuth SSO, billing and plan quotas, backups, and
|
||
support. Provider-specific and billing code stays out of this repo permanently;
|
||
the server exposes interfaces (`AuthProvider`, `QuotaProvider`) that the
|
||
managed deployment fills in.
|