Commit Graph
12 Commits
Author SHA1 Message Date
Snow LeeandClaude Opus 5 62c241d3d6 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
2026-07-28 17:48:54 -07:00
9088127176 feat(sync): bdrive forget + sync --prune to take ignored paths off the hub (BEA-20) (#68)
Adding a path to .bdriveignore only stopped future uploads: anything that
synced before the rule existed stayed on the hub forever, with no command
that removed it without deleting it from local disk on every device.

Two engine changes make an explicit removal safe:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

---------

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

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

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

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

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

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

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

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

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

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

---------

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

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

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

* architecture: Server gains the Billing display seam

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

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

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

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

---------

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

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

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

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

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

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

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

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

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

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

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

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

---------

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

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

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

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

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

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

One fix found while driving the real UI: Tailwind preflight is off in this
app, so copied shadcn form controls rendered monospace/black and cards drew
a near-white hairline. Both are now supplied by slot in style.css.
2026-07-27 09:56:59 +09:00
6628718f1d fix(cli): agent hooks never sync folders this device didn't opt into (#44)
* fix(cli): agent hooks never sync folders this device didn't opt into

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

---------

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

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

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 15:23:18 -07:00