219 Commits
Author SHA1 Message Date
7b863a4684 test(sandbox): a disposable Linux machine to run a scenario in (#92)
Some things cannot be tested from a Go test on your Mac. A real `claude`
session needs the real permission classifier and a $HOME it may write agent
hooks into. The systemd user unit only exists on Linux. A reboot needs
processes to die while the filesystem survives. Until now those were tested by
hand, against the real ~/.bdrive and ~/.claude — so testing onboarding from
scratch meant polluting the machine you were testing from, and `bdrive init`
registering hooks user-level made that worse.

This is an ENVIRONMENT, not a suite. It provides a hub on file:// storage, a
seeded account, browserless sign-in (bdrive-signin drives both halves of the
device flow), Claude Code, the binary under test, and a $HOME thrown away with
the container. Scenarios still live where they belong: deterministic ones in
internal/webapp/cli_e2e_test.go, the conversational one in the onboarding-e2e
skill. The rule, written into the Dockerfile so it survives me: if it doesn't
need a conversation or an OS, it's a Go test.

The two scripts it ships are the scenarios with nowhere else to go.
onboarding.sh runs a real `claude -p` following the LOCAL
INSTALL_FOR_AGENTS.md and checks the scope hard gate, hooks-via-init, and that
nothing reaches for a plugin or skill. daemon-linux.sh covers the systemd unit
and the daemon.pid/stop race.

Notes for whoever reads this next:

  - The binary is bind-mounted, not built in, so a code change rebuilds the
    binary and not the image. BDRIVE_SRC=<checkout> tests a branch without
    touching your working tree; BDRIVE_BIN=<binary> skips the build.
  - No `# syntax=` directive in the Dockerfile on purpose: it makes every
    build resolve the frontend from the registry, which turns a slow network
    into a build that hangs with no output. That also rules out RUN heredocs,
    hence boot.sh being a file.
  - Claude auth comes from CLAUDE_CODE_OAUTH_TOKEN (`claude setup-token`).
    The Keychain is deliberately not read: the container would refresh that
    token and rotate it out from under your Mac, logging you out there.
  - The hub lives only as long as the container's command, so the project
    link init prints is dead once a scripted run exits. Use the interactive
    shell to browse it.


Claude-Session: https://claude.ai/code/session_01DgF8JsoeNPVShGYWdooE72

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
v0.13.0
2026-07-30 14:34:33 +09:00
b927e56fab fix(daemon): let the daemon own its pidfile, so stop can stop it (#91)
* fix(daemon): let the daemon own its pidfile, so stop can stop it

`bdrive stop` could fail with "no such process" and leave sync running.

Liveness became the flock in #88, and Run already announces the child's own
pid only after it holds that lock. But Start still wrote daemon.pid from the
parent, right after fork, with the pid of a child that had not earned
anything yet. Two starts inside that window — `bdrive init` followed by the
login agent's `bdrive resume`, or two resumes close together — race: Running
still reads false, a second child spawns, it loses hold(), and it exits
without ever being the daemon. Its pid is already in the file.

Everything downstream trusts that file. Stop signals the loser and gets
ESRCH, so it reports failure while the winner keeps syncing — the one command
whose job is "stop sending my files" silently does not. status prints the
phantom pid, or "pid 0" when the loser's cleanup removed the file the winner
wrote.

So the parent no longer writes it: the pidfile belongs to whoever holds the
lock. Start now waits for the lock to be taken instead of assuming the spawn
worked, which also means a caller that gets a pid back can trust a daemon
owns it — `bdrive resume` used to print "started (pid N)" for a child that
had already died.

The regression test needs the real binary (Start execs os.Executable), so it
lives with the CLI e2e rather than in internal/daemon, whose tests synthesize
locks. It is deterministic on Linux and roughly one run in five on macOS,
where the window is tighter; `sandbox/run.sh daemon-linux` is the reliable
reproducer.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DgF8JsoeNPVShGYWdooE72

* docs(architecture): the daemon owns its pidfile, Start only waits for the lock

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DgF8JsoeNPVShGYWdooE72

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 14:31:44 +09:00
fd4f5c7964 feat(onboarding): folder name follows the project name, and project ids are UUIDs (#89)
The paste prompt now carries the project's name so an agent recommends a
folder of that name; with no project at all the recommendation is `shared/`
(and `bdrive init shared` names the new project after the folder), replacing
the old `wiki/` default.

New project ids are UUIDs instead of `p-` + 8 hex chars. The route validator
still accepts the legacy shape — ids are permanent — and the client-side URL
parsers (remote/http.go, bdrive share) now only check the shape of a URL
segment, leaving the hub as the single authority on which ids are valid.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 13:42:07 +09:00
fb6ce347c4 feat(daemon): survive a reboot — login autostart on macOS/Linux/Windows, and a lock instead of a pidfile (#88)
* feat(daemon): bring sync back after a reboot, and stop trusting the pidfile

A reboot killed every daemon and nothing restarted them. Agent hooks still
synced per turn, which is what made it easy to miss: a folder looked fine
while an agent worked in it and went stale the moment one didn't. `bdrive
init` now registers a login item (macOS: a user LaunchAgent) that runs the
new `bdrive resume` — one registration per machine, which starts a daemon for
every enrolled, unpaused mount, so adding a project later needs no
re-registration and `bdrive stop` still means stay stopped. `--no-autostart`
opts out, `bdrive autostart install|uninstall` manages it.

Writing the plist is the whole job: no `launchctl` shell-out. launchd loads
agents at login anyway, the caller has just started the daemon for this
session, and shelling out would let a test or a packaging script register a
real login item as a side effect.

The recovery path was also broken, which is why this is one change. Liveness
came from `kill(pid, 0)` on daemon.pid — but that file lives in
$BDRIVE_HOME and survives the reboot that killed its process, so any
same-user process recycling the pid read as a live daemon. `bdrive status`
said "running", and worse `daemon.Start` returned early, so the one
documented recovery (`bdrive init`) reported success and started nothing.
Liveness is now an flock held for the daemon's lifetime: the kernel drops it
at death or reboot, and it makes two daemons on one mount impossible. The pid
stays for display and for signalling.

internal/autostart is darwin-only today; autostart_other.go returns
ErrUnsupported and every caller already treats that as "nothing to do", so
Linux (systemd user unit) and Windows are one file each.

Tests: internal/daemon gets its first ones — a recycled pid must not read as
running (the exact regression), the lock decides liveness, a second holder is
refused. internal/autostart covers write/idempotency/stale-path-rewrite/
uninstall with HOME redirected, and lints the plist with plutil so launchd
can actually parse it. The CLI e2e asserts init registers the agent, that it
runs `resume`, that resume finds the live daemon instead of starting a
second, and that --no-autostart is silent.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016aYntCWwdUhpzUfEk3ddyJ

* feat(autostart): Linux support — a systemd user unit alongside the launchd agent

Same three functions, same discipline. Linux writes
$XDG_CONFIG_HOME/systemd/user/beardrive.service (Type=oneshot, no Restart= —
`bdrive resume` exits by design) plus the default.target.wants symlink that
`systemctl --user enable` would create, because systemd ignores a unit
nothing wants. No `systemctl` shell-out, for the same reasons as launchctl:
the file is the registration, it only matters at the next login, and a
container or ssh session has no session bus to talk to.

Install declines with ErrUnsupported unless systemd is actually the init
system (/run/systemd/system, i.e. sd_booted) — on Alpine, WSL1 or a slim
container a unit file is inert decoration, and reporting "registered" would
be a lie. Installed() likewise requires the enable symlink, not just the
unit: a unit nothing wants never starts.

os.UserConfigDir honors XDG_CONFIG_HOME, so relocated config dirs work.
Windows is now the only gap; autostart_other.go is !darwin && !linux, and the
shared writeIfDifferent/selfPath moved into the tag-free autostart.go (darwin
now uses them too).

Tests run on Linux, not just compiled for it: cross-compiled test binaries
executed in a container, both with /run/systemd/system present (unit written,
enabled, idempotent, stale ExecStart rewritten, broken symlink repaired,
XDG honored, uninstall removes both) and without it (Install declines and
writes nothing). The daemon flock tests were run there too, since flock
semantics are per-OS.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016aYntCWwdUhpzUfEk3ddyJ

* feat(autostart): Windows support — a per-user Run entry

Third platform, same three functions. Windows has no user service manager in
the launchd/systemd sense, so the registration is a HKCU\...\Run value via
golang.org/x/sys/windows/registry (already in the module graph; go mod tidy
just promotes it to direct).

Chosen over the alternatives for the same reason the other two write files:
no admin rights, no COM (a Startup-folder .lnk needs it), no schtasks
shell-out. It is also honestly discoverable — the entry appears in Task
Manager's Startup tab, where someone can disable it without knowing bdrive
exists. The executable is quoted because Explorer parses the value as a
command line and Program Files has a space in it.

Two things a reader should not have to discover for themselves:

- The tests here have NEVER RUN. They are written and compile-checked
  (GOOS=windows go test -c) from macOS; there is no Windows host or usable
  container on an arm64 mac. They execute the first time the suite runs on
  Windows. They also cannot use a temp HOME the way the macOS and Linux tests
  do — HKCU is real — so each one snapshots and restores the previous value.
- `GOOS=windows go build ./...` still does not pass, and this package is not
  why: internal/store's Lock uses syscall.Flock and internal/daemon uses
  syscall.Kill and Setsid, all unix-only (true before this branch too). A
  Windows port means LockFileEx plus a stop story for a platform with no
  SIGTERM — a separate change, against the sync invariants, and untestable
  from here. So this code is correct and currently unreachable.

autostart_other.go is now !darwin && !linux && !windows (the BSDs).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016aYntCWwdUhpzUfEk3ddyJ

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 13:32:13 +09:00
31f705e287 chore: drop the Claude plugin and the bundled skill — hooks are the integration (#85)
Two front doors for the same setup, and one of them was a second copy of
everything. The plugin shipped the skill the CLI already installs (to four
platforms, not one), hooks that ran the identical commands `bdrive hooks
install` writes machine-level, and an install flow duplicating
INSTALL_FOR_AGENTS.md. Nothing deduped, so a machine with both pulled twice
per turn — two blocking syncs — and showed two identical `beardrive` skills
in the picker.

What remains is `internal/agenthooks` plus the runbook: init registers a
blocking pull (which also injects the gated-link convention as
additionalContext), an async push on Write/Edit, and read-log for the
heatmap, in each platform's user config, once per machine. That is the whole
integration, and it is the part that was never optional.

Removed: plugin/, .claude-plugin/marketplace.json, internal/agentskills,
`bdrive skill`, `bdrive hook-approve` (its PreToolUse auto-approve only ever
helped when a plugin pre-installed it; the substitute is a `Bash(bdrive:*)`
permission entry, which is user-owned config and needs no code).

The e2e now asserts the absence: no SKILL.md in any platform's skills dir
after init, and no `skill` subcommand. login_test keeps the "no revoke
surface" wording check on logoutNote alone.


Claude-Session: https://claude.ai/code/session_016aYntCWwdUhpzUfEk3ddyJ

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 12:40:47 +09:00
dfb9da3260 feat(auth): one approval link for device sign-in, and a page that says what it grants (#83)
The headless flow printed a short code to retype into a bare "Approve"
form. Now `bdrive login --device` prints a single link — the token lives in
the path (/auth/device/<token>), so there is nothing to read off one screen
and type into another.

The page it opens is a consent page rather than a text field: it names the
account the device would act as, offers Switch account (logout now honors
?next, so you land back here), and shows the device name, OS, and the
address the server observed. That matters because this flow's weakness is a
stranger sending you their pending link; an anonymous "Approve" gives you
nothing to notice with. Approval is still a POST from the page, so a link
alone cannot grant, and SameSite=Lax keeps a cross-site form out.

Also aligns the /auth/* pages with the app's tokens, which had drifted:
card #0c0e10 vs --color-card #15171b, 8px controls vs --radius-ctl 7px,
hand-picked #ff9b91/#6fd699 vs --color-del/--color-add. The style block now
declares the tw.css tokens by name and every rule uses them.

Older CLIs still print /auth/device?code=…, so that shape 303s to the path
form; a pre-0.13 hub returning no verify_url still gets the old
type-the-code instruction from the CLI.


Claude-Session: https://claude.ai/code/session_016aYntCWwdUhpzUfEk3ddyJ

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 10:41:27 +09:00
2f68bbe92e feat(hub): optional analytics seam so a managed deployment can measure the app (#80)
Adds Server.Analytics (webapp.AnalyticsConfig) and emits it as /api/config
`analytics` when a key is set. The frontend loads posthog-js from the CDN at
runtime rather than as a dependency, so an unconfigured hub ships no tracker
and makes no third-party request — the OSS bundle grows 1.1KB (the loader),
not 230KB.

Product events come from one table in api/http.ts keyed on method+path.
Every mutating call in the app already goes through api()/postJSON(), so a
new write is measured or it isn't, instead of depending on someone
remembering a capture() call. Share creation is the one raw fetch and fires
its own.

Session replay masks every text node: in this product nearly all of it is
customer file names and document bodies. Replays are layout, clicks and
navigation only.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 10:30:02 +09:00
Snow LeeandClaude Opus 5 511b838da0 chore: gitignore .orca/
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 16:20:43 -07:00
4ff92c56ab feat(history): group agent runs and restore any version (BEA-6) (#69)
* feat(history): group agent runs and restore any version (BEA-6)

BearDrive recorded everything and could restore nothing. Now every version
of a file has a Restore button — in the hub's History view and as
`bdrive restore` — and the changes one agent run made read as one card
instead of N loose rows.

Restore is a NEW put op pointing at the old blob: journals are never
rewritten, so one-writer-per-journal holds and peers converge on the
restore like any other edit. The hub reuses RemoteSource.Commit (the
upload commit minus the upload); the CLI writes the bytes into the working
folder and lets the ordinary cycle journal them, so the sync engine gains
no new write path.

Grouping is a pure frontend group-by on (note, device) over the existing
/history response — no journal or API change.

Known gap, stated in the UI and the docs: nothing in the hub writes a
delete op yet, so a file a run *created* cannot be un-created.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(history): don't repeat a run's note on every row in its card

UI pass on the real hub: inside a run card the note is the card's header, so
printing it again on each row said the same thing N times. The header now
carries the note (linkified, so an agent's session link still opens) and the
collapse control is its own button rather than the whole header — the link
could not live inside a button.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
v0.12.0
2026-07-29 17:15:46 +09:00
dcd0517e92 feat(cli): bdrive scope --explain — prove what leaves this machine (BEA-24) (#70)
The local-first claim was asserted, never demonstrated: nothing anywhere
told you what your laptop chose *not* to send. `bdrive scope --explain`
walks the folder and prints two sorted lists — synced and not synced —
with counts and a pointer at what it does not answer.

The decisions come from the same walk the sync cycle uses. scan()'s
WalkDir decision tree moves into walkFolder (internal/syncer/walk.go),
the only copy of the rules; scan and Explain both go through it, so the
output provably cannot drift from real sync behavior.

Pure read: its own Filter, no Session, no volume flock, no network.
Fully-excluded directories collapse to one counted line; nested mounts
are annotated as syncing through their own project rather than called
"not synced", which would be a lie in a trust surface.

Known gap, deliberate: this answers "what leaves from now on", not
"what is already on the hub" — the footer points at `bdrive forget`.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 17:10:51 +09:00
ecc8328512 fix(shares): say when "the latest version" was (BEA-31) (#76)
A /s/<token> page promises the reader it always shows the latest
version, then never says when latest was — so a stranger can't tell
whether a living wiki page is from today or last March.

Print FileInfo.Time, already in hand at the point handleShared renders,
as a muted line above the content: human date, precise RFC3339 in the
title attribute. Zero time prints nothing rather than a 1970 date.

Markdown shells only. The .html/.htm branch stays a raw io.Copy and
binaries stay untouched — the new test asserts byte equality for both.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 17:09:20 +09:00
6da3c7957e fix(hub): deterministic order for public share links (BEA-30) (#75)
ShareDB.List ranged over a map, so the project Settings → Public links
table and the org-wide share audit came back in a different order on
every load — with a Revoke button on each row. Sort in List (Created
desc, then Path, then Token) so both surfaces inherit one total order.

Created.Equal rather than !=: a time.Time carries a monotonic reading
and a location, so two logically-equal instants can compare unequal,
which would make the comparator non-transitive.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 17:07:23 +09:00
acb99e85e3 feat(hub): set an expiry on a share link from the UI (BEA-29) (#74)
The CLI has had --expires all along and the Public links table has always
said "no expiry" — naming an alternative the hub UI never offered. Every
link minted from the browser was permanent.

Expiry is offered AFTER minting, not before: the one-click share stays one
click, and PATCHing the token we just handed out keeps the URL already on
the clipboard valid. Minting with a TTL would instead create a second link,
since ShareDB.Create only reuses permanent ones.

- ShareDB.SetExpiry re-dates a live share in place; repo-write failure
  restores the previous row rather than deleting it (unlike Create's
  rollback, this row already existed — dropping it would revoke a live
  link over a disk hiccup).
- PATCH /api/shares/{token} mirrors handleShareRevoke: resolve the token,
  requirePerm(PermWrite), act. Duration parsing is copied from
  handleShareCreate so the two routes can't drift.
- Share dialog gets an Expires select (Never / 24h / 7d / 30d). A failed
  PATCH toasts and reverts the control. Copy link keeps the dialog's
  initial focus — the new control would otherwise have taken it.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 17:05:06 +09:00
6ae941683e fix(hub): a folder URL with a trailing slash is the same page as without (BEA-28) (#73)
* fix(hub): a folder URL with a trailing slash is the same page as without (BEA-28)

* docs(architecture): Route.trailingSlash in the frontend diagram (BEA-28)

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 17:03:21 +09:00
feacbe3b3a fix(hub): put visible Open/Download controls on every history version (BEA-26) (#72)
A history row was already an address for the version it describes — BEA-7
made clicking one open that version, and the pinned file page has had
"Download this version" since. But the row is a bare `div role="button"`
with no visible affordance, so a reviewer who dumps every <button> and <a>
on the page finds neither, and concludes recovery from a bad edit is
impossible. Undiscoverable is close enough to absent.

Every content-bearing row now carries "Open this version" and "Download",
sitting on the same line as "show changes". No API work — /blob?sha= has
always served the bytes.

`apiBase` becomes HistoryRow's own prop rather than riding inside the
optional `diff`, which is per-file-only: without that the two feeds that
pass no diff (project/subtree, and the folder's Recent changes) could not
build a download URL at all.

Neither control claims aria-expanded — only the note and the diff really
expand anything — and both stop click and keydown from reaching the row so
acting on a version never doubles as navigating it.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 17:01:16 +09:00
4e34d03e14 feat(hooks): user-scope agent sync hooks, one-command setup, --only scoping (#71)
* feat(hooks): register agent sync hooks per machine, not per project

Agent platforms read hook config only from the directory a session starts
in — never a parent, never a subfolder. Project-level hooks therefore fired
only for sessions that happened to start at the mount, and, living inside a
synced folder, they replicated one machine's agent config to the whole team
(a second writer of a file bdrive already owns). Claude Code additionally
ignores project hooks until the folder is trusted, so in practice they were
often inert without any visible sign.

Hooks now go to each platform's user config, once per machine, covering
every session in every folder; the existing shell guard keeps them a no-op
outside BearDrive projects. Install migrates away blocks older versions
wrote into projects, and `bdrive hooks uninstall` removes ours while leaving
foreign hooks untouched.

Setup is also one command now. init absorbs the skill install, prints the
hub link, and takes --server, so connecting to a named hub no longer needs a
separate login; the runbook forbids preflight and command chaining, since
each distinct command costs the user a permission prompt. For plugin users a
PreToolUse hook auto-approves bdrive's own setup subcommands — narrowly: any
shell operator in the command disqualifies it.

Also drops --shared in favor of `init . --only wiki,docs`, which writes a
managed block of .bdriveignore rules instead of a second scope mechanism.
Because those rules sync, `sync --prune` now refuses on a scoped project
rather than stripping everything outside the scope from the hub for everyone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016aYntCWwdUhpzUfEk3ddyJ

* docs: fix stale claims an audit found against the new CLI

An audit of every doc surface against the code turned up claims that the
user-scope hook move and the one-command init made false: project-level
hooks "riding the repo", the Claude trust prompt, Codex's //hooks project
layer, `--no-hooks` skipping the skill (it does not), prune reconciling
against a per-device scope (it now refuses on a scoped project), and
`--scan-interval`/`--remote-interval` documented as init flags when they
only exist on `bdrive daemon run`.

Also documents the surface added today — `--server`, `bdrive hooks
uninstall`, and the plugin's PreToolUse auto-approval — refreshes the two
sample `init` transcripts to the real output, and corrects hook matchers
that had drifted from agenthooks.go.

`bdrive scope` told users to narrow an existing mount with `bdrive init .
--only <dirs>`, which resume then ignored — a dead end. Init now applies
--only on resume, writing the scope block, so the advice works.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016aYntCWwdUhpzUfEk3ddyJ

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 10:08:45 +09:00
Snow LeeandClaude Fable 5 22461a3b4f chore(release): v0.11.0 — changelog, plugin 0.4.0
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016aYntCWwdUhpzUfEk3ddyJ
v0.11.0
2026-07-27 16:09:16 -07:00
9088127176 feat(sync): bdrive forget + sync --prune to take ignored paths off the hub (BEA-20) (#68)
Adding a path to .bdriveignore only stopped future uploads: anything that
synced before the rule existed stayed on the hub forever, with no command
that removed it without deleting it from local disk on every device.

Two engine changes make an explicit removal safe:

- materialize's delete loop now consults the filter. A cached path absent
  from the replayed target that the rules exclude is dropped from tracking
  instead of unlinked — without this, any delete op for a now-filtered path
  wipes every peer's local copy, which is the data loss this issue is about.
- the filter is reloaded mid-cycle from the pulled .bdriveignore, before
  materialize. A peer receiving the new rules and the deletes they justify
  in one batch would otherwise materialize with stale rules and the guard
  would never fire. materialize's write side is split into materializeFile
  so the ignore file can land on its own.

On top of that, Session.Prune journals a delete for every path the replayed
state still holds that the SHARED rules exclude — reconciling against the
replay, not the local cache, because a path filtered out in an earlier cycle
was dropped from the cache back then and is invisible locally today. The
rules are deliberately ignore-only: the include scope lives in each device's
own .bdrive/config.json and does not sync, so pruning against it would let a
narrow-scope device delete a whole-folder teammate's files.

Plain `bdrive sync` and the daemon are unchanged — pruning is never a side
effect of editing .bdriveignore.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 07:22:11 +09:00
411809a785 fix(hub): the history kind is a badge, not a fake disclosure toggle (BEA-17) (#67)
* fix(hub): the history kind is a badge, not a fake disclosure toggle (BEA-17)

The +/x in a history row is the kind glyph (added/edited/deleted), but
sitting leftmost inside a role="button" row it read as a tree disclosure
control — clicked, it navigated away instead of expanding. Merge the glyph
and its word into one text badge and vacate the toggle slot: the kind is
now text (no icon shape, no colour-only meaning), and the row's only real
expander stays the note, which keeps its own control and aria-expanded
and now turns its chevron when open.

* fix(hub): fit DELETED in the kind badge and align the row's meta under the path

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-28 07:21:38 +09:00
f35d889cfb fix(hub): manage a file's public links from the file, not the org panel (BEA-16) (#66)
Minting a public link was one click on the file page; revoking it meant
knowing to go avatar menu → Organization → scroll to "PUBLIC SHARE LINKS".
The action was instant and local, the undo remote and unhinted.

GET /api/p/{project}/shares already existed at PermRead with no frontend
consumer, so this is UI-only:

- A "Publicly shared" banner on the file page whenever the open file has
  live links: the count, each URL, and copy / open / revoke — the same
  words the Share dialog uses. Revoking updates it in place.
- A "Public links" card in project Settings listing that project's live
  links (path, who, when, expiry) with Revoke.
- One SharesTable behind both, plus the org-wide audit, which stays as the
  cross-project view and now links each row back to its file.

The banner shows for anyone with read (a member should know the folder
they rely on is exposed); Revoke only where the Share button already is.

The spec's double-mint bug does not exist: ShareDB.Create already reuses a
live share for (project, path) when neither side has a TTL, and the web UI
never sends expires_in. The real defect was the dialog claiming "Public
link created" on a second click — it now just says "Public link".

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 07:11:32 +09:00
aeee881fa6 fix(hub): say what a read count is made of, and pin the debounce (BEA-15) (#65)
The reported symptom was a read counter inflating while you watched: reopen
one file a few times as one account and it climbs. It does not — the ledger
debounce is correct, and a handler-level repro proves it. What moves is the
displayed *total*, which sums three kinds of reader (human, agent, share)
with independent debounces: your own revisits fold into one visit, but the
syncing agent's reads and share-link hits keep landing in the same number
with nothing to say so.

So the count was right and its framing was wrong. heatText now breaks the
total out by reader kind whenever more than people are reading — the seeded
"14 reads (9 agent)" becomes "15 reads (6 human, 9 agent)" — at the single
chokepoint every heat surface already routes through (file meta line, folder
row meta, folder subtitle, heat-dot tooltip).

No ledger change: the investigation found nothing to fix there. The spec's
prime suspect (a sandboxed-iframe fetch losing the session and recording as
"anonymous") cannot fire in hub mode — authGate 401s an unauthenticated
/api/ request before recordRead runs, and every authenticated identity
carries an account email. TestReadCountsOnePageOpenOnce pins the invariant
so this stays legible: one signed-in person, one file, one 10-minute window
is exactly one read, whichever of /render, /file, /download served it; a
second account adds exactly one; the sandboxed iframe's cookie-less fetch
adds none; history spelunking adds none; and no actor identity reaches the
/heat response. TestReadLedgerDebounce now also covers the other half — once
the window passes, the same actor counts again.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 07:08:21 +09:00
f773b0c6e7 fix(hub): keep folder-row metadata on phones and name the heat dot (BEA-14) (#64)
Below 430px `.dl-meta` was `display:none`, so a folder listing on a phone
showed a filename and an unexplained coloured dot — no read count, no size,
no date. The comment justifying it assumed the dot carried the signal, but
the dot's meaning lived entirely in a `title=` attribute: never shown on
touch, never read by a screen reader on any viewport.

- `.dl-row` wraps and `.dl-meta` takes a full-width second line at ≤430px,
  indented 27px to align under the filename. The name still wins line one
  and is never truncated; the full string fits at 360px, so no shortened
  variant is needed.
- The heat dot gets `role="img"` + `aria-label` on every viewport, which
  also fixes desktop screen-reader users.
- Playwright assertion in layout.spec.ts at 360/390/430: meta matches the
  desktop string, filename untruncated, rows ≥44px, no horizontal scroll,
  every dot has an accessible name.

Desktop (≥431px) is unchanged — measured identical row heights and meta
positions before and after.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-28 07:07:03 +09:00
eba6fe1a64 fix(cli): stop pointing users at a device list that doesn't exist (BEA-13) (#63)
`bdrive logout` and the beardrive skill both told users the device token
"stays valid until it expires — revoke it from the hub's device list".
All three parts are false: there is no device list page or route, device
tokens carry no expiry field, and logout makes no server call at all — it
only rewrites the local settings file.

Both strings now say what is true. The CLI note moves to a package-level
`logoutNote` const so `login_test.go` can assert it, plus SKILL.md's logout
row, mentions neither a device list nor expiry.

Correcting the strings only; the real device list + revoke route touches
the `AuthProvider` seam and is filed separately.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 07:05:19 +09:00
589be3a6de fix(hub): open the project Dashboard to every member, rename /insights → /dashboard (BEA-12) (#62)
* fix(hub): open the project Dashboard to every member, rename /insights → /dashboard (BEA-12)

"Dashboard" was the first sidebar item a new member clicked and it always
refused: it landed on /<project>/insights showing "Insights is for hub
admins and org owners." The gate was client-side only — GET /heat is
gated on project membership and returns counts without actor identities,
so every member's browser could already fetch every number the page draws.

Drops the canInsights gate (nav item, dedicated route, project-home
embed, ⋯ menu entry) and renames the view route insights → dashboard so
the nav label, the URL and the page title finally agree. The shipped
/insights URL still resolves and normalizes to /dashboard (LEGACY_VIEWS
in router.ts) so bookmarks don't 404 and only one URL stays live.

No server change: /heat gating and response shape are untouched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(architecture): router VIEW_ROUTES now names dashboard, with LEGACY_VIEWS

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 07:05:00 +09:00
224b3d6f53 feat(hub): show what changed between file versions (BEA-10) (#61)
Per-file history rows carried identity and a byte size and nothing else —
to answer "what did the agent change?" you had to download two blobs and
diff them by hand. Every non-first version now expands to a line diff
against the previous version of that path, with a +N −M count.

No new endpoint: /blob?sha= already serves both sides and the history
response already names both shas. No new dependency: the LCS is ~40 lines
in src/lib/diff.ts, unit-tested on node's built-in runner (npm test) —
node ≥ 23 strips the types, so the frontend gains no dev dependency.

Blobs are fetched only on expand and cached by sha with an infinite
staleTime (content-addressed, so staleness never applies). Binary is
decided on the bytes, never the extension; either side over 1 MB gets the
too-large fallback, checked against Content-Length before the body is read.
Diffs are per-file only — the subtree feed mixes paths.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-28 07:02:46 +09:00
63612743df fix(webapp): a history row opens the version it describes (BEA-7) (#58)
* fix(webapp): a history row opens the version it describes (BEA-7)

Clicking a row in any history feed called onOpen(e.path) and dropped the
row's blob, so every row opened the CURRENT file — a 7/25 "added" row
rendered content written on 7/26 with nothing on screen saying so. The
backend already served the exact bytes (/blob?sha=); only the UI could
not reach them.

A version is now an address: /<project-id>/<path>?v=<sha>. Routes carry
it (useLocationPath had to snapshot search too, or the URL would change
and nothing would re-render), the file view fetches the pinned blob, and
a banner names the version's time and author, says it is not the current
file, and offers View current + Download this version. /render gains an
optional ?sha= so historical markdown renders as markdown instead of raw
source; history views are still never counted as reads. Delete rows have
no content, so they stay unclickable.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(webapp): keep an old version from borrowing the current file's framing

An unknown ?v= sat on a blank pane through react-query's retry before
saying anything, and the topbar still showed the path's read counts next
to content the banner had just called historical. A pinned version now
fails fast and drops the heat line.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 06:59:41 +09:00
Snow W. Lee (Sungwon)andGitHub 849794a8e5 feat(hub): agent-first onboarding on the no-projects empty state (#60) 2026-07-27 21:11:25 +09:00
Snow W. Lee (Sungwon)andGitHub e629ae4ab0 fix(hub): sort project history by time, not Lamport clock (BEA-9) (#59) 2026-07-27 20:49:22 +09:00
Snow W. Lee (Sungwon)andGitHub 65d4df5a84 One paste-able URL onboards any agent: INSTALL_FOR_AGENTS.md (#57) 2026-07-27 20:23:42 +09:00
Snow Lee 7768f1cf51 Add .env to .gitignore 2026-07-27 02:48:22 -07:00
056c883204 fix(sync): anchor --shared include entries to the mount root (BEA-5) (#56)
`bdrive init --shared wiki` wrote `include: ["wiki/"]`, which compile()
treats as an unanchored gitignore pattern — so any nested directory named
`wiki` synced too. Shared-subfolder mode is what people use to keep private
material out of a project, and it was silently widening the scope: 15 files
under .agents/, .claude/ and .gemini/ leaked into a real project from
.../detector/shared/ dirs.

cleanShared now emits "/wiki/", which fixes both callers (init --shared and
bdrive scope add). config.LoadProject anchors legacy single-segment entries
on read, so the existing mounts are fixed without a re-init — and that also
keeps `bdrive scope rm wiki` working against pre-fix configs, with a
belt-and-braces unanchored candidate key in scopeRemove for any config that
bypasses LoadProject.

Not touched: compile() itself, and no delete op for the already-leaked
remote files (BEA-20 — a delete would unlink teammates' local copies).

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 18:44:05 +09:00
46db6507c4 feat(hub): Billing in the account menu + in-app /billing view (managed hubs) (#55)
* feat(hub): Billing entry with current plan in the account menu (managed hubs)

webapp.Server gains an optional Billing hook — the display mirror of the
Quota seam: managed deployments return (plan, url) per signed-in user and
/api/config exposes it as the 'billing' block; OSS hubs leave it nil and
nothing changes. The frontend renders a Billing item with a plan chip under
the Organization section of the account menu when the block is present.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* architecture: Server gains the Billing display seam

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(hub): Billing is an in-app view at /billing (managed hubs)

The account-menu Billing entry now routes to a real SPA view instead of a
standalone server page: /billing is a top-level route like /orgs, rendered
in the app shell from BillingView, which fetches config.billing.url with
Accept: application/json (plan, usage, seats, plan cards, checkout/portal
form URLs). OSS hubs without a billing block get an honest 'no billing on
this hub' page at that path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 17:55:42 +09:00
2c85747464 feat(sync): .bdriveignore always syncs, regardless of scope or rules (#54)
The ignore file is exempt from filtering in Filter.Skip: it syncs even on
--shared mounts (where it sat outside the include list and was local-only)
and even when one of its own patterns matches it. One guard covers both
scan and materialize since they share the filter. Docs updated on all
three surfaces (SKILL.md, install.md, web/docs).


Claude-Session: https://claude.ai/code/session_01G3AdFdps7seYbW6FhyK58t

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 17:31:53 +09:00
0236e1b272 feat(cli): multi-folder --shared at init + bdrive scope to edit the sync scope (#53)
* feat(cli): init --shared accepts multiple subfolders (repeatable or comma-separated)

--shared is now a slice flag: `--shared wiki --shared docs` or
`--shared wiki,docs` sync several subfolders into one project
(include list ["wiki/", "docs/"]). The interactive scope prompt takes a
space- or comma-separated list. Entries resolving to ".", "", or ".."
error out — silently dropping them would widen scope to the whole folder.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AubcaQscjcQucXwh578vqG

* docs(plugin): skills/commands propose multiple --shared folders at init

The init/install flows now scan for all knowledge folder candidates and
offer them as one --shared list (one project, one permission set), noting
that folders needing different access belong in separate projects.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AubcaQscjcQucXwh578vqG

* feat(cli): bdrive scope — add/remove shared subfolders without editing JSON

`bdrive scope` shows the include list; `scope add`/`scope rm` edit it from
the mount root. The daemon re-reads config each tick, so changes apply in
seconds. rm deletes nothing (newly filtered paths drop from the cache with
no delete op); removing the last entry is refused since an empty include
list means whole-folder sync. add onto a whole-folder project is refused
for the same narrowing hazard. Skill/README/docs updated; scope added to
the cli-sync diagram's command list.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AubcaQscjcQucXwh578vqG

* docs+cli: scoping guide covers multi-folder --shared and bdrive scope; init hints on ignored --shared at resume

The scoping guide (the dedicated page for this feature) now shows
--shared wiki,docs and a "Change the scope later" section for bdrive
scope; setup-by-hand and project-files point at it. init resume with an
explicit --shared now says the flag is ignored instead of staying silent.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AubcaQscjcQucXwh578vqG

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 14:49:58 +09:00
69e7231a70 feat(hub): per-project permissions — none/read/write/admin, invite-only projects, honest degraded sync (#46)
Access was binary and org-wide: any org member got full read+write on every
project. Now each project carries four ordered levels, resolved by one
resolver and enforced at one choke point.

- `projectPerm` (perms.go) replaces `projectAllowed`; `proj(level, h)` in
  server.go gates every per-project route by the level it declares at
  registration, so no handler grows its own check.
- `Project` gains Creator/Default/Perms. `Default == ""` means write, so an
  upgraded hub behaves identically until someone edits permissions.
- Creator becomes the first project admin; org owners are implicitly admin
  everywhere in their org and a grant naming one is refused, not ignored; a
  project always keeps at least one explicit admin.
- Default `none` makes a project invite-only. A `none` member is treated
  exactly like a non-member, including on create-or-join by name.
- Rename/delete move from org-owner-only to project `admin`.
- Both metadata backends persist it: the file store rides along, the SQL
  store gains `project_perms` plus an idempotent ALTER for the two new
  columns (migrate() had only ever created tables).

Client side, a refusal stops looking like an outage: `remote.ErrForbidden`
plus `Result.ReadOnly` (push refused → pull-only) and `Result.NoAccess`
(pull refused → paused, working folder untouched). Neither sets Offline,
neither loses a local op, and re-granting self-heals on the next cycle.
`bdrive status`/`sync` and the daemon (once, on transition) say which.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 10:41:57 +09:00
1463fd8979 fix(hub): show "BearDrive" in the sidebar logo, never the storage name (#52)
The hub's brand fell back to config.volume — the bucket/dir basename —
so a hub on s3://beardrive/... rendered a lowercase "beardrive" logo and
tab title. Drop the fallback at the source (/api/config reports only what
a Brander provider returns) and let each app default on its own: the hub
to the literal "BearDrive", volume mode to the folder name (unchanged).

The e2e harness now seeds Volume: "beardrive" so hub.spec.ts's existing
#vault-name assertion actually catches this.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 10:18:06 +09:00
Snow W. Lee (Sungwon)andGitHub 65491f9576 feat(hub): rename, describe and pick an icon for a project, from its own Settings page (#51)
Project gains two optional fields — Description (<=280 chars) and Icon (a
lucide icon name) — and PATCH /api/projects/{id} becomes a real partial
update: every field is a *string, so only the keys present in the body
change, and {"description":""} clears where an omitted key leaves alone.
Validation returns 400 for an empty or >120-char name, a sibling-name
collision, a >280-char description, and an icon failing ^[a-z0-9-]{1,32}$.
The permission gate is deliberately untouched.

Storage: the file backend marshals Project whole, so it rides along; the SQL
backend needs the two columns added to an already-created table, which
CREATE TABLE IF NOT EXISTS can't do — hence addColumns(), an idempotent
ALTER helper (same shape BEA-2 introduces for creator/default_level, so the
two merge into one map).

Frontend: Settings is now shadcn sectioned cards (General / About / Danger
zone — adds card, separator, textarea to components/ui), with an RHF+zod
form that PATCHes only its dirty keys and refreshes the hub queries, so the
nav mark and dashboard header update without a reload. Icons come from a
curated ~30-icon lucide shortlist (named imports, so Vite still tree-shakes
the rest); an unknown or empty name renders the folder placeholder. The
glyph shows in the project mark on the switcher trigger and every menu row,
and beside the name on the dashboard header with the description under it.
The org admin panel loses its per-project Rename button, which collapses its
two project lists into one read-only list for everybody.

One fix found while driving the real UI: Tailwind preflight is off in this
app, so copied shadcn form controls rendered monospace/black and cards drew
a near-white hairline. Both are now supplied by slot in style.css.
2026-07-27 09:56:59 +09:00
93476700dc docs: PH gallery screenshots — history, insights heatmap, hub UI (#50)
Three 1440×900 screenshots of the seeded e2e hub for the Product Hunt
gallery (BEA-10): per-file history with agent attribution, Knowledge
Insights heatmap + reads×staleness quadrant, and the main hub file view.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 09:56:10 +09:00
8e8b3d7aa8 docs: quickstart→sync demo GIF + MP4 (agent-first, per content-calendar rev 3) (#49)
Recorded for the Wed 2026-07-29 beta (BEA-33): /beardrive:install → agent
writes plan.md → teammate's agent reads it fresh → share link. Outputs
replay the real fresh-machine run verified against v0.10.0; vhs tape +
storyboard committed for reproducible re-renders.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 07:21:51 +09:00
9bac355e5d chore: changelog for v0.10.0 (folds in un-changelogged v0.9.0) (#48)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
v0.10.0
2026-07-27 07:13:24 +09:00
11d667a447 fix(cli): report the real version for go-install builds (#47)
go install github.com/runbear-io/beardrive/cmd/bdrive@latest skips the
release ldflags, so every module-built binary claimed to be 0.1.0-dev —
useless in beta bug reports. Fall back to the module version Go stamps
into the binary (debug.ReadBuildInfo) when ldflags didn't set one.

Found during the BEA-33 fresh-machine quickstart check.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 07:11:58 +09:00
Snow W. Lee (Sungwon)andGitHub 379b19a9ec feat(web): delete a project from its Settings page, behind type-the-name (#45) 2026-07-26 15:57:32 +09:00
6628718f1d fix(cli): agent hooks never sync folders this device didn't opt into (#44)
* fix(cli): agent hooks never sync folders this device didn't opt into

The turn hooks decided "this folder is managed" from the mere presence of
.bdrive/config.json — a file designed to travel with the folder. Two holes:

- A config.json arriving via git clone / copied dir made one hook firing
  silently mint a device identity, register the mount, create a volume
  store, journal the whole folder, and inject the hub-link formula — on a
  device that never ran init or login.
- `bdrive stop` only killed the daemon: the next agent turn's
  `bdrive sync --hook` resumed a full sync cycle and kept injecting links,
  and `stop --forget` was undone within one turn by registry self-heal.

Fix: one gate (`syncBlocked`) in the paths all hooks route through —
sync/sync --hook/read-log now require the mount to already be enrolled in
this device's mounts.json (read without ResolveMount's enrolling
self-heal) and not paused. Hook mode exits silently; plain `bdrive sync`
errors with a `bdrive init` pointer. New per-device paused marker in the
volume dir: set by `bdrive stop`, cleared by `bdrive init` (startSync).
Only init enrolls or resumes; folder moves still self-heal since
enrollment is keyed by mount id, not path.

Docs updated (README, SKILL.md, docs cli reference, CHANGELOG). Tests:
hook/read-log no-op + no-enrollment on unenrolled and paused mounts,
plain-sync refusals, stop→pause→forget regression, paused marker contract.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: architecture-diagram PRs must show before/after excerpts of changed classes

The "Architecture changes" PR section now names exactly what changed and
shows Before and After mermaid excerpts scoped to the affected classes and
their immediate relationships — never the full diagram (Before = merge
base). Convention updated in CLAUDE.md, architecture/README.md, and the
pre-PR hook's reminder text.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: add cli-sync architecture diagram; widen diagram convention to the CLI

architecture/cli-sync.md draws the CLI and sync engine (cmd/bdrive +
internal/{syncer,store,journal,config,daemon,agenthooks}): the Session
cycle over Store/journal/remote, and the command layer with the new
syncBlocked opt-in gate, paused marker, and enrollment ownership. The
pre-PR hook and CLAUDE.md now watch these packages too, so CLI-side
structural changes trigger the before/after-excerpt convention the same
way server changes do.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: full-coverage architecture diagrams — overview, frontend, agentskills

Every application package is now drawn somewhere: overview.md (system
diagram — package map, device↔hub↔storage flow, agent surfaces, and the
private cloud/ repo as an external seam consumer), webapp-frontend.md (the
hub SPA's modules: App/HubApp/VolumeApp/Browser, the in-repo nav/router,
api layer, hooks, components), and agentskills added to cli-sync.md. The
pre-PR hook now watches all of cmd/, internal Go code, and frontend/src
(generated static/ excluded); CLAUDE.md and architecture/README.md state
the coverage rule: every code change lands in exactly one detail diagram's
scope, web/docs and cloud/ deliberately excluded.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: PR bodies start with a TL;DR — max 5 informal one-liners

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 15:53:23 +09:00
4f5f517a6f feat(cli): bdrive export/import — move projects between hubs with full history (#43)
The anti-lock-in story for cloud-hesitant users: export a project's
complete store (every device's journal + every blob, i.e. full history
and authorship) into a portable tar.gz, and import it as a fresh project
on any other hub — cloud → self-hosted or back.

The archive is simply the remote store layout plus a manifest, streamed
through the existing remote.Backend, so no server-side support is needed
and it works against every existing hub. Import verifies each blob's
content hash, rejects foreign tar entries, requires an empty target
project, and refuses journal-less archives. Reconnecting devices resume
exactly where they were, because their journals are byte-identical.

Docs: README + SKILL.md command tables, docs-site CLI reference section,
and a new step-by-step reference page (Migrate between hubs).

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
v0.9.0
2026-07-23 19:04:05 -07:00
071e21cd1a ci: auto-bump beardrive-cloud OSS pin on merge; pre-PR diagram hook (#42)
* tooling: pre-PR hook keeps architecture/ diagrams honest

PreToolUse(Bash) hook blocks gh pr create when internal/webapp or
internal/remote changed but architecture/ didn't; CLAUDE.md documents the
rule and the '# skip-diagram-check' escape hatch for non-structural changes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* ci: bump beardrive-cloud's OSS pin on every merge to main

Each OSS main push commits the new sha to OSS_COMMIT in
runbear-io/beardrive-cloud (CLOUD_BUMP_TOKEN: fine-grained PAT, that repo
only, contents r/w — already set). The bump push runs cloud CI against the
new pin and, only if green, the prod deploy — closing the OSS half of the
CI/CD loop. Rebase-retry loop absorbs racing merges.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 15:24:18 -07:00
220e27a9c2 review fixes: gate init next-steps on background mode, shared stdinIsTTY, daemon reconnects on token change, whoami surfaces settings errors, doc staleness (#41)
- init -f no longer prints 'daemon now keeps this folder in sync' after
  the foreground daemon has exited
- one stdinIsTTY() helper (TTY or Cygwin pty) shared by init's prompt
  gate and login's headless fallback — the two sites disagreed on Cygwin
- the daemon drops its remote backend when the device token changes, so
  an account switch mid-run reconnects with the new credential instead
  of pushing with the old one (httpBackend captures the token at open)
- whoami reports a settings read error instead of claiming 'not signed in'
- self-hosting/authentication and manual/setup-by-hand now describe the
  automatic device-code fallback instead of presenting --device as required

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 15:23:56 -07:00
2b9beaf41a docs: architecture class diagrams + keep-updated-in-PRs workflow (#40)
architecture/ holds mermaid class diagrams of the bdrive web server: the
Server core with its seams (Source, AuthProvider, Directory, QuotaProvider,
remote.Backend) and the MetaStore persistence layer.

Convention (CLAUDE.md): a PR that changes the drawn structure updates the
affected diagram in the same branch and embeds only the changed diagrams'
mermaid blocks in the PR description. A PreToolUse hook on gh pr create
(.claude/hooks/check-arch-diagrams.sh) blocks PR creation when
internal/webapp or internal/remote changed but architecture/ didn't;
override with '# skip-diagram-check' when nothing structural changed.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 15:23:18 -07:00
34afef17cc fix(cli): onboarding friction — headless login fallback, --version, init next steps, labeled authorship (#39)
BEA-7 launch-critical set from the onboarding audit:

- login: shells without a TTY auto-fall back to the device-code flow
  (agents/CI/SSH no longer hang on the browser callback); a failed
  browser open also falls back, and the waiting state hints --device
- bdrive --version now works (cobra root Version), same output as
  bdrive version
- init prints a next-steps block: daemon auto-sync note, the
  'bdrive init --project p-xxx' connect command for teammates,
  bdrive log / bdrive share
- authorship: the daemon re-reads settings.json every tick so a
  login/logout/account switch is reflected in op authorship instead
  of stamping a stale identity forever; whoami now shows the
  signed-in account and labels the git/OS author as the signed-out
  fallback

Docs updated in README, plugin SKILL.md, and web/docs reference/cli.md.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 09:35:48 -07:00
Snow LeeandClaude Fable 5 a00f7cd1a9 cli: hub-era help text; don't print unused projects.json path under SQL metadata
Root --help still described the retired direct-to-bucket model (S3/GCS as
the sync transport); clients sync only through a hub now. And with
database: sqlite/postgres the hub startup line printed a projects.json
path that is never read — misleading for self-hosters checking where
their metadata lives.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 07:52:14 -07:00
Snow LeeandClaude Fable 5 1de5fb1633 build: fix Docker/Cloud Run source deploy ignores
.dockerignore excluded plugin/, which is now a compiled package
(plugin/embed.go embeds SKILL.md). Add .gcloudignore so source uploads
skip node_modules; anchor /bdrive so cmd/bdrive isn't excluded.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UdqEkKvj4Dc2d718mcV6EY
2026-07-21 23:51:20 -07:00