* feat(webapp): history can be narrowed by path, author and date (BEA-67)
The project History view was a flat scroll with no controls but per-row
restore/open/download. Fine at eleven rows; unreadable after a month of
agent writes, and agents write far more than people do.
Four reader filters on GET /api/p/<id>/history — q= (case-insensitive
substring of the path), user= (exact account), since=/until= (UTC bounds,
inclusive at both ends, RFC3339 or a bare YYYY-MM-DD). They compose with
each other and with the existing path=/prefix= scoping, and they are
applied in the same walk as path/prefix — BEFORE the sort and the cursor
skip — so next_cursor keeps meaning "the next matching entry" and paging
under a filter needed no new machinery. kinds[] is still computed over
every op, so a filtered view classifies edits the same as the full feed.
A malformed since/until is a 400, not a silently unfiltered feed.
The filter bar drives those params through the URL rather than component
state: a narrowed feed is a link you can send, it survives reload, and
Back undoes it. The author list accumulates across fetches — rebuilding
it from the current feed would strand a reader who filtered by one
author with only their own name to pick from. Zero matches gets its own
empty state with a Clear.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* docs(architecture): the frontend diagram parses again
The escaped quotes inside the NewProjectDialog note made the whole
classDiagram fail to parse — it has been rendering as an error box, not a
diagram, since that note landed. Same breakage on origin/main; noticed
while adding HistoryFilters to the same file.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat: ask for a GitHub star where it can't annoy anyone
Three surfaces, all passive or once-per-setup:
- `bdrive init` prints one line after a successful setup, TTY-only. init
runs about once per project per machine, and the guard keeps it out of
CI logs and any output a script parses — putting a star plea in
repeating output is what got postinstall ads banned from npm.
- The hub sidebar gets a dim "Star on GitHub" link above the account row.
A link that always sits there reads as social proof; a dismissible
banner would need dismissal state and would still have interrupted.
- README grows star/pkg.go.dev/docs badges — until now the only
user-facing link to the repo was the docs sidebar.
Covered by two checks: the CLI e2e asserts init stays silent about the
repo when stdout is piped, and a Playwright spec pins the sidebar link's
href/target and that nothing modal appears with it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FNgeJVcsXQ5sTfWLgCR4cv
* fix(webapp): the star link wears the GitHub mark, at 11px
A star glyph next to "Star on GitHub" said the same word twice; the mark
is what people scan for. lucide dropped its brand icons in v1, so the
path is inline rather than a second icon dependency for one glyph — and
it is filled, so it sits outside the `.ico` stroke sizing.
Text drops 12px → 11px to match the account email below it: this is the
dimmest thing in the sidebar on purpose.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FNgeJVcsXQ5sTfWLgCR4cv
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat(templates): start a project from a structure, not an empty folder
A new project was an empty folder with a .bdriveignore in it, so every agent
session invented its own layout and the folder rotted into a pile. Both
surfaces now offer the same three starting points — from a template, from
scratch, from an existing folder (which is just a non-empty folder, and is
never restructured).
internal/templates holds the shipped set as literal go:embed'ed files: `docs`
(docs/, decisions/) and `para` (projects/, areas/, resources/, archives/).
cmd/bdrive is one binary for the CLI and the hub, so both read the identical
set — no gallery, no drift. The AGENTS.md in each is the deliverable: where a
new note goes, when something is archived, what a good filename looks like.
Every directory holds a real file, because BearDrive syncs paths and an empty
directory would never reach a teammate.
The hub seeds at creation through the existing Upload+Commit path, journaled
under its own device, and records the choice on the project record — so a user
who picked PARA in a browser sees PARA in the browser, and a later init cannot
seed a second copy. `bdrive init --template <name>` goes through the same
endpoint, with a local-seed fallback for a hub too old to know the field, and
seeds in place when re-run in an already-initialized folder (the agent's
post-init path). Seeding never overwrites an existing path, which is what makes
a double-seed a no-op rather than a divergence.
Refusals cost nothing: an unknown name and --template with --only are both
rejected before any network call or write.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* test(cli): joining a project that already has a template is refused by name
The one acceptance case with no test behind it: connecting to an existing
project with --template must say what the project was actually created from,
and must not write the other skeleton on the way out.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(templates): name the docs template in plain English, not an acronym
"Plain docs + ADRs" was the recommended, first, preselected-adjacent option in
a picker that non-engineers see — and it's the label people accept without
reading further, so half of it not parsing is the worst place for jargon. The
title also disagreed with its own blurb: "ADRs" over "docs/, decisions/", two
words for the same folder one line apart.
Now "Docs + decision records", which says the same thing to everyone and
matches the folder names. The term itself moves into
decisions/0001-record-decisions.md, where the reader is already inside the
structure and the file can teach it in passing.
One line in the registry drives both the web dialog and the CLI menu; the rest
is prose echoing it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat(templates): add the LLM wiki template
The third starting point from the issue title, unblocked: the spec parked it
because shipping an approximation under someone's name needed a source, and
there is now one — Karpathy's LLM Wiki gist. Worth noting the issue's own
one-line description of it ("few large, append-heavy topic pages") does not
match the source, which is the opposite: many interlinked pages, where a single
ingest touches 10-15 of them.
The pattern is three layers and three operations, not a folder shape. sources/
is yours and immutable; wiki/ is the agent's and it owns every page; AGENTS.md
is the schema layer — which is exactly the file this template system already
treats as the deliverable, so the fit is direct. index.md and log.md ship as
the two navigation files the pattern turns on.
Three of the things the gist tells you to go set up, BearDrive already is:
version history and collaboration (per-file history, bdrive log), an Obsidian-
style reader for [[wikilinks]] (the hub viewer), and a surface for the lint
pass (the dashboard is literally reads x staleness).
Two rules in the AGENTS.md are load-bearing and deliberate. A page write that
has not updated the index is an incomplete write — a stale index is worse than
a missing page, because it is read first and believed. And with no sources yet,
build nothing: the structure grows out of the material rather than ahead of it.
Shipped second, not first: docs stays the recommendation because a default is
the option chosen by people not reading closely, and this pattern degrades
badly when half-followed. Promoting it later is one line in the registry.
The shipped-template test now checks the "what happens when something stops
being true" question through a set of alternatives — PARA archives, a wiki
supersedes and revises — since the vocabulary honestly differs by structure.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat(web): "I already have a folder" as a starting point
The third way to start from the spec — adopt what you already have — had no
presence in the browser. Templates and "empty" were the only visible answers,
so someone with a folder of notes either hesitated or picked a template and got
four directories merged into their material.
The constraint that shapes it: the browser cannot reach your disk, so this
cannot change what is created. It creates the same empty project "Empty
project" does; what it changes is the next screen. Create therefore stays
enabled — disabling it would leave the dialog a dead end AND produce no project
id, which is the one thing the paste prompt actually needs.
Landing on the project home with the intent, three things differ: the guide
says "in the folder you already have", a note states plainly that connecting
never moves, renames or overwrites anything, and the paste prompt tells the
agent a folder already exists. That last one is the part that isn't cosmetic —
without it an agent reads an empty project and proposes creating shared/, the
one recommendation that is wrong here. It still asks which folder: that is the
runbook's hard gate and nothing here weakens it.
The intent rides in the URL (?connect=existing) rather than onto the project
record, the same way ?v= pins a file version. It belongs to whoever is
connecting right now — a teammate who connects next week has their own answer
and would be told the wrong thing by a persisted flag.
Five rows made the dialog tall enough to push Create off a short viewport, so
.modal scrolls internally. A hairline divider between the seeding and
non-seeding rows was tried and removed: --border is 7% white, which at 1px in a
gap renders as literally nothing. The gap is the cue that reads.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat(web): with no projects, open the create dialog and give the page a way in
A signed-in account with no projects landed on a page whose only path forward
was pasting a prompt into a coding agent. Now the create dialog opens itself —
with nothing to browse there is nothing else on that page to do — and the page
behind it leads with "Start a project" and a button, so closing the dialog is
not a dead end.
The dialog moves up to HubApp because three things ask for it now: the
sidebar's +, the empty state's button, and the auto-open. ProjectNav keeps only
an onNew callback; one owner beats three copies of the create handler.
Two guards on the auto-open. It fires once per mount, keyed off a ref rather
than the empty state, or closing it would immediately reopen it. And it never
fires on a read-only hub, which refuses creation server-side with a 403 —
opening a dialog that cannot succeed is worse than the page it covers.
The agent paste-prompt stays, demoted to "Or let your agent do it": it is still
the right path for someone who wants the folder connected in the same breath,
and it is the only path on a hub where this account cannot create.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(history): stop reporting the device IP on every change (BEA-43)
The history API embedded the whole DeviceInfo, so every project member
read a teammate's server-observed IP (plus user/last_seen) next to every
change on a page whose job is "who changed this file". Project a
three-field historyDevice instead — id/name/os — mirroring heatByDevice.
The device registry is unchanged: Observe/requestIP and both MetaStore
backends keep recording the IP. This is a response projection, not a
change in what gets collected.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* docs: history shows device name and OS, not the IP (BEA-43)
README, SKILL.md and the docs site all promised the History view would
show the connecting IP. It no longer does — the registry still records it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
History could restore an edit or a deletion, but a file a run CREATED was
the one thing it couldn't reverse — the ADDED row said so in copy and
offered no button. The missing capability was a hub-written delete op:
restore.go only ever journaled puts.
POST /api/p/<id>/remove journals exactly one journal.KindDelete op under
the hub's own device identity, behind restore's gates (gateUpload,
PermWrite, cleanUploadPath, quota CheckWrite/RecordUsage) plus a volume-
snapshot existence check so the API 404s on what the tree doesn't show.
Commit's journal-append tail moves into RemoteSource.appendOp, which both
writes now share — one writer per journal, unchanged.
The ADDED-in-a-run row gets an "undo — remove file" control that confirms
first (it reaches every synced device), and the DELETED row it leaves
behind restores the file with its original bytes.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
`bdrive log` sorted by the lamport clock, so the wall-clock stamps it prints
came out non-monotonic — two 06:09:24 rows above a 06:10:00 row. And every op
of one scan is stamped with that scan's commit time, so a 22-file agent run
collapsed onto a single stamp. Neither is readable as a timeline, which is the
whole job of the command.
Two display-only changes:
- `journal.Op` gains `Mtime` (`omitzero`, so old journals and old binaries are
unaffected), populated on put ops from the `os.FileInfo` the scan already
holds. Deletes and conflict copies keep their commit time.
- `syncer.DisplayTime` / `SortForDisplay` order by the timestamp that is
actually printed, ties broken by reversed `journal.Less`. `bdrive log` sorts
and *then* truncates, so `-n 25` is the 25 newest by that stamp.
`journal.Less`, `Sort`, and `Replay` are untouched — replay order is the
convergence contract, so the sort lives in `syncer`, not in `journal`.
`LogEntries` also keeps returning causal order because `bdrive restore` walks
it to find a file's previous version.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat(auth): ask before signing a terminal in as whoever the browser is
`bdrive login` opened /auth/cli and the browser bounced straight back with a
code. Whoever the browser happened to be signed in as is who the terminal
became — silently. That is frequently not the account the user meant: a
personal login left open in the default browser, a teammate's session on a
shared machine. The mistake surfaces much later, as a synced folder full of
commits authored by the wrong person, which is far more work to undo than one
click would have been.
The device flow already got this right in #83 — it names the account, offers
to switch, and says what approving grants. The browser flow said nothing at
all, for the same outcome: a token that acts as you.
So /auth/cli now confirms first. GET renders the page (who you would be
signing in as, a Switch account link that comes back to this same pending
sign-in, what is asking, and where it is waiting); POST is what mints the
code and redirects to the loopback listener. A GET therefore grants nothing,
so a link someone else got you to open can no longer mint a code on your
behalf.
whoBlock loses its pendingGrant parameter and renders only the identity half.
What is asking differs per flow — a device has a name and an OS, a CLI on this
computer has a loopback port — so each page now renders its own rows through a
small helper instead of whoBlock pretending to a shape neither quite fits.
The CLI's own wording follows: "waiting for you to approve the sign-in in your
browser", since being signed in already is no longer the whole story.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DgF8JsoeNPVShGYWdooE72
* docs: the browser sign-in confirms first, and says whose account it grants
README, the CLI reference, and the self-hosting auth page all described the
old behaviour — sign in and the page bounces a code straight to the terminal.
They also read as though only `--device` had an approval step. Both flows now
confirm; say so, and say why it matters (the browser session is often not the
account the user meant).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DgF8JsoeNPVShGYWdooE72
* feat(auth): one web step for a first sign-in, not two
The confirmation page fixed the wrong-account problem and created a smaller
one: a user with no browser session now saw two pages on their first
`bdrive init` — sign in, then approve — where the sign-in had already settled
the only question the second page asks.
So authenticating *for* a pending CLI sign-in now counts as approving it. The
login and signup pages carry a line saying a terminal is waiting and that the
account used here is the one it will act as, which is where that consent is
made informed; reaching the callback then needs no second click.
The marker is server-side, bound to the exact pending sign-in, single use, and
two minutes long, so it can only ever skip the page it was granted for and only
once. It cannot be forged: setting it requires authenticating as that account,
and anyone who could do that could click Approve anyway.
An existing session still gets the page — that is the case where the browser
may be signed in as someone the user did not intend, which is the whole reason
it exists. Net effect: exactly one web interaction either way.
The device flow keeps its explicit approval. Its page names a machine that
isn't this one, along with the OS and address it came from — information no
login form can convey, about a grant to somewhere else.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DgF8JsoeNPVShGYWdooE72
* fix(auth): keep `bdrive login` on one line in the approval hint
It wrapped mid-phrase into two separate code boxes, which reads as two
commands rather than one.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DgF8JsoeNPVShGYWdooE72
* refactor(auth): one approval page for both sign-in flows
The two flows ask the same question — shall this thing act as you? — and had
two copies of the page asking it, differing in three strings. They had already
started drifting: a wrapping fix went into the CLI copy only, leaving the
device page able to break `bdrive login --device` across two code boxes. A
page whose whole purpose is consistent disclosure is a bad place to keep two
of everything.
So pageAuth owns the shape (session check, redirect to login, whoBlock, rows,
the Approve form, the note) and each flow supplies an authRequest describing
what differs: how the request is identified, what is asking, and what
approving does.
Two asymmetries are now explicit rather than accidental. freshAuthSkips is
true only for the local flow — signing in and approving are the same act when
the terminal is on this machine, and are not when the token goes to another
one. live() reports whether the request still exists, because the device
flow's link expires while the CLI flow carries its whole request in the URL
and has nothing to expire.
detail is a function, not a slice: the device rows come off the pending grant,
which only exists after live() has found it.
No test changed. The pages render byte-identically — same sha256 for all three
CLI screenshots before and after — and the device flow was driven end to end
against a real hub, approving a real `bdrive login --device`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DgF8JsoeNPVShGYWdooE72
* feat(auth): both sign-in flows always ask you to approve
Consistency between the two flows is worth more than the click it saves.
Letting a sign-in count as its own approval made the local flow one step and
the device flow two, so the same product asked for consent in two different
shapes depending on which machine you were on — and the shape that skipped it
was the one where the page had something to tell you.
So the fresh-auth marker is gone: sign in, then approve, on both flows. That
drops a map, two methods, a descriptor field, and a branch in pageAuth — the
unified handler now has exactly one path through it.
A first `bdrive init` on a fresh machine is two web pages again. That is the
deliberate trade: the approval page is where a user sees which account a
machine is about to act as, and nothing shortcuts it.
The sign-in page keeps the line saying a terminal is waiting. It no longer
carries the consent — the next page does — so it is there to explain why a
password prompt appeared at all.
TestBothFlowsAlwaysAskToApprove replaces the one-step test and runs the same
assertions over both flows as subtests: no session sends you to sign in
carrying the request, signing in returns to the request without granting, the
approval page is there every time, and only the POST grants.
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>
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>
* 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>
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>
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>
* 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>
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>
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>
* 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>
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>
* 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>
`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>
* 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>
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>
* 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>
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>
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>
The hub's install guide told Codex and Hermes users to run four CLI
commands by hand, and the one people skipped — `bdrive hooks install` —
is exactly the one that makes files sync at turn boundaries. Hand the
setup to the agent instead, the way the Claude tab hands it to the
plugin.
- `bdrive skill install` (internal/agentskills, plugin/embed.go): the
binary now carries the beardrive skill and writes it to any agent that
reads SKILL.md — ~/.{claude,codex,gemini,hermes}/skills/beardrive/.
User-level on purpose: the skill is about the CLI, not one folder, and
a synced project folder should never carry it. Idempotent; refreshes a
stale copy after a CLI upgrade. Bare `bdrive skill` prints the table,
mirroring `bdrive hooks`.
- Guide's Codex/Hermes tabs are now a single paste, no terminal: the
prompt has the agent install the CLI, keep the skill, sign in, init,
and register hooks. The commands ride inside the prompt because these
agents ship no BearDrive knowledge (Claude's tab is terse only because
the plugin carries it). `login --device` there — a browser-callback
sign-in is invisible to an agent mid-turn, while the device flow gives
it a code and URL to relay. Plain commands live on in an "or run it
yourself" fallback.
- Docs realigned: README, SKILL.md, /beardrive:install, self-hosting.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Members and shares-audit render through @tanstack/react-table (sortable,
spec-first); org rename, hub signup policy, and onboarding create/join are
react-hook-form + zod with inline errors replacing toast-on-typo. 55/55.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VbiaaVM2ACxeRi8ySG9ybc
Design feedback applied:
- icons are lucide-react (lucide.dev) behind the existing <Icon name> API;
the inline SVG sprite is gone
- org bar (sidebar footer): the Manage text button is a gear icon opening
the org admin panel; sign-out moved here, after the gear
- header: the org-manage (people) button is removed; the header gear now
opens a new per-PROJECT settings panel (identity facts + connect guide);
sign-out stays in the header only when no project/org bar is present
(volume mode, loading, empty state)
Playwright suite updated and green (44/44); static/ rebuilt.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VbiaaVM2ACxeRi8ySG9ybc
- login copy across README/skill/commands: bare `bdrive login` (BearDrive
Cloud) is the default; signup auto-creates a free personal workspace,
pending invites route into the team; self-hosted hubs pass a URL
- init flows propose a detected knowledge folder (wiki/docs/notes/handbook/
vault) as --shared instead of asking open-endedly; repo-root rule restated
- install always ends by handing the user a `bdrive url` link to a
representative page — the activation moment
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VbiaaVM2ACxeRi8ySG9ybc
Zero repo-fixable blockers/majors; all four round-4 surfaces verified
fixed. The two optional minors applied: the README command-table login
cell and SKILL's init cell now carry the waitlist-only caveat inline.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P5cxPQdSGJnjXCYY9GeWXt
Reviewer round 4 (9/6/9/9/7/9) caught that round 3 only fixed the
Quick start: the bare-login default survived in the four highest-
traffic surfaces. All inverted to self-host-first:
- README HERO code block (+ the other-machine one-liner and the
every-other-device shorthand): bdrive login https://your-hub, with
Cloud as a clearly-labeled waitlist callout
- plugin/commands/install.md + init.md step 2: ask for the team's hub
URL; bare 'bdrive login' explicitly flagged as targeting the
not-yet-open Cloud
- SKILL.md walkthrough step 1 + the hub-example comment
- website hero terminal animation: 'syncing → your team's hub', share
URL on a neutral acme-hub.example host (og:url and the coming-soon
card untouched — those are honest)
- README's login reference sentence now says the beardrive.ai default
is waitlist-only
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P5cxPQdSGJnjXCYY9GeWXt
Reviewer round 3 (9/7/9/8/7/9) caught the sharpest truth bug yet: the
README quickstart's bare 'bdrive login' defaults to beardrive.ai, which
is waitlist-only and not accepting logins — a launch-day visitor's
first command would fail silently. Fixes:
- quickstart step 1 now routes to self-hosting ('bdrive login
https://your-hub', ~10-minute guide linked) with Cloud honestly
framed as waitlist; website closer promises 'Self-host in one
binary' instead of 'under a minute, done'
- handoff gains a P0 gate: verify the FULL first run (login → init →
edit → log) against a working hub before any launch; bare 'bdrive
login' must not be advertised until Cloud accepts logins
- handoff: seed 3-5 good-first-issues (CONTRIBUTING promises them);
replace the mailto waitlist with a real form before Product Hunt
- README trimmed 583→524 lines: the Authentication and Choosing-a-
database operator reference moved verbatim into docs/self-hosting.md
with a compact pointer left behind
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P5cxPQdSGJnjXCYY9GeWXt
Reviewer round 2 scored 8/7/9/8/8/8 with one repo-fixable major left:
three deeper website spots (storage strip, architecture card, bring-
your-own-storage card) still implied a hub-less direct-to-bucket mode
that v0.3.0 removed — all rewritten to the hub-on-your-storage reality;
themes-preview.html's five retracted 'no server required' strings fixed.
Also: install-path verification added to the handoff P0 and the launch
prep checklist (a 404 brew tap on launch day is fatal); README
credentials table now says the s3/gs/file rows are hub-operator storage,
never client remotes; the agent promise now leads on both README and
website (people-sharing supports it); share.png re-captured against a
real rendered runbook (headings/table/code) instead of a one-line stub.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P5cxPQdSGJnjXCYY9GeWXt
GTM reviewer round 1 (baseline: positioning 6, funnel 4, open-paid 8,
community 3, launch 3, metrics 2; 3 blockers + 3 majors, all repo-fixable):
- TRUTH: removed the false 'no server required' claim (x3 on the
website) and stale 'S3/GCS directly' sync claims in both plugin
manifests — hub-only since v0.3.0
- POSITIONING: one line everywhere ('the open-source Google Drive for
AI agents' + the attribution/read-analytics wedge) across README,
website, marketplace.json, plugin.json, SKILL.md; third 'Why' card
and AGPL-why rationale; nav CTA → Star on GitHub
- COMMUNITY: CONTRIBUTING.md, issue forms + PR template, public dated
ROADMAP.md, CHANGELOG.md seeded from all 8 releases
- DEMO ASSETS: real product screenshots captured from the seeded demo
hub (Insights treemap, browse-with-heat, public share page) embedded
in README + website — no fabricated content anywhere
- DOCS: self-hosting quickstart, metrics plan (north star: active
shared brains), launch-plan drafts (Show HN/PH/launch week), and
docs/gtm-handoff.md — the prioritized external-action checklist
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P5cxPQdSGJnjXCYY9GeWXt
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
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
- #root { display: contents } so body's flex layout sees through the
React mount point (main pane was stuck at content width)
- README: Web frontend development section; CLAUDE.md: commands +
webapp description rewritten for the React/Vite reality (committed
dist, check-dist pre-release step, the no-router-library and
no-post-commit-DOM-patching lessons)
- verified: clean git-archive checkout builds with no Node and serves
the React app; visual parity pass across desktop + mobile surfaces;
42 e2e specs green; plugin docs untouched (no frontend internals)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P5cxPQdSGJnjXCYY9GeWXt
Replace the old "append conventions to CLAUDE.md" step with a two-file
pattern for teaching agents a newly mounted shared folder:
- <shared>/AGENTS.md (synced): the team's single source of truth for the
folder's structure and conventions — scaffolded once by the project
creator, read (never rewritten) by joiners.
- A repo-root AGENTS.md/CLAUDE.md pointer (per machine, not synced): the
awareness/routing layer. Required because discovery differs by
platform — Claude Code and Hermes load nested instruction files lazily
(only after entering the folder), and Codex never discovers them at
all (root→cwd path only).
SKILL.md gains a "Teaching agents the folder" section with the platform
discovery table and a first-contact orientation ritual; /beardrive:install
step 4 and /beardrive:init step 6 now offer both files as separate
consents; install.md step 3 gains the same never-sync-a-repo-root hard
rule init.md already had; README/CLAUDE.md descriptions updated.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P5cxPQdSGJnjXCYY9GeWXt
Admin/org-owner Insights view (⋯ menu): dependency-free SVG scatter of
every file by 30-day reads × days since last change, log scales, with the
hot-but-stale danger quadrant shaded and a ranked fix-these-first list;
lens toggle for all/human/agent reads. The Claude Code plugin gains a
PostToolUse(Read) hook so plugin users feed agent-read telemetry without
project-level hook registration. Docs synced: README, SKILL.md,
plugin install/init commands, CLAUDE.md, design doc marked implemented.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P5cxPQdSGJnjXCYY9GeWXt
`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
A subdirectory with its own .bdrive/config.json now syncs only through
its own project: the parent mount never scans into it, materializes
over it, or emits deletes for paths under it (cached paths are dropped
without delete ops, the same posture as newly ignored files). Detection
is presence-based (config.IsMount) so even a corrupt nested config
still fences. This makes the team-knowledge-folder-inside-a-personal-
mount topology safe.
The plugin's init command and skill gain a "connecting knowledge
tooling" playbook: a detection ladder (gbrain -> OKF wiki -> plain
docs folder -> starting-point menu) and two hard rules — one transport
per folder (git-tracked dirs are handed off via git rm --cached +
gitignore before syncing) and knowledge-as-scoped-subfolder (never a
repo root). Conflict copies documented with their exact glob
(*.bdrive-conflict-*), since openknowledge validate cannot see them.
New flow tests: shared-subfolder scope both ways, teammate connect
over identical/divergent local content, nested mount syncing
independently on two projects, and a guard pinning the conflict-copy
filename to the documented glob.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P5cxPQdSGJnjXCYY9GeWXt
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
- 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
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
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
Replace the frontend's hash-based routing with the History API and real
`/` paths, so URLs read like native file paths:
/#p-4e61c7d4/shared%2Fidea.md -> /p-4e61c7d4/shared/idea.md
/#shared%2Fnotes.md -> /shared/notes.md (volume mode)
/#join/<token> -> /join/<token>
Client (app.js):
- parseRoute() reads location.pathname; pushURL/syncURL push native paths
(segments percent-encoded, "/" kept literal); a popstate handler restores
back/forward. Invites read from /join/<token>.
- All api/ fetches and the app.js/style.css refs are now root-absolute so a
deep path doesn't break relative URL resolution.
Server (server.go):
- New Server.frontend handler: real assets serve directly; every other
non-API/auth/share GET returns index.html (SPA fallback), so deep links
and refreshes resolve instead of 404ing. Reserved prefixes stay 404s.
- Invite links minted as /join/<token> (orgs.go).
Tests: TestFrontendSPAFallback covers the fallback + reserved-prefix 404s;
existing invite test updated. Verified end to end in the running hub
(deep-link reload, back/forward, no %2F/# in the bar). Docs updated.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7Q9ZKSZRTdvrSJkYLUmYs
Self-contained static page (website/index.html, no build step, no
dependencies): Supabase-style structure — centered hero with announcement
pill and gradient payoff line, animated terminal demo, storage strip,
bento feature cards, closing CTA — on a honey-amber (#f5a623) accent over
near-black. Copy is built around the two value props: share files with
people (public URLs) and share context across AI agents ("your agent
knows what their agent knows"), with a Claude Code demo acting out both.
Cloud is waitlist-only (mailto; no backend yet), no pricing shown.
themes-preview.html keeps the accent-color comparison used to pick honey.
README headline and intro updated to the same positioning; GitHub repo
description updated to match.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7Q9ZKSZRTdvrSJkYLUmYs