mirror of
https://github.com/runbear-io/beardrive.git
synced 2026-08-25 08:08:08 +02:00
v0.14.0
53
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
9f13c70b78 |
feat: ask for a GitHub star where it can't annoy anyone (#108)
* 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> |
||
|
|
a84fe444f5 |
fix(hooks): every mount gets its own hub link, so agents stop mixing projects up (#105)
A session whose root holds two connected folders got exactly one project's
base URL in its turn context: `bdrive sync --hook` emitted for the first
mount and stopped. Agents then hung the other project's paths on that URL,
producing links to project B carrying project A's path — confidently wrong,
and 404 on arrival.
The context now carries every mount as a `prefix → URL` pair, where the
prefix is the mount's path as the agent sees it from the session's folder.
A session started INSIDE a mount has no prefix to strip, so its own subpath
is baked into the base URL instead — the other half of the same bug, which
made every link from a subdirectory session miss its leading segments.
The formula also now states outright that the URL path is folder-relative
and that a path matching no listed folder is not synced and must not be
linked at all. Encoding is per-segment, `/` left literal (encodePathSegments).
Two more fell out along the way:
- `runHookSync` returns nil on every path by design ("a hook must never
fail the turn"), so the caller's `err == nil` guard could never be false.
A first mount that emitted nothing — non-hub remote, session open error —
still consumed the one emission, and NO mount got a link. It now reports
whether it produced a URL.
- stdin was read once per mount, so with several mounts only the first got
its session note stamped. The event JSON is read once per run now.
Single-mount output is unchanged apart from the folder-relative sentence.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
1c703c95f8 |
feat(templates): start a project from a structure, not an empty folder (#97)
* 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> |
||
|
|
4dfe6f44f0 |
feat(history): undo a file an agent run created (BEA-35) (#82)
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> |
||
|
|
872ba702cb |
fix(log): bdrive log reads as a timeline — newest first by edit time (#BEA-40) (#90)
`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> |
||
|
|
b1c0bba415 |
feat(auth): the browser sign-in says whose account the terminal gets (#96)
* 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> |
||
|
|
118abc67e2 | feat(cli): "bdrive serve" replaces "bdrive web" (web stays as an alias), and the README leads with the agent install (#94) | ||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
dfef5720df |
webapp: organizations behind a Directory seam
The hub already abstracted authentication — AuthProvider, with BuiltinAuth as the built-in implementation — and then reached around that seam three times: Accounts() was declared on neither interface, admin.go type-asserted *BuiltinAuth (five handlers silently degraded to 404/empty under any other provider), and organizations were not on the seam at all. That last gap had teeth. A deployment whose identities come from elsewhere had no way to own its orgs, so the code that did own them wrote into the hub's OrgDB from the side — and nothing stopped the hub from inventing an org that the identity system had never heard of. One did: a hub-created org held every project while the mirrored one sat empty, and no sync path could see the difference. Directory (directory.go) is where organizations live now. LocalDirectory wraps today's OrgDB unchanged — same last-owner protection, same normEmail, same "o-"+randHex(4) ids, same file/SQL persistence — so a self-hosted hub behaves exactly as before. A deployment whose orgs are owned elsewhere implements the same interface, returns ErrManagedElsewhere from the write half, and the handlers answer 409 with ManageURL. The hub never learns why a write was refused, only where to send the user. Two rules shape the interface. Reads are on the request path: Role runs on every project request, including the /store/* endpoints a device hits every few seconds with a token that carries no identity claims, so an implementation backed by a remote system answers from its own cache — and that cache is its business, not the hub's. Writes are optional, because "this hub owns its orgs" is a deployment fact, not a code path. - Server.Orgs *OrgDB becomes Server.Dir Directory: 28 call sites, 8 nil-checks, one writeDirErr helper for the 409 translation. - /api/orgs gains manage_url per org — the destination of the account menu's Settings entry. The client follows a link and never branches on which kind of hub it is talking to. - Org administration becomes a real route, /orgs/<id>, retiring one of the two URL-less panels CLAUDE.md grandfathers. When a directory's ManageURL is not hub-local, the SPA fallback redirects there instead — so a hub that cannot administer its orgs cannot paint a console whose every control 409s. - Accounts() moves onto AuthProvider. admin.go's type assertion becomes an optional AccountApprover, and a provider without one now answers 503 rather than an empty approval queue: "no queue here" and "queue is empty" are different answers and only one of them was true. Two reviews drove the rest. The architecture review caught a browser page load that could delete org members (a display read ran the full membership reconcile, and a 200 with an empty user list evicted everyone), one write site that escaped the 409 translation, and a webhook that could wedge an event stream behind an unappliable event. The design review, over eight rounds, caught the org page rendering live controls on a hub that cannot use them, a share link made unrevokable by a long filename, nine keyboard tab stops parked off-screen behind a closed drawer, and — five separate times — a fix of mine that looked right in the source and did nothing in the browser. Conformance tests run both a writable and a read-only implementation against one contract; the seat, prune, and out-of-order regressions each have a test written to fail against the old code. |
||
|
|
5c6099177b |
feat: bdrive skill install + one-paste Codex/Hermes setup guide
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>
|
||
|
|
a16580b300 |
fix(cli): log and status show the signed-in account, not the git author fallback
bdrive log preferred Op.Author (git/OS identity) even when the op carried the hub account (Op.User/UserName), so team history showed local git emails. Found by the agent-onboarding live e2e: device signed in as e2e-bot showed snow@runbear.io in log. Now: UserName → User → Author. bdrive status likewise shows 'signed in as Name <email>' when a session exists. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VbiaaVM2ACxeRi8ySG9ybc |
||
|
|
644caff70e |
feat(hooks): every mentioned file path gets a gated hub link — formula injected each turn
Field report follow-up: an agent with a stale skill copy couldn't find the gated URL after creating a file. Instructions rot; hook output is computed fresh from the binary every turn. Claude Code's turn-start pull hook now runs 'bdrive sync --hook claude-code', which: - pulls as before, and stamps the session note from the event JSON (replacing the sh/sed pipeline for the pull leg) - emits the project's gated-link formula as UserPromptSubmit additionalContext: whenever the agent mentions a synced file path in prose, it appends the hub link on an emoji — `<path>` [🔗](<url>) — path plain (it's the local path), hyperlink on the emoji only; code blocks stay plain; bdrive share stays explicit-opt-in-public Blind-tested: an agent given only the injected context decorated every path mention correctly, kept the code-block command plain, and checked files were synced before linking. - hooks install now CONVERGES marker-identified groups to the current shape (command/matcher/flags), so improvements reach existing projects on reinstall instead of being frozen by the idempotency marker; hermes same - plugin: UserPromptSubmit → beardrive-pull.sh (stdout passes through); version 0.3.0 - SKILL 'Share what you make' generalized to 'Link what you mention' (URL formula documented for non-Claude platforms); install.md pointer template updated Never fails the turn: every error path in --hook mode is a silent successful exit; offline still emits (links serve online teammates). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P5cxPQdSGJnjXCYY9GeWXt |
||
|
|
5ff82c2c49 |
feat(cli): bdrive url — internal, permission-walled links agents share when they create files
New command printing a file's hub viewer URL: sign-in + project-org membership required to open (the internal counterpart to bdrive share's public links). Computed locally from the mount config — hub origin + project id from the remote, path segments percent-encoded with literal slashes; unsynced paths (ignored, or outside a --shared scope) are refused so nobody gets handed a 404. --sync pushes first so a just-created file's link resolves immediately; no arg = project home. The plugin docs now instruct agents to include this link in their reply whenever they create a shareable artifact (.md/.html/.csv/...) in the shared folder, reserving bdrive share for people outside the hub: SKILL.md command map + 'Share what you make' guidance, install.md root pointer template + payoff step, README, CLAUDE.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P5cxPQdSGJnjXCYY9GeWXt |
||
|
|
476113ebdc |
fix(hooks): read heat captures grep matches and shell-command reads, not just Read
Field report: an agent session read a dozen-plus files via Bash (grep/cat/tail/find) and Grep, but read heat showed almost nothing — the read-log hook only matched the native Read tool. - matchers broadened per platform: claude Read|Grep|Bash, codex read_file|shell, gemini +search_file_content|run_shell_command, hermes read_file|grep|bash; plugin hooks.json matches Read|Grep|Bash - read-log is now tool-aware: shell events mine the command line for existing files it names (redirect targets and flags excluded), grep events mine the response for the files the matches came from (content lines and filename lists), and listing tools (Glob, ls) are deliberately ignored — seeing a file's name is not reading it - hooks install upgrades a registered hook's stale matcher in place, so re-running it after a binary upgrade rolls coverage out to existing projects instead of being skipped by the idempotency marker - docs: SKILL.md platform table + read-heat wording, install/init commands, README command table Note from the same report, verified not a bug: read-log resolves the mount via the folder's own .bdrive/config.json (config.ResolveMount), so a stale duplicate registry entry cannot swallow reads. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P5cxPQdSGJnjXCYY9GeWXt |
||
|
|
2ce714ac4d |
feat(sync): agent read reporting — read-log, spool, hub report, hook matchers
Phase 3 of the read heatmap: `bdrive read-log` parses any platform's hook event JSON from stdin and queues in-project file reads (mount-relative, ignore/include-filtered) in a per-volume spool — no network on the hook path. The sync cycle drains the spool best-effort to the hub's new POST /api/p/<id>/reads via the remote ReadReporter capability (https backend only); a failed report retries next cycle and never fails or offlines the cycle. `bdrive hooks install` now registers a third hook per platform on its read-tool matcher (claude Read, codex read_file best-effort, gemini read_file|read_many_files, hermes read_file), each idempotent on its own marker so sync-only configs upgrade in place. Agent reads land as agent traffic in the heat view, actor = device id. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P5cxPQdSGJnjXCYY9GeWXt |
||
|
|
28dc882c66 |
feat(web): read heatmap phase 1 — ledger, heat API, folder heat dots
Read telemetry per docs/design/read-heatmap.md: a ReadLedger over a new batch-oriented MetaStore ReadRepo (file reads.json + SQL read_stats) aggregates viewer and share reads into daily per-actor buckets, debounced to visits, folded into all-time rows past retention. GET /api/p/<id>/heat serves per-path counts (human/agent/share, distinct readers, last read) — never identities. /store sync traffic and history blob views are not reads. The viewer shows heat dots and read counts on folder listings and the file meta line. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P5cxPQdSGJnjXCYY9GeWXt |
||
|
|
626a9c0a07 |
feat(cli): bdrive hooks — agent-agnostic sync hook registration
`bdrive hooks install` detects the agent platforms in use — Claude Code (.claude/), Codex (.codex/), Gemini CLI (.gemini/), Hermes (~/.hermes/) — and idempotently merges beardrive's turn-boundary sync hooks into each platform's own hook config (JSON for claude/codex/gemini, YAML for hermes), preserving existing hooks. All four pipe hook JSON with a session_id, so one POSIX-sh hook command serves every platform: pull at turn start, push after edits, changes stamped "<agent> session <id>". Bare `bdrive hooks` prints the detection/registration table. The beardrive skill now runs it automatically after `bdrive init`, and /beardrive:install's hand-maintained settings.json block is replaced by the command, so the hook content has one source of truth in the binary. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P5cxPQdSGJnjXCYY9GeWXt |
||
|
|
c1f0d0f8ee |
feat(sync): session-linked notes — stamp changes with the agent session
`bdrive sync --note <text>` stamps session context onto every op the cycle commits and persists it in the volume store (note.json, --note-ttl default 30m) so the daemon's own scans stamp it too — winning the race between one-shot hook syncs and the 3s daemon scan. The plugin sync hook extracts session_id from hook stdin JSON and passes it automatically, so history links every change to the Claude Code session that made it. Conflict-copy ops keep their own note; expired/cleared notes stop applying. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P5cxPQdSGJnjXCYY9GeWXt |
||
|
|
64fa269e52 |
perf(sync): parallel blob upload + progress bar for initial import
The initial import was latency-bound: the push loop uploaded blobs strictly serially, doing an Exists round-trip then a Put per blob (~2-3N sequential round-trips against the hub). - Upload unique blobs in parallel (errgroup, 16 in flight) — the main win. - Drop the separate Exists round-trip: the backend's Put is idempotent and the hub already skips content it has (reported during signing), so the check was redundant. - Session.OnProgress emits upload progress (done/total files + bytes) from the push phase; the CLI renders an in-place bar on a TTY and periodic percentage lines otherwise, wired into `bdrive init`'s initial cycle and `bdrive sync`. The daemon stays silent. Multi-device convergence tests pass with -race; new TestPushProgress covers the progress emission. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R7Q9ZKSZRTdvrSJkYLUmYs |
||
|
|
b06d0d6a32 |
feat(cli)!: hub-only — remove remote command; reposition docs
BearDrive is now hub-based only. Clients sync through a `bdrive web` hub over https and never talk to object storage directly. - Remove the `bdrive remote` / `bdrive remote set` command entirely. Wiring a folder's remote is `bdrive init` (always a hub); switching hubs is `bdrive login <url>` then re-`init`. Object storage stays as the *hub's* backing store (`bdrive web s3://…`), never a client remote. - Reposition README, CLAUDE.md, and the beardrive skill from "no server required / direct-to-bucket" to hub-based; section 2 of the skill is now "storing a hub's data" (server-side), and the stale `init --remote` / `remote set` examples are fixed. BREAKING: direct client-to-bucket sync is no longer supported. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R7Q9ZKSZRTdvrSJkYLUmYs |
||
|
|
074cc4bfa8 |
feat(cli): add bdrive logout; clarify remote set host kinds
- New `bdrive logout` clears the saved token + account (folders untouched); `--forget` also drops the remembered server. Switching hubs is `bdrive login <new-url>` then re-`init`. - `remote set --help` now explains the two remote kinds — object storage (s3/gs/file) vs a bdrive hub (https://<server>) — with examples, instead of leading with only s3://. (The https:// hub scheme was already accepted.) - Docs updated (README, CLAUDE.md, SKILL.md). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R7Q9ZKSZRTdvrSJkYLUmYs |
||
|
|
62175e29a9 |
feat(web): database config (file/sqlite/postgres) + docs (phase 3)
Wire a `database` block in bdrive web config: {driver: file|sqlite|postgres,
dsn}. Default file, unchanged (existing per-registry paths honored). sqlite
or postgres routes all five hub registries through the SQL MetaStore.
Verified: signup + project + invite round-trip survives a restart on BOTH
file and sqlite; the sqlite DB holds real relational tables/rows (not JSON
blobs). Docs: a "choosing a database" section (README), the persistence
architecture note (CLAUDE.md), and a config mention (SKILL.md).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7Q9ZKSZRTdvrSJkYLUmYs
|
||
|
|
bd7ea16200 |
feat(web): invite-only signup by default; harden signup postures
A hub on a public URL could be signed up to with any fake email. Make the safe posture the default and turn the dangerous ones into startup errors. Three supported postures: - invite-only (NEW default): allow_signup defaults false. A valid org invite link bootstraps an account even when self-signup is closed — the only way in. pageLogin/pageSignup detect a /join/<token> target via BuiltinAuth.InviteValid (wired to OrgDB.ValidInvite) and offer account creation; signupInvited skips the domain/approval/verification gates and activates immediately (the owner's invite is the vetting). - approval-gated: allow_signup + require_approval (no SMTP needed). - domain-restricted + verified: allow_signup + allowed_domains + require_verification (needs smtp). Hardening: - BuiltinAuth.ValidateSignupPolicy (run at hub startup in web.go) refuses to boot an open hub with no gate, or require_verification without a mailer (the link would otherwise only reach the server log). - handleAdminPolicy rejects enabling verification without SMTP; the UI toggle is disabled with clearer copy in that case. Tests: TestValidateSignupPolicy, TestInviteBootstrapsAccountWhenSignupClosed, TestPolicyVerificationNeedsMailer. Verified end to end in the running hub (outside-domain invitee onboards via a link; direct signup stays closed). Docs updated (README, CLAUDE.md, SKILL.md). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R7Q9ZKSZRTdvrSJkYLUmYs |
||
|
|
40fcda7cf7 |
polish(hub): round-3 — mobile actions reachable, admin gating settings UI
Fixes the two round-2 majors. User: on mobile the per-file actions (Share/History/Upload/Download) are now icon buttons in the header — reachable again (round 2 wrongly hid them). Search tolerates simple English plurals (ideas→idea), the no-matches state states what search covers, the sidebar shows the hub brand instead of the raw device name, and history-row downloads carry a download attribute. Logged-out loads redirect to sign-in from /api/config instead of firing 401-ing API calls. Admin: a hub-admin "Signup & access" settings screen (⚙ Admin in the sidebar) toggles email verification and admin approval live — persisted to auth.json and surviving restart — while the domain allowlist and admin list are shown read-only (deliberately server-config-owned so a browser session can't widen access). Pending approvals live on the same screen. Config toggles are now *bool so an explicit config value pins the setting each boot, else the UI-saved policy stands. Invite revoke confirms; role change re-renders the panel. Tests: policy persistence + reload, policy API admin-only. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R7Q9ZKSZRTdvrSJkYLUmYs |
||
|
|
baad9e6dc2 |
feat(hub): signup gating, admin lifecycle, and onboarding UX
Makes a self-hosted hub safe to expose on a public URL and operable without hand-editing JSON — addressing the blocker/major findings from the persona usability evaluations. Signup gating (config auth block, all optional): - allowed_domains: signup email must match (e.g. only @runbear.io) - require_verification: email-link activation before sign-in (reuses mailer) - require_approval: hub admins approve new accounts (admins list) - brand shown on the sign-in page; allow_signup:false already hid Sign up Accounts carry a Status (active/unverified/pending); non-active accounts cannot authenticate. Admin lifecycle (endpoints + web UI): - org: rename, member role change, member remove (last-owner guarded), invite list + revoke - project: create (web), rename, delete (from the org panel) - hub admins: approve/deny pending signups (sidebar bell + panel) - org-wide public-share audit with revoke UX: onboarding empty-state (explains invites, paste-invite + create-project) instead of a blank sidebar; visible "Search ⌘K" button; toasts replace blocking alert(); responsive layout with an off-canvas sidebar; joining via #join now survives a logged-out click (token carried through login). Web uploads are attributed to the signed-in account, not the server. Login/signup are rate-limited per IP. Tests: domain/verification/approval gates, auth rate limit, org+project lifecycle, owner-only guards, invite→join→role→remove over HTTP. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R7Q9ZKSZRTdvrSJkYLUmYs |
||
|
|
6ef1470088 |
feat(web): share-page footer + per-IP rate limit on /s/*
Rendered-markdown share pages get a small "Shared with BearDrive" footer linking to the repo — the shell template only, so shared raw HTML is still served byte-for-byte and the /s/ sandbox CSP is unchanged. All /s/* responses now sit behind a per-IP token bucket (default 120 req/min sustained + burst, tunable via "share_rpm" in the web config) so the one unauthenticated surface of a hub can't be scraped into a free CDN. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R7Q9ZKSZRTdvrSJkYLUmYs |
||
|
|
94ef66a4a9 |
feat(hub): organizations — projects walled by team membership, with invites
Every hub project now belongs to exactly one org (file-backed orgs.json, same load-at-open + atomic-rewrite discipline as the other registries). Membership (email -> owner|member) gates every per-project route: the viewer APIs, uploads, history, blobs, shares management, and the device sync store proxy; /api/projects lists only your orgs' projects, and project names are now scoped per org. Public share links (/s/) stay public by design. Design choices, per the simplest-consistent rule: - Migration: a pre-org hub sweeps all org-less projects into a "default" org at startup; ALL existing accounts join it (they could all see every project before, so anything narrower would lock someone out), oldest account as owner. Zero manual steps. - An account in no org that creates a project gets a fresh org named after itself, so nobody is ever blocked from starting to sync. - Invites are expiring multi-use links (default 7 days): an owner mints /#join/<token>, any signed-in account that opens it joins as member. The web UI shows the org in a sidebar footer (members on click, Invite button for owners). bdrive init needed no changes: its connect-existing flow lists projects through the now-filtered API. Tests: OrgDB + migration units; a 403/access matrix over every per-project route; invite mint/redeem flow; and a multi-device syncer test proving a device holding an org-B token can neither pull org A's files nor push into its store (sync degrades to Offline, never partial). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R7Q9ZKSZRTdvrSJkYLUmYs |
||
|
|
c0e785a4ba |
feat: public share links and /beardrive:install for Claude Code teams
Share links (bdrive share <file>, or the web UI's Share button): - Mint an unguessable public URL (/s/<token>) anyone can open — no account. HTML renders as a page, markdown gets a standalone shell, PDFs open inline; ?download=1 attaches. - Sandboxed: /s/* responses carry CSP `sandbox allow-scripts` + nosniff and never see auth cookies, so shared content's scripts run in an opaque origin and can't touch hub sessions. - Links serve the file's LATEST synced content and live until revoked; --expires makes self-destructing ones; --list/--revoke manage them. Re-sharing a file returns the same link. File-backed shares.json. - CLI resolves the project by walking up to .bdrive/ from the shared file, warns when the hub address is private (LAN-only links), and hints when the file hasn't synced yet. /beardrive:install (plugin command) — team onboarding driven by Claude: - Ensures the bdrive binary, signs in (bdrive login), runs bdrive init (whole folder or a shared subfolder like wiki/). - Asks before appending a CLAUDE.md section that teaches agents to put shareable artifacts in the shared folder and mint URLs with bdrive share; asks before registering project-level hooks in .claude/settings.json: blocking pull at UserPromptSubmit, async push on PostToolUse Write|Edit — teammates sync with or without the plugin. - Fix: the plugin hook script still checked for the old `.bdrive` file and was a silent no-op since the directory change; now checks -d. Tests: share creation gating, public access + sandbox headers, dedupe, latest-content semantics, revoke, expiry, markdown/download variants, list filtering, registry persistence. Docs updated (README sharing + Claude Code sections, SKILL.md, CLAUDE.md). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R7Q9ZKSZRTdvrSJkYLUmYs |
||
|
|
2d7f2f8bfa |
feat: auth, move-proof projects, interactive init/login, web history
Authentication (previous phase, now landed together with its follow-ups): - Email+password+name accounts behind an AuthProvider interface; the OSS server ships BuiltinAuth only (file-backed auth.json: bcrypt password hashes + SHA-256 token digests, plaintext never stored; server-owned /auth/* pages; managed deployments can swap in another provider). - bdrive login: loopback-callback browser flow (sign-up on the page, the terminal finishes itself) with a device-code fallback for headless machines; long-lived revocable device tokens in settings.json. - Password reset via plain SMTP (stdlib) with a log-link fallback when no SMTP is configured. Move-proof projects: - .bdrive is now a directory; config.json carries a stable mount id. The volume store (~/.bdrive/volumes/<mount-id>/) and registry are keyed by that id — never the folder path — so renames/moves are free. - The daemon re-reads the project config each tick and exits cleanly (propagating nothing) when its folder vanishes; the registry self-heals and the next bdrive command at the new location resumes with zero spurious changes. bdrive init is the front door (mnt/umnt removed; bdrive stop pauses): - Interactive on a TTY (create new / connect existing project from the server's list; whole folder / shared subfolder via the include list), full flag bypass (--name/--project/--shared/--yes), never prompts without a TTY. Runs the login flow first when there is no session. Default server: beardrive.ai (config.DefaultServer). Web history (revert-ready): - Hubs now always require auth; journal ops carry the signed-in account (user/user_name) alongside the git/OS fallback author. - File-backed device registry: per-device name, OS, account, and the public IP the server observed, joined into history at read time. - GET /api/p/<id>/history?path=|prefix= (newest first) and GET /api/p/<id>/blob?sha= stream any exact version — blobs are retained forever, so the next phase's revert is re-putting an old blob. - UI: History button (file versions or project feed), per-folder history shortcut, view/download of any past version. Tests: auth flows (callback, device-code, reset single-use, persistence, gating), history API + device registry, folder-move survival, registry self-heal, ops-carry-account; docs (README/SKILL/CLAUDE) updated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R7Q9ZKSZRTdvrSJkYLUmYs |
||
|
|
a7bb790615 |
feat: multi-project sync hub, bdrive login/init onboarding, .bdrive rename
The web server (bdrive web) becomes a full sync hub, and client devices
get one-command onboarding — without ever seeing storage info or holding
cloud credentials:
- bdrive web -c config.json: server configurable from a JSON file
(remote/addr/upload/upload_ttl/projects_db); explicit flags win.
- Hub mode: pointing bdrive web at a storage root hosts many projects,
each under <root>/<project-id>/ (remote.Prefixed). Projects live in a
file-backed registry (projects.json — loaded at open, rewritten
atomically per change) with create-or-join-by-name semantics.
- Per-project APIs: /api/projects (list/create/get) and
/api/p/<id>/{tree,file,render,download,upload/*,store/*}. The web UI
grows a project list with per-project browsing and hash deep links.
- Browser uploads and a store proxy for syncing devices: presigned
direct-to-storage PUTs when the backend can sign (S3 presign, GCS V4
signed URLs; expiring, credential-free), relayed through the server
otherwise. Journals are never presigned — only immutable blobs.
Blobs-before-journal and one-writer-per-journal invariants hold.
- https:// remote backend: a device syncs one hub project through
/api/p/<id>/store/* — mnt/sync/daemon/log all work unchanged.
- bdrive login <url>: verify a hub and remember it as the device default
(settings.json). bdrive init: create-or-join a project named after the
folder (--name/--project override), write .bdrive, seed a starter
.bdriveignore, mount, and start the daemon — one command per project.
- Hard-break rename: .beardrive->.bdrive, .beardriveignore->.bdriveignore,
~/.beardrive->~/.bdrive, BEARDRIVE_HOME->BDRIVE_HOME, temp/conflict
prefixes; old names are no longer read.
- Tests: presigning, project registry persistence, store API validation
and gating, project isolation over live HTTP, browser upload flows, and
two-device convergence through a hub (incl. read-only pull-only mode).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7Q9ZKSZRTdvrSJkYLUmYs
|
||
|
|
fafb7fe24d |
feat: fold the web viewer into the CLI as bdrive web
One binary instead of two: cmd/bdrive-web becomes the `web` subcommand (same flags and positional folder-or-URL argument). Measured cost of carrying the webapp in the CLI: +1.4 MB on a ~56 MB binary (~2.4%) — the cloud SDKs dominate either way. Drops the second goreleaser build and the separate go install path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HHEUaYfFHhmDvqLYw74Ehz |
||
|
|
1ac128db2c |
rename: sfs -> BearDrive everywhere; CLI becomes bdrive
Product and project are BearDrive; the CLI binary is bdrive (bdrive mnt, bdrive sync, ...), the web viewer is bdrive-web. All conventions follow the full name: .beardrive settings file, .beardriveignore, ~/.beardrive home, BEARDRIVE_HOME, .beardrive-conflict-* / .beardrive-tmp-* files. Plugin/skill/marketplace renamed to beardrive (/beardrive:mount). Module path is now github.com/runbear-io/beardrive — merge only after renaming the GitHub repo, or go install breaks. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HHEUaYfFHhmDvqLYw74Ehz |
||
|
|
7f098b3ff5 |
rename: brand the product BearDrive, keep sfs as the CLI
BearDrive is the product and repo brand (open source + future managed cloud); sfs stays the CLI command, and .sfs/.sfsignore/~/.sfs conventions are unchanged, so nothing breaks. Brew formula becomes runbear-io/tap/beardrive (still installs the sfs binary). Not included here (need the GitHub repo rename first): the Go module path and repo URLs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HHEUaYfFHhmDvqLYw74Ehz |