20 Commits
Author SHA1 Message Date
Snow LeeandClaude Fable 5 5ff82c2c49 feat(cli): bdrive url — internal, permission-walled links agents share when they create files
New command printing a file's hub viewer URL: sign-in + project-org
membership required to open (the internal counterpart to bdrive share's
public links). Computed locally from the mount config — hub origin +
project id from the remote, path segments percent-encoded with literal
slashes; unsynced paths (ignored, or outside a --shared scope) are
refused so nobody gets handed a 404. --sync pushes first so a
just-created file's link resolves immediately; no arg = project home.

The plugin docs now instruct agents to include this link in their reply
whenever they create a shareable artifact (.md/.html/.csv/...) in the
shared folder, reserving bdrive share for people outside the hub:
SKILL.md command map + 'Share what you make' guidance, install.md root
pointer template + payoff step, README, CLAUDE.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P5cxPQdSGJnjXCYY9GeWXt
2026-07-14 11:29:54 -07:00
Snow LeeandClaude Fable 5 476113ebdc fix(hooks): read heat captures grep matches and shell-command reads, not just Read
Field report: an agent session read a dozen-plus files via Bash
(grep/cat/tail/find) and Grep, but read heat showed almost nothing —
the read-log hook only matched the native Read tool.

- matchers broadened per platform: claude Read|Grep|Bash, codex
  read_file|shell, gemini +search_file_content|run_shell_command,
  hermes read_file|grep|bash; plugin hooks.json matches Read|Grep|Bash
- read-log is now tool-aware: shell events mine the command line for
  existing files it names (redirect targets and flags excluded), grep
  events mine the response for the files the matches came from
  (content lines and filename lists), and listing tools (Glob, ls) are
  deliberately ignored — seeing a file's name is not reading it
- hooks install upgrades a registered hook's stale matcher in place, so
  re-running it after a binary upgrade rolls coverage out to existing
  projects instead of being skipped by the idempotency marker
- docs: SKILL.md platform table + read-heat wording, install/init
  commands, README command table

Note from the same report, verified not a bug: read-log resolves the
mount via the folder's own .bdrive/config.json (config.ResolveMount),
so a stale duplicate registry entry cannot swallow reads.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P5cxPQdSGJnjXCYY9GeWXt
2026-07-13 15:05:05 -07:00
Snow LeeandClaude Fable 5 2ce714ac4d feat(sync): agent read reporting — read-log, spool, hub report, hook matchers
Phase 3 of the read heatmap: `bdrive read-log` parses any platform's hook
event JSON from stdin and queues in-project file reads (mount-relative,
ignore/include-filtered) in a per-volume spool — no network on the hook
path. The sync cycle drains the spool best-effort to the hub's new
POST /api/p/<id>/reads via the remote ReadReporter capability (https
backend only); a failed report retries next cycle and never fails or
offlines the cycle. `bdrive hooks install` now registers a third hook per
platform on its read-tool matcher (claude Read, codex read_file
best-effort, gemini read_file|read_many_files, hermes read_file), each
idempotent on its own marker so sync-only configs upgrade in place. Agent
reads land as agent traffic in the heat view, actor = device id.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P5cxPQdSGJnjXCYY9GeWXt
2026-07-11 14:49:02 -07:00
Snow LeeandClaude Fable 5 28dc882c66 feat(web): read heatmap phase 1 — ledger, heat API, folder heat dots
Read telemetry per docs/design/read-heatmap.md: a ReadLedger over a new
batch-oriented MetaStore ReadRepo (file reads.json + SQL read_stats)
aggregates viewer and share reads into daily per-actor buckets, debounced
to visits, folded into all-time rows past retention. GET /api/p/<id>/heat
serves per-path counts (human/agent/share, distinct readers, last read) —
never identities. /store sync traffic and history blob views are not reads.
The viewer shows heat dots and read counts on folder listings and the file
meta line.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P5cxPQdSGJnjXCYY9GeWXt
2026-07-11 14:41:07 -07:00
Snow LeeandClaude Fable 5 626a9c0a07 feat(cli): bdrive hooks — agent-agnostic sync hook registration
`bdrive hooks install` detects the agent platforms in use — Claude Code
(.claude/), Codex (.codex/), Gemini CLI (.gemini/), Hermes (~/.hermes/) —
and idempotently merges beardrive's turn-boundary sync hooks into each
platform's own hook config (JSON for claude/codex/gemini, YAML for
hermes), preserving existing hooks. All four pipe hook JSON with a
session_id, so one POSIX-sh hook command serves every platform: pull at
turn start, push after edits, changes stamped "<agent> session <id>".
Bare `bdrive hooks` prints the detection/registration table.

The beardrive skill now runs it automatically after `bdrive init`, and
/beardrive:install's hand-maintained settings.json block is replaced by
the command, so the hook content has one source of truth in the binary.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P5cxPQdSGJnjXCYY9GeWXt
2026-07-11 10:57:07 -07:00
Snow LeeandClaude Fable 5 c1f0d0f8ee feat(sync): session-linked notes — stamp changes with the agent session
`bdrive sync --note <text>` stamps session context onto every op the cycle
commits and persists it in the volume store (note.json, --note-ttl default
30m) so the daemon's own scans stamp it too — winning the race between
one-shot hook syncs and the 3s daemon scan. The plugin sync hook extracts
session_id from hook stdin JSON and passes it automatically, so history
links every change to the Claude Code session that made it. Conflict-copy
ops keep their own note; expired/cleared notes stop applying.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P5cxPQdSGJnjXCYY9GeWXt
2026-07-11 10:56:53 -07:00
Snow LeeandClaude Opus 4.8 64fa269e52 perf(sync): parallel blob upload + progress bar for initial import
The initial import was latency-bound: the push loop uploaded blobs strictly
serially, doing an Exists round-trip then a Put per blob (~2-3N sequential
round-trips against the hub).

- Upload unique blobs in parallel (errgroup, 16 in flight) — the main win.
- Drop the separate Exists round-trip: the backend's Put is idempotent and
  the hub already skips content it has (reported during signing), so the
  check was redundant.
- Session.OnProgress emits upload progress (done/total files + bytes) from
  the push phase; the CLI renders an in-place bar on a TTY and periodic
  percentage lines otherwise, wired into `bdrive init`'s initial cycle and
  `bdrive sync`. The daemon stays silent.

Multi-device convergence tests pass with -race; new TestPushProgress covers
the progress emission.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7Q9ZKSZRTdvrSJkYLUmYs
2026-07-10 10:11:54 -07:00
Snow LeeandClaude Opus 4.8 b06d0d6a32 feat(cli)!: hub-only — remove remote command; reposition docs
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 <url>` 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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7Q9ZKSZRTdvrSJkYLUmYs
2026-07-10 09:55:58 -07:00
Snow LeeandClaude Opus 4.8 074cc4bfa8 feat(cli): add bdrive logout; clarify remote set host kinds
- New `bdrive logout` clears the saved token + account (folders untouched);
  `--forget` also drops the remembered server. Switching hubs is
  `bdrive login <new-url>` then re-`init`.
- `remote set --help` now explains the two remote kinds — object storage
  (s3/gs/file) vs a bdrive hub (https://<server>) — 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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7Q9ZKSZRTdvrSJkYLUmYs
2026-07-10 09:01:11 -07:00
Snow LeeandClaude Opus 4.8 62175e29a9 feat(web): database config (file/sqlite/postgres) + docs (phase 3)
Wire a `database` block in bdrive web config: {driver: file|sqlite|postgres,
dsn}. Default file, unchanged (existing per-registry paths honored). sqlite
or postgres routes all five hub registries through the SQL MetaStore.

Verified: signup + project + invite round-trip survives a restart on BOTH
file and sqlite; the sqlite DB holds real relational tables/rows (not JSON
blobs). Docs: a "choosing a database" section (README), the persistence
architecture note (CLAUDE.md), and a config mention (SKILL.md).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7Q9ZKSZRTdvrSJkYLUmYs
2026-07-10 06:54:49 -07:00
Snow LeeandClaude Opus 4.8 bd7ea16200 feat(web): invite-only signup by default; harden signup postures
A hub on a public URL could be signed up to with any fake email. Make the
safe posture the default and turn the dangerous ones into startup errors.

Three supported postures:
- invite-only (NEW default): allow_signup defaults false. A valid org
  invite link bootstraps an account even when self-signup is closed — the
  only way in. pageLogin/pageSignup detect a /join/<token> target via
  BuiltinAuth.InviteValid (wired to OrgDB.ValidInvite) and offer account
  creation; signupInvited skips the domain/approval/verification gates and
  activates immediately (the owner's invite is the vetting).
- approval-gated: allow_signup + require_approval (no SMTP needed).
- domain-restricted + verified: allow_signup + allowed_domains +
  require_verification (needs smtp).

Hardening:
- BuiltinAuth.ValidateSignupPolicy (run at hub startup in web.go) refuses to
  boot an open hub with no gate, or require_verification without a mailer
  (the link would otherwise only reach the server log).
- handleAdminPolicy rejects enabling verification without SMTP; the UI
  toggle is disabled with clearer copy in that case.

Tests: TestValidateSignupPolicy, TestInviteBootstrapsAccountWhenSignupClosed,
TestPolicyVerificationNeedsMailer. Verified end to end in the running hub
(outside-domain invitee onboards via a link; direct signup stays closed).
Docs updated (README, CLAUDE.md, SKILL.md).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7Q9ZKSZRTdvrSJkYLUmYs
2026-07-09 16:57:49 -07:00
Snow LeeandClaude Fable 5 40fcda7cf7 polish(hub): round-3 — mobile actions reachable, admin gating settings UI
Fixes the two round-2 majors.

User: on mobile the per-file actions (Share/History/Upload/Download) are
now icon buttons in the header — reachable again (round 2 wrongly hid
them). Search tolerates simple English plurals (ideas→idea), the
no-matches state states what search covers, the sidebar shows the hub
brand instead of the raw device name, and history-row downloads carry a
download attribute. Logged-out loads redirect to sign-in from /api/config
instead of firing 401-ing API calls.

Admin: a hub-admin "Signup & access" settings screen (⚙ Admin in the
sidebar) toggles email verification and admin approval live — persisted to
auth.json and surviving restart — while the domain allowlist and admin
list are shown read-only (deliberately server-config-owned so a browser
session can't widen access). Pending approvals live on the same screen.
Config toggles are now *bool so an explicit config value pins the setting
each boot, else the UI-saved policy stands. Invite revoke confirms; role
change re-renders the panel.

Tests: policy persistence + reload, policy API admin-only.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7Q9ZKSZRTdvrSJkYLUmYs
2026-07-08 22:55:14 -07:00
Snow LeeandClaude Fable 5 baad9e6dc2 feat(hub): signup gating, admin lifecycle, and onboarding UX
Makes a self-hosted hub safe to expose on a public URL and operable
without hand-editing JSON — addressing the blocker/major findings from
the persona usability evaluations.

Signup gating (config auth block, all optional):
- allowed_domains: signup email must match (e.g. only @runbear.io)
- require_verification: email-link activation before sign-in (reuses mailer)
- require_approval: hub admins approve new accounts (admins list)
- brand shown on the sign-in page; allow_signup:false already hid Sign up
Accounts carry a Status (active/unverified/pending); non-active accounts
cannot authenticate.

Admin lifecycle (endpoints + web UI):
- org: rename, member role change, member remove (last-owner guarded),
  invite list + revoke
- project: create (web), rename, delete (from the org panel)
- hub admins: approve/deny pending signups (sidebar bell + panel)
- org-wide public-share audit with revoke

UX: onboarding empty-state (explains invites, paste-invite + create-project)
instead of a blank sidebar; visible "Search ⌘K" button; toasts replace
blocking alert(); responsive layout with an off-canvas sidebar; joining
via #join now survives a logged-out click (token carried through login).
Web uploads are attributed to the signed-in account, not the server.
Login/signup are rate-limited per IP.

Tests: domain/verification/approval gates, auth rate limit, org+project
lifecycle, owner-only guards, invite→join→role→remove over HTTP.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7Q9ZKSZRTdvrSJkYLUmYs
2026-07-08 22:23:27 -07:00
Snow LeeandClaude Fable 5 6ef1470088 feat(web): share-page footer + per-IP rate limit on /s/*
Rendered-markdown share pages get a small "Shared with BearDrive" footer
linking to the repo — the shell template only, so shared raw HTML is
still served byte-for-byte and the /s/ sandbox CSP is unchanged. All
/s/* responses now sit behind a per-IP token bucket (default 120 req/min
sustained + burst, tunable via "share_rpm" in the web config) so the one
unauthenticated surface of a hub can't be scraped into a free CDN.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7Q9ZKSZRTdvrSJkYLUmYs
2026-07-08 17:07:54 -07:00
Snow LeeandClaude Fable 5 94ef66a4a9 feat(hub): organizations — projects walled by team membership, with invites
Every hub project now belongs to exactly one org (file-backed orgs.json,
same load-at-open + atomic-rewrite discipline as the other registries).
Membership (email -> owner|member) gates every per-project route: the
viewer APIs, uploads, history, blobs, shares management, and the device
sync store proxy; /api/projects lists only your orgs' projects, and
project names are now scoped per org. Public share links (/s/) stay
public by design.

Design choices, per the simplest-consistent rule:
- Migration: a pre-org hub sweeps all org-less projects into a "default"
  org at startup; ALL existing accounts join it (they could all see every
  project before, so anything narrower would lock someone out), oldest
  account as owner. Zero manual steps.
- An account in no org that creates a project gets a fresh org named
  after itself, so nobody is ever blocked from starting to sync.
- Invites are expiring multi-use links (default 7 days): an owner mints
  /#join/<token>, any signed-in account that opens it joins as member.
  The web UI shows the org in a sidebar footer (members on click,
  Invite button for owners).

bdrive init needed no changes: its connect-existing flow lists projects
through the now-filtered API.

Tests: OrgDB + migration units; a 403/access matrix over every
per-project route; invite mint/redeem flow; and a multi-device syncer
test proving a device holding an org-B token can neither pull org A's
files nor push into its store (sync degrades to Offline, never partial).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7Q9ZKSZRTdvrSJkYLUmYs
2026-07-08 17:03:48 -07:00
Snow LeeandClaude Fable 5 c0e785a4ba feat: public share links and /beardrive:install for Claude Code teams
Share links (bdrive share <file>, or the web UI's Share button):
- Mint an unguessable public URL (/s/<token>) anyone can open — no
  account. HTML renders as a page, markdown gets a standalone shell,
  PDFs open inline; ?download=1 attaches.
- Sandboxed: /s/* responses carry CSP `sandbox allow-scripts` + nosniff
  and never see auth cookies, so shared content's scripts run in an
  opaque origin and can't touch hub sessions.
- Links serve the file's LATEST synced content and live until revoked;
  --expires makes self-destructing ones; --list/--revoke manage them.
  Re-sharing a file returns the same link. File-backed shares.json.
- CLI resolves the project by walking up to .bdrive/ from the shared
  file, warns when the hub address is private (LAN-only links), and
  hints when the file hasn't synced yet.

/beardrive:install (plugin command) — team onboarding driven by Claude:
- Ensures the bdrive binary, signs in (bdrive login), runs bdrive init
  (whole folder or a shared subfolder like wiki/).
- Asks before appending a CLAUDE.md section that teaches agents to put
  shareable artifacts in the shared folder and mint URLs with bdrive
  share; asks before registering project-level hooks in
  .claude/settings.json: blocking pull at UserPromptSubmit, async push
  on PostToolUse Write|Edit — teammates sync with or without the plugin.
- Fix: the plugin hook script still checked for the old `.bdrive` file
  and was a silent no-op since the directory change; now checks -d.

Tests: share creation gating, public access + sandbox headers, dedupe,
latest-content semantics, revoke, expiry, markdown/download variants,
list filtering, registry persistence. Docs updated (README sharing +
Claude Code sections, SKILL.md, CLAUDE.md).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7Q9ZKSZRTdvrSJkYLUmYs
2026-07-08 15:02:13 -07:00
Snow LeeandClaude Fable 5 2d7f2f8bfa feat: auth, move-proof projects, interactive init/login, web history
Authentication (previous phase, now landed together with its follow-ups):
- Email+password+name accounts behind an AuthProvider interface; the OSS
  server ships BuiltinAuth only (file-backed auth.json: bcrypt password
  hashes + SHA-256 token digests, plaintext never stored; server-owned
  /auth/* pages; managed deployments can swap in another provider).
- bdrive login: loopback-callback browser flow (sign-up on the page, the
  terminal finishes itself) with a device-code fallback for headless
  machines; long-lived revocable device tokens in settings.json.
- Password reset via plain SMTP (stdlib) with a log-link fallback when no
  SMTP is configured.

Move-proof projects:
- .bdrive is now a directory; config.json carries a stable mount id.
  The volume store (~/.bdrive/volumes/<mount-id>/) and registry are keyed
  by that id — never the folder path — so renames/moves are free.
- The daemon re-reads the project config each tick and exits cleanly
  (propagating nothing) when its folder vanishes; the registry self-heals
  and the next bdrive command at the new location resumes with zero
  spurious changes.

bdrive init is the front door (mnt/umnt removed; bdrive stop pauses):
- Interactive on a TTY (create new / connect existing project from the
  server's list; whole folder / shared subfolder via the include list),
  full flag bypass (--name/--project/--shared/--yes), never prompts
  without a TTY. Runs the login flow first when there is no session.
  Default server: beardrive.ai (config.DefaultServer).

Web history (revert-ready):
- Hubs now always require auth; journal ops carry the signed-in account
  (user/user_name) alongside the git/OS fallback author.
- File-backed device registry: per-device name, OS, account, and the
  public IP the server observed, joined into history at read time.
- GET /api/p/<id>/history?path=|prefix= (newest first) and
  GET /api/p/<id>/blob?sha= stream any exact version — blobs are retained
  forever, so the next phase's revert is re-putting an old blob.
- UI: History button (file versions or project feed), per-folder history
  shortcut, view/download of any past version.

Tests: auth flows (callback, device-code, reset single-use, persistence,
gating), history API + device registry, folder-move survival, registry
self-heal, ops-carry-account; docs (README/SKILL/CLAUDE) updated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7Q9ZKSZRTdvrSJkYLUmYs
2026-07-08 13:12:49 -07:00
Snow LeeandClaude Fable 5 a7bb790615 feat: multi-project sync hub, bdrive login/init onboarding, .bdrive rename
The web server (bdrive web) becomes a full sync hub, and client devices
get one-command onboarding — without ever seeing storage info or holding
cloud credentials:

- bdrive web -c config.json: server configurable from a JSON file
  (remote/addr/upload/upload_ttl/projects_db); explicit flags win.
- Hub mode: pointing bdrive web at a storage root hosts many projects,
  each under <root>/<project-id>/ (remote.Prefixed). Projects live in a
  file-backed registry (projects.json — loaded at open, rewritten
  atomically per change) with create-or-join-by-name semantics.
- Per-project APIs: /api/projects (list/create/get) and
  /api/p/<id>/{tree,file,render,download,upload/*,store/*}. The web UI
  grows a project list with per-project browsing and hash deep links.
- Browser uploads and a store proxy for syncing devices: presigned
  direct-to-storage PUTs when the backend can sign (S3 presign, GCS V4
  signed URLs; expiring, credential-free), relayed through the server
  otherwise. Journals are never presigned — only immutable blobs.
  Blobs-before-journal and one-writer-per-journal invariants hold.
- https:// remote backend: a device syncs one hub project through
  /api/p/<id>/store/* — mnt/sync/daemon/log all work unchanged.
- bdrive login <url>: verify a hub and remember it as the device default
  (settings.json). bdrive init: create-or-join a project named after the
  folder (--name/--project override), write .bdrive, seed a starter
  .bdriveignore, mount, and start the daemon — one command per project.
- Hard-break rename: .beardrive->.bdrive, .beardriveignore->.bdriveignore,
  ~/.beardrive->~/.bdrive, BEARDRIVE_HOME->BDRIVE_HOME, temp/conflict
  prefixes; old names are no longer read.
- Tests: presigning, project registry persistence, store API validation
  and gating, project isolation over live HTTP, browser upload flows, and
  two-device convergence through a hub (incl. read-only pull-only mode).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7Q9ZKSZRTdvrSJkYLUmYs
2026-07-08 07:13:00 -07:00
Snow LeeandClaude Fable 5 fafb7fe24d feat: fold the web viewer into the CLI as bdrive web
One binary instead of two: cmd/bdrive-web becomes the `web` subcommand
(same flags and positional folder-or-URL argument). Measured cost of
carrying the webapp in the CLI: +1.4 MB on a ~56 MB binary (~2.4%) —
the cloud SDKs dominate either way. Drops the second goreleaser build
and the separate go install path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HHEUaYfFHhmDvqLYw74Ehz
2026-07-07 16:06:26 -07:00
Snow LeeandClaude Fable 5 1ac128db2c rename: sfs -> BearDrive everywhere; CLI becomes bdrive
Product and project are BearDrive; the CLI binary is bdrive (bdrive mnt,
bdrive sync, ...), the web viewer is bdrive-web. All conventions follow
the full name: .beardrive settings file, .beardriveignore, ~/.beardrive
home, BEARDRIVE_HOME, .beardrive-conflict-* / .beardrive-tmp-* files.
Plugin/skill/marketplace renamed to beardrive (/beardrive:mount).

Module path is now github.com/runbear-io/beardrive — merge only after
renaming the GitHub repo, or go install breaks.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HHEUaYfFHhmDvqLYw74Ehz
2026-07-07 15:02:40 -07:00