From 074cc4bfa82788ba8dfa9487f23e0fc45e83088c Mon Sep 17 00:00:00 2001 From: Snow Lee Date: Fri, 10 Jul 2026 09:01:11 -0700 Subject: [PATCH 1/2] feat(cli): add `bdrive logout`; clarify `remote set` host kinds MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - New `bdrive logout` clears the saved token + account (folders untouched); `--forget` also drops the remembered server. Switching hubs is `bdrive login ` then re-`init`. - `remote set --help` now explains the two remote kinds — object storage (s3/gs/file) vs a bdrive hub (https://) — with examples, instead of leading with only s3://. (The https:// hub scheme was already accepted.) - Docs updated (README, CLAUDE.md, SKILL.md). Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01R7Q9ZKSZRTdvrSJkYLUmYs --- CLAUDE.md | 2 +- README.md | 8 +++-- cmd/bdrive/cmds.go | 19 ++++++++++-- cmd/bdrive/login.go | 50 ++++++++++++++++++++++++++++++++ cmd/bdrive/main.go | 1 + plugin/skills/beardrive/SKILL.md | 3 +- 6 files changed, 77 insertions(+), 6 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 9cef551..18e38bd 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -41,7 +41,7 @@ Package roles (`internal/`): - **`config`** — global state under `$BDRIVE_HOME` (default `~/.bdrive`): device identity (`device.json`), settings (`settings.json`: default server + device token + signed-in account), and the mount registry (`mounts.json`, keyed by **stable mount id**, holding only each mount's last-known path). The per-folder `.bdrive/` directory (`project.go`) holds `config.json` with the mount id + volume/remote/include; **nothing is keyed by the folder path**, so renames/moves are free — `ResolveMount` self-heals the registry path, and the volume store lives at `~/.bdrive/volumes//`. `.bdrive/` is never synced and holds no credentials. - **`webapp`** — the `bdrive web` server, in two modes. Single-volume: `Source` is a `DirSource` (plain folder from disk) or `RemoteSource` (folds journals into a file tree with per-file provenance). Hub: `Root` + `Projects` host many projects on one storage root, each under `//` via `remote.Prefixed`; `ProjectDB` (`projects.go`) is a file-backed registry (JSON, loaded at open, rewritten atomically per change) with create-or-join-by-name semantics, name-scoped per organization. Orgs (`orgs.go`, file-backed `orgs.json`) wall projects by membership (email → owner|member): every per-project route — viewer APIs, uploads, history, shares management, the `/store/*` sync proxy — 403s for non-members, `/api/projects` lists only your orgs' projects, owners mint expiring multi-use invite links (`/join/`), and a pre-org hub migrates all projects into a "default" org (all existing accounts join, oldest owns) at startup. `QuotaProvider` (`quota.go`) is the plan-enforcement seam mirroring `AuthProvider` — CheckWrite/RecordUsage on every write path, CheckSeat on invite redemption; OSS ships only `UnlimitedQuota`, managed deployments swap the provider. Renders markdown (goldmark + Obsidian `[[wikilinks]]`). With `--upload` it accepts writes: browser uploads (`upload.go` — direct-to-storage via presigned URLs when the backend implements `remote.PutSigner`, relayed otherwise; ops journaled under the server's own device) and the per-project `/api/p//store/*` proxy (`store.go`) that whole devices sync through — the `https://` remote backend (`remote/http.go`) is its client; journals are never presigned, only immutable blobs. Frontend is dependency-free vanilla JS embedded via `go:embed static`; it learns everything from `/api/config` (+ `/api/projects` in hub mode) and never sees storage info or credentials. It uses native History-API path routing (`//` in hub mode, `/` in volume mode, `/join/` for invites — no `#`, slashes stay literal); `Server.frontend` serves `index.html` as the SPA fallback for any non-asset, non-API/auth/share route so deep links and refreshes resolve, and all client API/asset URLs are root-absolute so a deep path doesn't break relative resolution. **Hub metadata persistence** (accounts, projects, orgs+invites, shares, devices — never blobs or journals) sits behind a pluggable `MetaStore` of typed repos (`db.go`): the service structs (`BuiltinAuth`, `OrgDB`, `ProjectDB`, `ShareDB`, `DeviceRegistry`) keep their in-memory maps + logic and persist each change as one record through a repo. Two backends — `db_file.go` (the historical JSON files, still the zero-dep default, reached via the `Open*(path)` constructors) and `db_sql.go` (one `database/sql` impl over pure-Go drivers: `modernc.org/sqlite` locally, `jackc/pgx` for Postgres/Supabase, portable schema + idempotent migrations + transactional multi-row writes). `web.go`'s `database` config (`{driver:file|sqlite|postgres, dsn}`) selects it; file is default and untouched. `db_conformance_test.go` runs the same service ops against every backend. -`cmd/bdrive/` is a thin cobra CLI over these packages (`login`, `init`, `stop`, `sync`, `status`, `log`, `remote`, `web`, `whoami`, `daemon`, `version` — `mnt`/`umnt` are gone; `init` is the front door and `stop` pauses). `bdrive login` signs the device in (bare form uses the remembered server or `config.DefaultServer` = beardrive.ai; loopback-callback browser flow in `login.go`, `--device` for headless) and stores server+token+account in `settings.json`. `bdrive init` is interactive on a TTY (survey menus: create-new vs connect-existing with a project list; whole-folder vs `--shared `, which becomes the include list) with full flag bypass (`--name/--project/--shared/--yes`) and never prompts without a TTY; it runs the login flow first when there is no session, writes `.bdrive/config.json`, seeds `.bdriveignore`, and starts sync via `startSync`; re-running it resumes — including after a folder move. `bdrive web -c config.json` configures the server from a file, explicit flags winning. +`cmd/bdrive/` is a thin cobra CLI over these packages (`login`, `logout`, `init`, `stop`, `sync`, `status`, `log`, `remote`, `web`, `whoami`, `daemon`, `version` — `mnt`/`umnt` are gone; `init` is the front door and `stop` pauses). `bdrive login` signs the device in (bare form uses the remembered server or `config.DefaultServer` = beardrive.ai; loopback-callback browser flow in `login.go`, `--device` for headless) and stores server+token+account in `settings.json`; `bdrive logout` clears the saved token+account (keeps the remembered server unless `--forget`) — switching hubs is `bdrive login ` then re-`init`. The per-folder remote (`bdrive remote set`) accepts object storage (`s3://`/`gs://`/`file://`) or a hub (`https://`), distinct from the device's logged-in server. `bdrive init` is interactive on a TTY (survey menus: create-new vs connect-existing with a project list; whole-folder vs `--shared `, which becomes the include list) with full flag bypass (`--name/--project/--shared/--yes`) and never prompts without a TTY; it runs the login flow first when there is no session, writes `.bdrive/config.json`, seeds `.bdriveignore`, and starts sync via `startSync`; re-running it resumes — including after a folder move. `bdrive web -c config.json` configures the server from a file, explicit flags winning. Authentication (`webapp/auth.go`, `authlocal.go`, `mail.go`) is **mandatory in hub mode** — the config's `auth` block tunes `users_db`/`allow_signup`/`allowed_domains`/`require_verification`/`require_approval`/`admins`/`smtp`; the plain-folder viewer stays auth-free — and sits behind the `AuthProvider` interface — the OSS server ships only `BuiltinAuth` (email+password accounts and device tokens in a file-backed `auth.json`; bcrypt for passwords, SHA-256 digests for tokens, plaintext never stored; server-owned `/auth/*` pages; one-time codes for the CLI callback and device flows; SMTP reset mail with a log-link fallback). **Signup is invite-only by default** (`allow_signup` defaults false): a valid org invite bootstraps an account even when self-signup is closed — `BuiltinAuth.InviteValid` (wired to `OrgDB.ValidInvite`) lets `pageSignup`/`pageLogin` offer account creation for a `/join/` target, and `signupInvited` skips the domain/verification/approval gates and activates immediately (the invite is the vetting). `BuiltinAuth.ValidateSignupPolicy` (called at hub startup, `web.go`) refuses an ungated open hub and email-verification-without-SMTP rather than silently leaving the door open. The three postures: invite-only (default), approval-gated (`require_approval`), and domain-restricted+verified (`allowed_domains`+`require_verification`+`smtp`); `allow_signup`/`allowed_domains`/`admins` stay server-config-owned so a browser session can't widen access. A managed deployment can swap in a different provider (e.g. PropelAuth) without touching the CLI or API — keep provider-specific code out of this repo. The sync client picks up its token from `BDRIVE_TOKEN` or `settings.json` and sends `X-Bdrive-Device{,-Name,-Os}` headers (`remote/http.go`); the hub's file-backed device registry (`webapp/devices.go`) records per-device name/OS/account/server-observed IP. Journal ops carry the signed-in account (`Op.User`/`UserName` from `Session.Account`; `Author` remains the git/OS fallback). History (`webapp/history.go`): `GET /api/p//history?path=|prefix=` (newest first, device-registry join) and `GET /api/p//blob?sha=` stream any exact version — blobs are retained forever, so the future revert phase is just re-putting an old blob as a new op. Share links (`webapp/shares.go`, file-backed `shares.json`): any signed-in member mints `/s/` public URLs (`bdrive share`, or the UI's Share button) serving the file's LATEST content until revoked (optional expiry); `/s/*` responses are sandboxed (CSP `sandbox allow-scripts`, no auth cookies) so shared HTML can't attack hub sessions — keep that header on any change; `/s/*` also sits behind a per-IP token bucket (`ratelimit.go`, `share_rpm` config), and markdown share pages get a "Shared with BearDrive" footer (raw HTML is never injected into). diff --git a/README.md b/README.md index 27b3d0a..488d4ac 100644 --- a/README.md +++ b/README.md @@ -113,7 +113,8 @@ beardrive uses each provider's standard credential chain — nothing beardrive-s | Command | Description | |---|---| -| `bdrive login [server-url]` | Sign this device in (browser flow; `--device` for headless; default server beardrive.ai) | +| `bdrive login [server-url]` | Sign this device in (browser flow; `--device` for headless; default server beardrive.ai). Switch hubs with `bdrive login ` | +| `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 — interactive on a TTY, flags (`--name/--project/--shared/--yes`) for scripts; re-run to resume | | `bdrive stop [folder]` | Stop syncing (files stay; `bdrive init` resumes) | | `bdrive share ` | Public URL for a synced file (`--list`, `--revoke`, `--expires`) | @@ -232,7 +233,10 @@ 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 — -`--status` shows the current server and account). `bdrive init` then, per +`--status` shows the current server and account). To move to a **different +hub**, run `bdrive login ` 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), and **sync the whole folder or only a shared subfolder** (e.g. `./shared`). Every question diff --git a/cmd/bdrive/cmds.go b/cmd/bdrive/cmds.go index a2cccbc..0dc6a0f 100644 --- a/cmd/bdrive/cmds.go +++ b/cmd/bdrive/cmds.go @@ -198,8 +198,23 @@ func remoteCmd() *cobra.Command { } set := &cobra.Command{ Use: "set ", - Short: "Set the remote (s3://bucket/prefix, gs://bucket/prefix, file:///path, https://bdrive-server)", - Args: cobra.ExactArgs(2), + Short: "Set where a folder syncs (object storage, or a bdrive hub)", + Long: `Point a folder at where it syncs. Two kinds of remote: + + Object storage — sync straight to a bucket you own (you hold the creds): + s3://bucket/prefix gs://bucket/prefix file:///abs/path + + A bdrive hub — sync through a bdrive web server that holds the storage + credentials for you (sign in first with "bdrive login "): + https://your-hub.example.com + +Switching hubs is usually done with "bdrive login " then "bdrive init", +which wires the folder to a project on that hub for you. Use "remote set" for +object storage, or to re-point a folder by hand.`, + Example: ` bdrive remote set ./notes s3://acme-bdrive/notes + bdrive remote set ./notes gs://acme-bdrive/notes + bdrive remote set ./notes https://drive.example.com`, + Args: cobra.ExactArgs(2), RunE: func(cmd *cobra.Command, args []string) error { folder, err := absFolder(args[:1]) if err != nil { diff --git a/cmd/bdrive/login.go b/cmd/bdrive/login.go index fb325a8..5e3f708 100644 --- a/cmd/bdrive/login.go +++ b/cmd/bdrive/login.go @@ -106,6 +106,56 @@ With no argument the remembered server is used, or ` + config.DefaultServer + `. return c } +func logoutCmd() *cobra.Command { + var forget bool + c := &cobra.Command{ + Use: "logout", + Short: "Sign this device out (clear the saved token)", + Long: `Clear this device's saved sign-in — the token and account — so it is no +longer authenticated to the bdrive server. The remembered server is kept so +"bdrive login" re-authenticates to it; pass --forget to clear that too. + +To switch to a different server, just run "bdrive login ". +Your synced folders are untouched; this only affects this device's session.`, + Example: ` bdrive logout # sign out, keep the server remembered + bdrive logout --forget # sign out and forget the server`, + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + settings, err := config.LoadSettings() + if err != nil { + return err + } + if settings.Token == "" && settings.Email == "" && !(forget && settings.Server != "") { + fmt.Println("already signed out") + return nil + } + who, server := settings.Email, settings.Server + settings.Token, settings.Email, settings.Name = "", "", "" + if forget { + settings.Server = "" + } + if err := config.SaveSettings(settings); err != nil { + return err + } + switch { + case who != "" && server != "" && !forget: + fmt.Printf("signed out %s from %s\n", who, server) + case who != "": + fmt.Printf("signed out %s\n", who) + default: + fmt.Println("signed out") + } + if forget { + fmt.Println("forgot the remembered server (run `bdrive login ` to set a new one)") + } + fmt.Println("note: the device token stays valid on the server until it expires — revoke it from the hub's device list if needed") + return nil + }, + } + c.Flags().BoolVar(&forget, "forget", false, "also forget the remembered server") + return c +} + // runLogin executes the sign-in flow against a server known to require auth // and persists server + token + account to settings. func runLogin(server string, cfg serverConfig, useDevice bool) error { diff --git a/cmd/bdrive/main.go b/cmd/bdrive/main.go index fef1c97..6048420 100644 --- a/cmd/bdrive/main.go +++ b/cmd/bdrive/main.go @@ -31,6 +31,7 @@ everything keeps working offline; changes sync when the remote is reachable.`, } root.AddCommand( loginCmd(), + logoutCmd(), initCmd(), shareCmd(), stopCmd(), diff --git a/plugin/skills/beardrive/SKILL.md b/plugin/skills/beardrive/SKILL.md index 91f93d0..2776a22 100644 --- a/plugin/skills/beardrive/SKILL.md +++ b/plugin/skills/beardrive/SKILL.md @@ -21,7 +21,8 @@ Use this skill whenever the user is working with the `bdrive` CLI: initializing | Change history | `bdrive log [] [-p path] [-n N]` | | Show / set remote | `bdrive remote []` · `bdrive remote set ` | | This device's identity | `bdrive whoami` | -| Sign this device in (once per device) | `bdrive login [url]` — bare form uses the remembered server or beardrive.ai. Opens the sign-in page in a browser (sign-up available there); the terminal completes on its own and stores a per-device token. `--device` prints a code to approve from any browser (SSH/headless); `--status` shows server + account. Password reset: "Forgot password?" on the sign-in page (emailed via the server's SMTP config, or the link appears in the server log). | +| Sign this device in (once per device) | `bdrive login [url]` — bare form uses the remembered server or beardrive.ai. Opens the sign-in page in a browser (sign-up available there); the terminal completes on its own and stores a per-device token. `--device` prints a code to approve from any browser (SSH/headless); `--status` shows server + account. Password reset: "Forgot password?" on the sign-in page (emailed via the server's SMTP config, or the link appears in the server log). **Switch hubs** with `bdrive login `, then re-run `bdrive init` in each folder. | +| Sign this device out | `bdrive logout` — clears the saved token + account (folders untouched); `--forget` also drops the remembered server. The device token stays valid server-side until it expires — revoke it from the hub's device list to be sure. | | Share a synced file publicly by URL | `bdrive share ` — prints a link anyone can open (HTML renders as a page, markdown rendered, PDFs inline; sandboxed; always the latest content; no account needed). `--expires 24h` for self-destructing links; `--list` / `--revoke ` to manage. Put generated reports in the shared folder, sync, then share. | | Set up a project for a Claude Code team | `/beardrive:install` — installs the CLI, signs in, runs init (whole/shared folder), offers a CLAUDE.md section about the shared folder, and registers project-level hooks (blocking pull at prompt-submit, async push after Write/Edit) in `.claude/settings.json` | | Per-file / folder change history in the web UI | History button (file versions or project feed) and per-folder ⌚ — each entry: account, time, device (name/OS/IP), view/download of that exact version. API: `GET /api/p//history?path=\|prefix=`, `GET /api/p//blob?sha=` | From b06d0d6a328395c9fd86edd86b36ae9395c0056c Mon Sep 17 00:00:00 2001 From: Snow Lee Date: Fri, 10 Jul 2026 09:55:58 -0700 Subject: [PATCH 2/2] =?UTF-8?q?feat(cli)!:=20hub-only=20=E2=80=94=20remove?= =?UTF-8?q?=20`remote`=20command;=20reposition=20docs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BearDrive is now hub-based only. Clients sync through a `bdrive web` hub over https and never talk to object storage directly. - Remove the `bdrive remote` / `bdrive remote set` command entirely. Wiring a folder's remote is `bdrive init` (always a hub); switching hubs is `bdrive login ` then re-`init`. Object storage stays as the *hub's* backing store (`bdrive web s3://…`), never a client remote. - Reposition README, CLAUDE.md, and the beardrive skill from "no server required / direct-to-bucket" to hub-based; section 2 of the skill is now "storing a hub's data" (server-side), and the stale `init --remote` / `remote set` examples are fixed. BREAKING: direct client-to-bucket sync is no longer supported. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01R7Q9ZKSZRTdvrSJkYLUmYs --- CLAUDE.md | 4 +- README.md | 22 +++++----- cmd/bdrive/cmds.go | 72 -------------------------------- cmd/bdrive/main.go | 1 - plugin/skills/beardrive/SKILL.md | 36 +++++++--------- 5 files changed, 30 insertions(+), 105 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 18e38bd..f2e71ab 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## What this is -**BearDrive** is the product name; **`bdrive`** is its CLI binary (file conventions: the `.bdrive/` settings directory and `.bdriveignore` at the project root, `~/.bdrive` home, `BDRIVE_HOME`). BearDrive is a Go CLI that mounts any folder as a synced volume: contents sync across devices through cloud object storage (S3, GCS, S3-compatible, or a plain directory), with per-file change history and offline support. No server required — devices converge through append-only journals in a dumb object store; an optional `bdrive web` server can front the store as a sync hub for storage-blind client devices. +**BearDrive** is the product name; **`bdrive`** is its CLI binary (file conventions: the `.bdrive/` settings directory and `.bdriveignore` at the project root, `~/.bdrive` home, `BDRIVE_HOME`). BearDrive is a Go CLI that mounts any folder as a synced volume: contents sync across devices and teammates through a **`bdrive web` hub**, with accounts, per-file change history, and offline support. Clients are storage-blind — they sync through the hub over `https://` and never hold storage credentials; the hub owns the object store (S3, GCS, S3-compatible, or a plain directory) and devices converge through append-only journals in it. (Direct client-to-bucket sync without a hub is no longer supported; the object-storage backends exist only as the hub's own storage.) The repo ships one binary: `cmd/bdrive` — the CLI, the sync daemon, and the web server (`bdrive web`: viewer, uploads, multi-project sync hub). @@ -41,7 +41,7 @@ Package roles (`internal/`): - **`config`** — global state under `$BDRIVE_HOME` (default `~/.bdrive`): device identity (`device.json`), settings (`settings.json`: default server + device token + signed-in account), and the mount registry (`mounts.json`, keyed by **stable mount id**, holding only each mount's last-known path). The per-folder `.bdrive/` directory (`project.go`) holds `config.json` with the mount id + volume/remote/include; **nothing is keyed by the folder path**, so renames/moves are free — `ResolveMount` self-heals the registry path, and the volume store lives at `~/.bdrive/volumes//`. `.bdrive/` is never synced and holds no credentials. - **`webapp`** — the `bdrive web` server, in two modes. Single-volume: `Source` is a `DirSource` (plain folder from disk) or `RemoteSource` (folds journals into a file tree with per-file provenance). Hub: `Root` + `Projects` host many projects on one storage root, each under `//` via `remote.Prefixed`; `ProjectDB` (`projects.go`) is a file-backed registry (JSON, loaded at open, rewritten atomically per change) with create-or-join-by-name semantics, name-scoped per organization. Orgs (`orgs.go`, file-backed `orgs.json`) wall projects by membership (email → owner|member): every per-project route — viewer APIs, uploads, history, shares management, the `/store/*` sync proxy — 403s for non-members, `/api/projects` lists only your orgs' projects, owners mint expiring multi-use invite links (`/join/`), and a pre-org hub migrates all projects into a "default" org (all existing accounts join, oldest owns) at startup. `QuotaProvider` (`quota.go`) is the plan-enforcement seam mirroring `AuthProvider` — CheckWrite/RecordUsage on every write path, CheckSeat on invite redemption; OSS ships only `UnlimitedQuota`, managed deployments swap the provider. Renders markdown (goldmark + Obsidian `[[wikilinks]]`). With `--upload` it accepts writes: browser uploads (`upload.go` — direct-to-storage via presigned URLs when the backend implements `remote.PutSigner`, relayed otherwise; ops journaled under the server's own device) and the per-project `/api/p//store/*` proxy (`store.go`) that whole devices sync through — the `https://` remote backend (`remote/http.go`) is its client; journals are never presigned, only immutable blobs. Frontend is dependency-free vanilla JS embedded via `go:embed static`; it learns everything from `/api/config` (+ `/api/projects` in hub mode) and never sees storage info or credentials. It uses native History-API path routing (`//` in hub mode, `/` in volume mode, `/join/` for invites — no `#`, slashes stay literal); `Server.frontend` serves `index.html` as the SPA fallback for any non-asset, non-API/auth/share route so deep links and refreshes resolve, and all client API/asset URLs are root-absolute so a deep path doesn't break relative resolution. **Hub metadata persistence** (accounts, projects, orgs+invites, shares, devices — never blobs or journals) sits behind a pluggable `MetaStore` of typed repos (`db.go`): the service structs (`BuiltinAuth`, `OrgDB`, `ProjectDB`, `ShareDB`, `DeviceRegistry`) keep their in-memory maps + logic and persist each change as one record through a repo. Two backends — `db_file.go` (the historical JSON files, still the zero-dep default, reached via the `Open*(path)` constructors) and `db_sql.go` (one `database/sql` impl over pure-Go drivers: `modernc.org/sqlite` locally, `jackc/pgx` for Postgres/Supabase, portable schema + idempotent migrations + transactional multi-row writes). `web.go`'s `database` config (`{driver:file|sqlite|postgres, dsn}`) selects it; file is default and untouched. `db_conformance_test.go` runs the same service ops against every backend. -`cmd/bdrive/` is a thin cobra CLI over these packages (`login`, `logout`, `init`, `stop`, `sync`, `status`, `log`, `remote`, `web`, `whoami`, `daemon`, `version` — `mnt`/`umnt` are gone; `init` is the front door and `stop` pauses). `bdrive login` signs the device in (bare form uses the remembered server or `config.DefaultServer` = beardrive.ai; loopback-callback browser flow in `login.go`, `--device` for headless) and stores server+token+account in `settings.json`; `bdrive logout` clears the saved token+account (keeps the remembered server unless `--forget`) — switching hubs is `bdrive login ` then re-`init`. The per-folder remote (`bdrive remote set`) accepts object storage (`s3://`/`gs://`/`file://`) or a hub (`https://`), distinct from the device's logged-in server. `bdrive init` is interactive on a TTY (survey menus: create-new vs connect-existing with a project list; whole-folder vs `--shared `, which becomes the include list) with full flag bypass (`--name/--project/--shared/--yes`) and never prompts without a TTY; it runs the login flow first when there is no session, writes `.bdrive/config.json`, seeds `.bdriveignore`, and starts sync via `startSync`; re-running it resumes — including after a folder move. `bdrive web -c config.json` configures the server from a file, explicit flags winning. +`cmd/bdrive/` is a thin cobra CLI over these packages (`login`, `logout`, `init`, `stop`, `sync`, `status`, `log`, `web`, `whoami`, `daemon`, `version` — `mnt`/`umnt`/`remote` are gone; `init` is the front door and `stop` pauses). `bdrive login` signs the device in (bare form uses the remembered server or `config.DefaultServer` = beardrive.ai; loopback-callback browser flow in `login.go`, `--device` for headless) and stores server+token+account in `settings.json`; `bdrive logout` clears the saved token+account (keeps the remembered server unless `--forget`). Switching hubs is `bdrive login ` then re-`init` — `init` is the only thing that writes a folder's remote (always a hub, `server + "/p/" + id`); there is no client command to point a folder at a raw bucket. `bdrive init` is interactive on a TTY (survey menus: create-new vs connect-existing with a project list; whole-folder vs `--shared `, which becomes the include list) with full flag bypass (`--name/--project/--shared/--yes`) and never prompts without a TTY; it runs the login flow first when there is no session, writes `.bdrive/config.json`, seeds `.bdriveignore`, and starts sync via `startSync`; re-running it resumes — including after a folder move. `bdrive web -c config.json` configures the server from a file, explicit flags winning. Authentication (`webapp/auth.go`, `authlocal.go`, `mail.go`) is **mandatory in hub mode** — the config's `auth` block tunes `users_db`/`allow_signup`/`allowed_domains`/`require_verification`/`require_approval`/`admins`/`smtp`; the plain-folder viewer stays auth-free — and sits behind the `AuthProvider` interface — the OSS server ships only `BuiltinAuth` (email+password accounts and device tokens in a file-backed `auth.json`; bcrypt for passwords, SHA-256 digests for tokens, plaintext never stored; server-owned `/auth/*` pages; one-time codes for the CLI callback and device flows; SMTP reset mail with a log-link fallback). **Signup is invite-only by default** (`allow_signup` defaults false): a valid org invite bootstraps an account even when self-signup is closed — `BuiltinAuth.InviteValid` (wired to `OrgDB.ValidInvite`) lets `pageSignup`/`pageLogin` offer account creation for a `/join/` target, and `signupInvited` skips the domain/verification/approval gates and activates immediately (the invite is the vetting). `BuiltinAuth.ValidateSignupPolicy` (called at hub startup, `web.go`) refuses an ungated open hub and email-verification-without-SMTP rather than silently leaving the door open. The three postures: invite-only (default), approval-gated (`require_approval`), and domain-restricted+verified (`allowed_domains`+`require_verification`+`smtp`); `allow_signup`/`allowed_domains`/`admins` stay server-config-owned so a browser session can't widen access. A managed deployment can swap in a different provider (e.g. PropelAuth) without touching the CLI or API — keep provider-specific code out of this repo. The sync client picks up its token from `BDRIVE_TOKEN` or `settings.json` and sends `X-Bdrive-Device{,-Name,-Os}` headers (`remote/http.go`); the hub's file-backed device registry (`webapp/devices.go`) records per-device name/OS/account/server-observed IP. Journal ops carry the signed-in account (`Op.User`/`UserName` from `Session.Account`; `Author` remains the git/OS fallback). History (`webapp/history.go`): `GET /api/p//history?path=|prefix=` (newest first, device-registry join) and `GET /api/p//blob?sha=` stream any exact version — blobs are retained forever, so the future revert phase is just re-putting an old blob as a new op. Share links (`webapp/shares.go`, file-backed `shares.json`): any signed-in member mints `/s/` public URLs (`bdrive share`, or the UI's Share button) serving the file's LATEST content until revoked (optional expiry); `/s/*` responses are sandboxed (CSP `sandbox allow-scripts`, no auth cookies) so shared HTML can't attack hub sessions — keep that header on any change; `/s/*` also sits behind a per-IP token bucket (`ratelimit.go`, `share_rpm` config), and markdown share pages get a "Shared with BearDrive" footer (raw HTML is never injected into). diff --git a/README.md b/README.md index 488d4ac..119437f 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,11 @@ # BearDrive — Google Drive for AI agents **BearDrive** mounts any folder as a synced volume: its contents stay -synchronized across all your devices through cloud object storage, every -change is tracked (who, when, on which device), and everything keeps -working offline. The CLI is `bdrive`. +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 web` server you (or we) run on an object store — clients sync +through it over HTTPS and never touch the storage directly. Two things it's for: **sharing files with people** — any synced file becomes a public URL that renders as a page — and **sharing context across @@ -33,16 +35,17 @@ $ bdrive login && cd ~/workspace && bdrive init 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 remote. Each - device only writes its own append-only journal, so no locking service or - server is needed — any object store works. +- **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, IP). Content is stored content-addressed, so every version is retained — view or download any point in a file's history. -- **Cloud-provider agnostic** — 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). +- **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. @@ -121,7 +124,6 @@ beardrive uses each provider's standard credential chain — nothing beardrive-s | `bdrive sync [folder]` | Run one sync cycle now | | `bdrive status [folder]` | Projects, daemon state, pending changes | | `bdrive log [folder] [-p path] [-n N]` | Change history: account, device, time, file | -| `bdrive remote [folder]` / `bdrive remote set ` | Show / set the remote (advanced, incl. direct-to-bucket) | | `bdrive web [folder \| storage-root-url]` | Web server: viewer (rendered markdown, downloads, history), uploads, multi-project sync hub | | `bdrive whoami` | Device identity used in change tracking | diff --git a/cmd/bdrive/cmds.go b/cmd/bdrive/cmds.go index 0dc6a0f..35ae9c6 100644 --- a/cmd/bdrive/cmds.go +++ b/cmd/bdrive/cmds.go @@ -2,7 +2,6 @@ package main import ( "fmt" - "net/url" "time" "github.com/spf13/cobra" @@ -174,77 +173,6 @@ func logCmd() *cobra.Command { return c } -func remoteCmd() *cobra.Command { - c := &cobra.Command{ - Use: "remote [folder]", - Short: "Show or set the cloud remote of a mounted folder", - Args: cobra.MaximumNArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - folder, err := absFolder(args) - if err != nil { - return err - } - proj, err := mustProject(folder) - if err != nil { - return err - } - if proj.Remote == "" { - fmt.Println("(none)") - } else { - fmt.Println(proj.Remote) - } - return nil - }, - } - set := &cobra.Command{ - Use: "set ", - Short: "Set where a folder syncs (object storage, or a bdrive hub)", - Long: `Point a folder at where it syncs. Two kinds of remote: - - Object storage — sync straight to a bucket you own (you hold the creds): - s3://bucket/prefix gs://bucket/prefix file:///abs/path - - A bdrive hub — sync through a bdrive web server that holds the storage - credentials for you (sign in first with "bdrive login "): - https://your-hub.example.com - -Switching hubs is usually done with "bdrive login " then "bdrive init", -which wires the folder to a project on that hub for you. Use "remote set" for -object storage, or to re-point a folder by hand.`, - Example: ` bdrive remote set ./notes s3://acme-bdrive/notes - bdrive remote set ./notes gs://acme-bdrive/notes - bdrive remote set ./notes https://drive.example.com`, - Args: cobra.ExactArgs(2), - RunE: func(cmd *cobra.Command, args []string) error { - folder, err := absFolder(args[:1]) - if err != nil { - return err - } - raw := args[1] - u, err := url.Parse(raw) - if err != nil || (u.Scheme != "s3" && u.Scheme != "gs" && u.Scheme != "file" && u.Scheme != "http" && u.Scheme != "https") { - return fmt.Errorf("invalid remote %q (want s3://bucket/prefix, gs://bucket/prefix, file:///path, or https://bdrive-server)", raw) - } - proj, err := mustProject(folder) - if err != nil { - return err - } - proj.Remote = raw - if proj, err = config.SaveProject(folder, proj); err != nil { - return err - } - if _, _, err := config.ResolveMount(folder); err != nil { // sync the registry - return err - } - fmt.Printf("remote of %s set to %s\n", folder, raw) - fmt.Println("run `bdrive sync` to sync now (a running daemon picks it up automatically)") - return nil - }, - } - c.AddCommand(set) - return c -} - func daemonCmd() *cobra.Command { c := &cobra.Command{ Use: "daemon", diff --git a/cmd/bdrive/main.go b/cmd/bdrive/main.go index 6048420..1258d29 100644 --- a/cmd/bdrive/main.go +++ b/cmd/bdrive/main.go @@ -38,7 +38,6 @@ everything keeps working offline; changes sync when the remote is reachable.`, syncCmd(), statusCmd(), logCmd(), - remoteCmd(), webCmd(), whoamiCmd(), daemonCmd(), diff --git a/plugin/skills/beardrive/SKILL.md b/plugin/skills/beardrive/SKILL.md index 2776a22..173181e 100644 --- a/plugin/skills/beardrive/SKILL.md +++ b/plugin/skills/beardrive/SKILL.md @@ -1,13 +1,13 @@ --- name: beardrive -description: Use BearDrive — a synced file system for AI agents and teams. Start syncing any project folder (bdrive init) and it stays in sync across devices and teammates through a BearDrive server or object storage, with accounts, per-file change history, public share links, and offline support. Use when the user wants to "set up beardrive", "sync this folder", "mount a folder", "share this file by URL", "start/stop syncing", "connect to a beardrive server", "check bdrive status", "see what changed", "who changed this file?", or troubleshoot a stuck sync. +description: Use BearDrive — a synced file system for AI agents and teams. Start syncing any project folder (bdrive init) and it stays in sync across devices and teammates through a BearDrive hub, with accounts, per-file change history, public share links, and offline support. Use when the user wants to "set up beardrive", "sync this folder", "share this file by URL", "start/stop syncing", "connect to a beardrive server", "switch to a different hub", "check bdrive status", "see what changed", "who changed this file?", or troubleshoot a stuck sync. --- # BearDrive — synced file system for AI agents -**BearDrive** (CLI: `bdrive`) turns any folder into a synced project: a background daemon per project scans for local changes and exchanges with the server (or object store). Files on disk are always real files — every tool, editor, and agent works on them with no integration. +**BearDrive** (CLI: `bdrive`) turns any folder into a synced project: a background daemon per project scans for local changes and exchanges them with a **hub** (a `bdrive web` server). Files on disk are always real files — every tool, editor, and agent works on them with no integration. -Use this skill whenever the user is working with the `bdrive` CLI: initializing or stopping projects, syncing, sharing files by URL, configuring a remote, inspecting state, reading change history, or debugging. ("Mount" in older docs = today's `bdrive init`.) +Use this skill whenever the user is working with the `bdrive` CLI: initializing or stopping projects, syncing, sharing files by URL, signing in / switching hubs, inspecting state, reading change history, or debugging. ("Mount" in older docs = today's `bdrive init`.) ## Command map @@ -19,7 +19,6 @@ Use this skill whenever the user is working with the `bdrive` CLI: initializing | One sync cycle now | `bdrive sync []` | | Mounts + daemon + pending state | `bdrive status []` | | Change history | `bdrive log [] [-p path] [-n N]` | -| Show / set remote | `bdrive remote []` · `bdrive remote set ` | | This device's identity | `bdrive whoami` | | Sign this device in (once per device) | `bdrive login [url]` — bare form uses the remembered server or beardrive.ai. Opens the sign-in page in a browser (sign-up available there); the terminal completes on its own and stores a per-device token. `--device` prints a code to approve from any browser (SSH/headless); `--status` shows server + account. Password reset: "Forgot password?" on the sign-in page (emailed via the server's SMTP config, or the link appears in the server log). **Switch hubs** with `bdrive login `, then re-run `bdrive init` in each folder. | | Sign this device out | `bdrive logout` — clears the saved token + account (folders untouched); `--forget` also drops the remembered server. The device token stays valid server-side until it expires — revoke it from the hub's device list to be sure. | @@ -92,7 +91,7 @@ bdrive login https://drive.example.com:4173 cd ~/agent-workspace && bdrive init --name agent-workspace ``` -Devices connecting the same project (by name or id) converge through the hub. Direct-to-bucket setups (no hub) remain possible via `bdrive remote set s3://…` after an offline init. +Devices connecting the same project (by name or id) converge through the hub. A hub is required — clients always sync through a `bdrive web` server and never talk to the object store directly. Hub projects belong to an **organization**: only members of the project's org can see or sync it (project names are scoped per org too). Your first `bdrive init` creates your org automatically. **Hubs are invite-only by default** — the safe posture for a public URL. To give a teammate access, an org **owner** opens the web UI and clicks **Invite** in the sidebar footer — it mints an expiring join link (`…/join/`); the teammate opens it and creates an account through the link (invites bootstrap signup even when public self-signup is closed), and is in. An admin can instead open self-service signup with a gate (admin approval, or allowed-domains + email verification) under **Admin → Signup & access** / the config's `auth` block. @@ -140,9 +139,9 @@ bdrive stop ./notes --forget --- -## 2. Cloud storage setup +## 2. Storing a hub's data (object storage) -beardrive uses each provider's standard credential chain — nothing beardrive-specific. +This section is about the **hub** (`bdrive web `), which keeps its blobs + journals in an object store you point it at — **clients never touch storage**; they sync through the hub over `https://`. The credentials below live on the hub's machine, not on client devices. beardrive uses each provider's standard credential chain — nothing beardrive-specific. ### Supported URL schemes @@ -153,22 +152,19 @@ beardrive uses each provider's standard credential chain — nothing beardrive-s | `file:///abs/path` | Plain directory (local, NAS, Dropbox folder, …) | `file:///Volumes/nas/beardrive/notes` | | `https://host:port/p/` | One project on a `bdrive web` hub — the client holds **no storage credentials**; the server device owns the bucket config. Server must run with `--upload` for clients to push. Set up with `bdrive init` (never hand-write the `/p/` URL) | `https://drive.example.com:4173/p/p-7f3a2c91` | -`bdrive remote set` validates the scheme and rejects anything else. The prefix can be multi-segment (`s3://bucket/team/agent/workspace`); beardrive writes `blobs/` and `journal/` underneath it. +The `s3://`/`gs://`/`file://` URL is the **hub's** storage root; the prefix can be multi-segment (`s3://bucket/team/agent/workspace`), and beardrive writes `blobs/` and `journal/` underneath it. The `https://…/p/` scheme is what a **client** folder uses, wired automatically by `bdrive init` (never hand-written). -### Setting the remote +### Pointing a hub at storage (server-side) ```sh -# At mount time -bdrive init ./workspace --remote s3://acme-beardrive/workspace +# Start a hub on an S3 bucket; clients then `bdrive login` + `bdrive init` +bdrive web s3://acme-beardrive/root --upload -# After mounting -bdrive remote set ./workspace s3://acme-beardrive/workspace - -# Inspect -bdrive remote ./workspace +# Or from a config file +bdrive web -c config.json # { "remote": "s3://acme-beardrive/root", "upload": true, … } ``` -After `remote set`, run `bdrive sync ./workspace` to push immediately. A running daemon picks up the change on its next interval. +Clients never set storage: they run `bdrive login ` once, then `bdrive init` per folder. ### Amazon S3 (`s3://`) @@ -207,14 +203,14 @@ export AWS_ENDPOINT_URL=https://.r2.cloudflarestorage.com export AWS_REGION=auto export AWS_ACCESS_KEY_ID=... export AWS_SECRET_ACCESS_KEY=... -bdrive init ./workspace --remote s3://my-r2-bucket/workspace +bdrive web s3://my-r2-bucket/workspace --upload # MinIO export AWS_ENDPOINT_URL=http://minio.local:9000 export AWS_REGION=us-east-1 export AWS_ACCESS_KEY_ID=minioadmin export AWS_SECRET_ACCESS_KEY=minioadmin -bdrive init ./workspace --remote s3://beardrive/workspace +bdrive web s3://beardrive/workspace --upload ``` Persist these in the user's shell rc or a systemd/launchd unit so the daemon also has them. @@ -230,7 +226,7 @@ gcloud auth application-default login # Service account export GOOGLE_APPLICATION_CREDENTIALS=/path/to/key.json -bdrive init ./workspace --remote gs://acme-beardrive/workspace +bdrive web gs://acme-beardrive/workspace --upload ``` Service account needs `storage.objects.{get,list,create,delete}` on the bucket (`roles/storage.objectAdmin` bucket-scoped works).