mirror of
https://github.com/runbear-io/beardrive.git
synced 2026-08-25 08:08:08 +02:00
main
13
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
a3408bb198 |
fix(shares): a folder is not a sync problem (#169)
bdrive share <folder> answered "not synced to this project yet" on a fully synced folder, so users went looking for a sync fault that wasn't there. snap.files maps FILES, so a folder always missed the existence check and landed in the same 404 as a path that doesn't exist — and the CLI bolted "wait a few seconds for the daemon" onto it. Tell the two apart in the handler (any key under p+"/" — never bare p, or notes-archive/x.md makes "notes" a folder): 400 with "share links are per-file" naming the lexicographically smallest file inside, 404 with today's wording otherwise. The CLI's daemon hint was already 404-only, so it stops appearing on its own; the only CLI change is printing a 400 body without httpBodyError's status prefix. The web UI mints through the same handler, so it gets the same distinction. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
c3b9fa5858 |
fix(cli): status looks at the folder, not just the cache (BEA-106) (#171)
`bdrive status` answered from the state cache and the journal and never looked at the working folder. With the daemon stopped, an edit nobody has scanned is in neither — so the one command that answers "is this folder in sync?" reported `pending: 0` with the change sitting right there. A wrong "you're clean" is worse than no answer. syncer.Drift is a sibling of Explain/SyncedFiles with the same contract: loadFilter + walkFolder + the scan's own size+mtime compare, and nothing else. status prints it as a `local:` line, distinct from `pending` — they are different states and a change can be in either or both. The load-bearing property is that it stays a pure read. status is what someone runs when sync is stuck; a version that scanned-and-committed would change what it was asked to describe, and would write ops from a command nobody expects to write. Pinned by a test hashing the device journal and the state cache before and after. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
a3dfa73fef |
Catch a credential when it syncs, not only when you share it (#162)
* refactor(secrets): lift the share-time credential rules into internal/secrets The rules only ever ran on the rarest path a file takes. Moving them out of internal/webapp is what lets internal/syncer run the same six rules on the path every file takes, without inverting the dependency. Pure move plus one addition: Label(), the six human strings that until now lived only in the frontend's SECRET_LABELS — so 'bdrive share' stops printing a bare rule id where the web dialog says 'an AWS access key'. Rule ids and the rule/line JSON tags are unchanged: Browser.tsx keys off them, so they are a wire contract. * feat(sync): warn when a synced file looks like it holds a credential The six share-time rules now run on the path every file takes. A file with an AWS key in it used to ride a normal sync to the hub, to every teammate's disk and into every future agent's context with no badge and no warning — while the Share dialog one click later blocked that exact file. Warn, never block: the op is journaled and pushed exactly as before. A hold arm would mean a false positive silently parks someone's changes, and it would break the cycle's degrade-to-offline posture. - scan() reads the blob PutBlobFile just wrote (the bytes that were actually journaled), only on the branches that wrote one — an unchanged file is still never re-read. - Findings persist per path in secrets-<mount>.json, merged rather than replaced: nearly every cycle scans zero files, and a whole-set rewrite would erase the warning seconds after it appeared. Fixing the file clears it. - bdrive status grows a secrets block; the agent hook appends one advisory sentence. Rule ids and line numbers only, never the matched bytes. - SaveSecrets failing logs and continues: advisory telemetry never gets a veto over convergence. * docs: the credential check now runs on sync, not only on share README, the CLI reference and project-files get the new bdrive status block and the warn-never-block posture, with the three limits stated (checked when it changes, first 1 MiB, writing device only). Diagrams: internal/secrets is a package of its own in the overview, secretLog joins the sync engine, and the share-gate class notes that it no longer owns the rules. * test(sync): assert an unchanged file is never re-read for credentials The check must ride the branch that already reads the file. Clearing the record by hand and cycling proves it: a scan that re-read unchanged files would put the finding back, and the daemon's 3-second tick would pay for it on every file. |
||
|
|
edfe46c0aa |
post_sync: run a local command when teammates' changes land (#163)
Inbound sync was invisible to the machine it landed on — a local index, cache or notifier had to poll. A `post_sync` command in the folder's own .bdrive/config.json now runs once per cycle that applied peer changes, with the batch as JSON on stdin. The batch rides out on a new Result.Inbound rather than the inbound spool: DrainInbound is destructive and `bdrive sync --hook` is its only consumer, so a second drainer would silently empty the agent's "teammates changed X" context. Both are kept, and both comments now say why. Cycle becomes a thin wrapper over cycleLocked so the hook is spawned after the volume flock drops — a property of the code shape, not a rule each of the seven call sites has to remember. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
0dd474baab |
Delta sync: large files move as content-defined chunks (#161)
* feat(sync): delta sync — large files move as content-defined chunks Files over 4 MiB push as chunks/<sha256> pieces plus a manifests/<sha256> chunk list keyed by the whole file's hash, so Op.Blob alone locates it and the journal format is byte-identical. A 1-byte edit to a 20 MiB file now transfers ~2 MB instead of ~21 MB, both directions; chunk boundaries come from a rolling hash (restic/chunker), so front insertions stay cheap. The hub reassembles whole blobs on demand (spool, verify, backfill, serve), which is the entire backward-compatibility story: old clients ask for blobs/<sha> and never learn anything changed. Proven by e2e tests that build the real pre-change binary from the pinned merge-base commit. The push skip-proof is one Exists per chunk — three cheaper proxies (local basis, manifest existence, stored manifest content) each proved false or forgeable across four CTO review rounds and are recorded in the code comment. Hub-side, manifests are write-once and must name only chunks the store holds; reassembly is bounded at 256 MiB against amplified manifests. Also: per-file sync ceiling 32 -> 100 MiB; import refuses archives whose journals reference content they do not hold (--allow-incomplete overrides). Deploy hubs before clients: old hubs refuse chunk keys (push degrades to offline-retry), and old clients cap reads at 32 MiB so 32-100 MiB files report "blob corrupt on remote" until the client upgrades. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R6nqxi5a9qcENmJvrgBJF7 * fix(test): fetch the pinned pre-delta commit on shallow CI clones buildOldBinary archives the pinned merge-base sha, which a fetch-depth-1 actions/checkout does not have — all three old-binary e2e tests failed in CI with exit 128 while passing on any full local clone. On archive failure, fetch just that commit (--depth=1, one object; actions/checkout persists credentials so the in-job fetch works) and retry. Verified against a real GitHub shallow clone: archive fails, the single-sha fetch succeeds, archive then yields the pre-delta tree. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R6nqxi5a9qcENmJvrgBJF7 --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
33ca0caeab |
fix(sync): connecting a folder adopts the project's files instead of forking them (#159)
A device's first cycle on a volume was treated as a concurrent edit for every path the project already held. Whichever side's clock happened to sort higher won -- so a joiner's seeded .bdriveignore or agent-written AGENTS.md could replace the team's -- and the loser landed beside it as a .bdrive-conflict-<device>-<time> file. A first cycle is a join, not an edit. Cycle step 1b holds the scan's ops back over the pull and demotes any whose path the project already holds to lamport 0, which sorts under every op a project can carry (scan's clock starts at 1). The project's version then wins deterministically on every device, and conflictCopies skips those ops the way it already skips re-asserted ones. The local content is still journaled and pushed, so it stays in History and `bdrive restore --list <path>` can bring it back. Reported as `adopted: N`. Concurrent edits after the join are untouched. Claude-Session: https://claude.ai/code/session_01GLCyEWP2XZhYjBssvdCDQm Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
d3d92bf904 |
feat(webapp): refuse to share a file that looks like it holds credentials (BEA-111) (#137)
Minting a share link ran zero content checks: a file holding an AWS-shaped key became a public URL on one click, and the CLI printed nothing but the link. handleShareCreate now reads the first 1 MiB and runs six anchored rules between the synced-path check and Shares.Create, answering 409 with rule ids and line numbers unless the request carries confirm: true. The matched text never leaves scanSecrets — not into the body, not into a log line. TestShareSecretNeverEchoed greps both for the planted string, because a 409 body is the easiest place in this codebase to leak it. Both callers carry the override, since the gate alone would turn any false positive into a hard block with no way out: `bdrive share --force`, and the browser's Share-anyway dialog on modalConfirm (no new component). A path that already has a live link skips the scan — its content is public already, so withholding the URL protects nobody — but alreadyPublic drops links whose creator left the org, since those 404 at /s/ and would otherwise wave a secrets file straight through. A failed blob read is 503, not a silent pass: the repo's "degrade rather than fail" posture is for sync cycles, and a check that skips itself on a storage hiccup is the false confidence this exists to remove. Every user-facing string says the file was checked at the moment you shared it. A link serves the file's LATEST content forever, so a key written into an already-shared file is never caught — that open loop stays open, and the copy is the only thing stopping v1 from claiming otherwise. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |