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>
This commit is contained in:
Snow W. Lee (Sungwon)
2026-07-27 14:49:58 +09:00
committed by GitHub
co-authored by Claude Fable 5
parent 69e7231a70
commit 0236e1b272
13 changed files with 350 additions and 58 deletions
+3 -2
View File
@@ -67,8 +67,8 @@ $ bdrive login https://your-hub && cd ~/workspace && bdrive init
(last-writer-wins), and the losing version is preserved as a
`name.bdrive-conflict-<device>-<time>` file. Nothing is silently dropped.
- **Selective sync** — a gitignore-style `.bdriveignore` opts files out, and
`bdrive init --shared <dir>` (or the interactive prompt) narrows sync to
one shared subfolder.
`bdrive init --shared <dirs>` (or the interactive prompt) narrows sync to
one or more shared subfolders (`--shared wiki,docs`, or repeat the flag).
- **macOS & Linux.**
## Install
@@ -140,6 +140,7 @@ hub's own storage, never something a syncing client points at directly:
| `bdrive logout` | Sign this device out — clear the saved token/account (`--forget` also drops the remembered server) |
| `bdrive init [folder]` | Create/connect a project and start syncing — interactive on a TTY, flags (`--name/--project/--shared/--yes`) for scripts; re-run to resume |
| `bdrive stop [folder]` | Stop syncing, including agent sync hooks (files stay; `bdrive init` resumes) |
| `bdrive scope [add\|rm <dirs...>]` | Show or change which subfolders sync (the include list set by `init --shared`) — no JSON editing; the daemon picks changes up in seconds. `rm` deletes nothing, locally or on the hub |
| `bdrive url [path]` | Internal hub link for a file/folder (sign-in + membership required; `--sync` pushes first; no arg = project home). Computed locally |
| `bdrive share <file>` | Public URL for a synced file (`--list`, `--revoke`, `--expires`) |
| `bdrive sync [folder]` | Run one sync cycle now. `--note <text>` stamps session context (e.g. an agent session id) onto changes — shown in `bdrive log` and hub history; keeps applying to daemon-committed changes until `--note-ttl` (default 30m) expires. `--hook <label>` is agent-hook plumbing: event JSON on stdin, sync + note, gated-link formula (Claude Code hook JSON) on stdout |
+1 -1
View File
@@ -88,7 +88,7 @@ classDiagram
class Commands {
init login logout
sync stop status log
sync stop scope status log
url share export import
web daemon hooks read-log skill
}
+51 -24
View File
@@ -47,7 +47,8 @@ venv/
// folder just resumes syncing (which is also how a moved/renamed folder
// picks up where it left off).
func initCmd() *cobra.Command {
var projectID, projectName, shared string
var projectID, projectName string
var shared []string
var yes, foreground bool
c := &cobra.Command{
Use: "init [folder]",
@@ -56,8 +57,8 @@ func initCmd() *cobra.Command {
syncing it through your bdrive server.
On a terminal, init asks what you want: create a new project or connect an
existing one, and whether to sync the whole folder or only a shared
subfolder (e.g. ./shared). Flags answer those questions non-interactively;
existing one, and whether to sync the whole folder or only shared
subfolders (e.g. ./shared). Flags answer those questions non-interactively;
without a TTY init never prompts (it creates-or-joins a project named after
the folder and syncs the whole folder).
@@ -70,6 +71,7 @@ the folder was renamed or moved.`,
bdrive init ./notes --name shared-notes
bdrive init --project p-7f3a2c91 # connect an existing project
bdrive init --shared shared # only ./shared syncs
bdrive init --shared wiki,docs # only ./wiki and ./docs sync
bdrive init --yes # accept all defaults (no prompts)`,
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
@@ -86,6 +88,9 @@ the folder was renamed or moved.`,
return err
} else if ok && proj.Remote != "" {
fmt.Printf("resuming %s (project %s)\n", folder, proj.Volume)
if cmd.Flags().Changed("shared") {
fmt.Println("note: --shared is ignored on resume — change what syncs with `bdrive scope add`/`rm`")
}
return startSync(cmd.Context(), folder, proj, foreground, 3*time.Second, 10*time.Second)
}
@@ -115,20 +120,18 @@ the folder was renamed or moved.`,
}
// What syncs?
if shared == "" && interactive && !cmd.Flags().Changed("shared") {
if len(shared) == 0 && interactive && !cmd.Flags().Changed("shared") {
shared, err = chooseScope()
if err != nil {
return err
}
}
var include []string
if shared != "" {
shared = strings.Trim(path.Clean(filepath.ToSlash(shared)), "/")
if shared == "" || shared == "." || strings.HasPrefix(shared, "..") {
return fmt.Errorf("invalid shared folder %q", shared)
}
include = []string{shared + "/"}
if err := os.MkdirAll(filepath.Join(folder, filepath.FromSlash(shared)), 0o755); err != nil {
include, err := cleanShared(shared)
if err != nil {
return err
}
for _, inc := range include {
if err := os.MkdirAll(filepath.Join(folder, filepath.FromSlash(strings.TrimSuffix(inc, "/"))), 0o755); err != nil {
return err
}
}
@@ -152,8 +155,12 @@ the folder was renamed or moved.`,
}
}
fmt.Printf("initialized %s\n server: %s\n project: %s (%s)\n", folder, server, p.Name, p.ID)
if shared != "" {
fmt.Printf(" syncing: ./%s only\n", shared)
if len(include) > 0 {
dirs := make([]string, len(include))
for i, inc := range include {
dirs[i] = "./" + strings.TrimSuffix(inc, "/")
}
fmt.Printf(" syncing: %s only\n", strings.Join(dirs, ", "))
}
if err := startSync(cmd.Context(), folder, proj, foreground, 3*time.Second, 10*time.Second); err != nil {
return err
@@ -174,7 +181,7 @@ next steps:
}
c.Flags().StringVar(&projectID, "project", "", "connect an existing project by id (p-xxxxxxxx)")
c.Flags().StringVar(&projectName, "name", "", "project name to create or join (default: folder name)")
c.Flags().StringVar(&shared, "shared", "", "sync only this subfolder (e.g. shared)")
c.Flags().StringSliceVar(&shared, "shared", nil, "sync only these subfolders (repeatable or comma-separated, e.g. wiki,docs)")
c.Flags().BoolVarP(&yes, "yes", "y", false, "accept defaults, never prompt")
c.Flags().BoolVarP(&foreground, "foreground", "f", false, "run the sync daemon in the foreground")
return c
@@ -248,23 +255,43 @@ func chooseProject(server, token, defaultName string) (serverProject, error) {
return projects[idx], nil
}
// chooseScope returns "" for whole-folder sync, or the shared subfolder.
func chooseScope() (string, error) {
// chooseScope returns nil for whole-folder sync, or the shared subfolders.
func chooseScope() ([]string, error) {
var mode string
if err := survey.AskOne(&survey.Select{
Message: "What should sync?",
Options: []string{"The whole folder", "Only a shared subfolder"},
Options: []string{"The whole folder", "Only shared subfolders"},
}, &mode); err != nil {
return "", err
return nil, err
}
if mode == "The whole folder" {
return "", nil
return nil, nil
}
dir := "shared"
if err := survey.AskOne(&survey.Input{Message: "Shared subfolder:", Default: "shared"}, &dir); err != nil {
return "", err
dirs := "shared"
if err := survey.AskOne(&survey.Input{Message: "Shared subfolder(s), space- or comma-separated:", Default: "shared"}, &dirs); err != nil {
return nil, err
}
return dir, nil
return strings.Fields(strings.ReplaceAll(dirs, ",", " ")), nil
}
// cleanShared normalizes --shared entries into include patterns ("wiki/"):
// slashes cleaned, duplicates dropped. Any entry that resolves to the mount
// root or escapes it is an error — a silently-dropped "." would widen the
// scope to the whole folder.
func cleanShared(shared []string) ([]string, error) {
var out []string
seen := map[string]bool{}
for _, s := range shared {
s = strings.Trim(path.Clean(filepath.ToSlash(strings.TrimSpace(s))), "/")
if s == "" || s == "." || strings.HasPrefix(s, "..") {
return nil, fmt.Errorf("invalid shared folder %q", s)
}
if !seen[s] {
seen[s] = true
out = append(out, s+"/")
}
}
return out, nil
}
type serverProject struct {
+57
View File
@@ -0,0 +1,57 @@
package main
import (
"reflect"
"testing"
)
func TestScopeRemove(t *testing.T) {
include := []string{"wiki/", "docs/", "*.md"}
for _, tc := range []struct {
args []string
want []string
err bool
}{
{args: []string{"docs"}, want: []string{"wiki/", "*.md"}},
{args: []string{"docs/"}, want: []string{"wiki/", "*.md"}}, // normalized match
{args: []string{"*.md"}, want: []string{"wiki/", "docs/"}}, // literal pattern match
{args: []string{"wiki", "docs"}, want: []string{"*.md"}},
{args: []string{"notes"}, err: true}, // not in scope
} {
got, err := scopeRemove(include, tc.args)
if tc.err != (err != nil) {
t.Errorf("scopeRemove(%q) err = %v, want err %v", tc.args, err, tc.err)
continue
}
if !tc.err && !reflect.DeepEqual(got, tc.want) {
t.Errorf("scopeRemove(%q) = %q, want %q", tc.args, got, tc.want)
}
}
}
func TestCleanShared(t *testing.T) {
for _, tc := range []struct {
in []string
want []string
err bool
}{
{in: nil, want: nil},
{in: []string{"wiki"}, want: []string{"wiki/"}},
{in: []string{"wiki", "docs"}, want: []string{"wiki/", "docs/"}},
{in: []string{" wiki ", "./docs/", "wiki"}, want: []string{"wiki/", "docs/"}}, // trimmed, cleaned, deduped
{in: []string{"a/b"}, want: []string{"a/b/"}},
{in: []string{""}, err: true},
{in: []string{"wiki", ""}, err: true}, // "wiki,,docs" typo must not half-apply
{in: []string{"."}, err: true}, // would silently mean whole-folder sync
{in: []string{"../up"}, err: true},
} {
got, err := cleanShared(tc.in)
if tc.err != (err != nil) {
t.Errorf("cleanShared(%q) err = %v, want err %v", tc.in, err, tc.err)
continue
}
if !tc.err && !reflect.DeepEqual(got, tc.want) {
t.Errorf("cleanShared(%q) = %q, want %q", tc.in, got, tc.want)
}
}
}
+1
View File
@@ -52,6 +52,7 @@ everything keeps working offline; changes sync when the remote is reachable.`,
shareCmd(),
urlCmd(),
stopCmd(),
scopeCmd(),
syncCmd(),
readLogCmd(),
hooksCmd(),
+169
View File
@@ -0,0 +1,169 @@
package main
import (
"fmt"
"os"
"path/filepath"
"strings"
"github.com/spf13/cobra"
"github.com/runbear-io/beardrive/internal/config"
)
// bdrive scope shows and edits the project's sync scope — the include list
// in .bdrive/config.json that `init --shared` seeds — so growing or
// shrinking what syncs never means hand-editing JSON. The daemon re-reads
// the config every tick, so changes apply within seconds.
func scopeCmd() *cobra.Command {
c := &cobra.Command{
Use: "scope",
Short: "Show or change which subfolders sync (the include list)",
Long: `Show or change the project's sync scope: the include list in
.bdrive/config.json, as set by init --shared. An empty list means the whole
folder syncs. Run from the mount root; the daemon picks changes up within
seconds.
Removing a folder stops syncing it but deletes nothing — local files stay,
and the hub keeps everything already synced.`,
Example: ` bdrive scope # show what syncs
bdrive scope add docs # also sync ./docs
bdrive scope rm docs # stop syncing ./docs (files stay everywhere)`,
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
folder, err := absFolder(nil)
if err != nil {
return err
}
proj, err := mustProject(folder)
if err != nil {
return err
}
printScope(proj)
return nil
},
}
c.AddCommand(scopeAddCmd(), scopeRmCmd())
return c
}
func scopeAddCmd() *cobra.Command {
return &cobra.Command{
Use: "add <dir>...",
Short: "Add shared subfolders to the sync scope",
Args: cobra.MinimumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
folder, err := absFolder(nil)
if err != nil {
return err
}
proj, err := mustProject(folder)
if err != nil {
return err
}
if len(proj.Include) == 0 {
return fmt.Errorf("this project syncs the whole folder; adding %s would narrow it to only that — re-run `bdrive init` with --shared if you want a scoped sync", strings.Join(args, ", "))
}
incs, err := cleanShared(args)
if err != nil {
return err
}
seen := map[string]bool{}
for _, i := range proj.Include {
seen[i] = true
}
added := 0
for _, inc := range incs {
if seen[inc] {
continue
}
if err := os.MkdirAll(filepath.Join(folder, filepath.FromSlash(strings.TrimSuffix(inc, "/"))), 0o755); err != nil {
return err
}
proj.Include = append(proj.Include, inc)
seen[inc] = true
added++
}
if added == 0 {
fmt.Println("already in the sync scope")
} else if _, err := config.SaveProject(folder, proj); err != nil {
return err
}
printScope(proj)
return nil
},
}
}
func scopeRmCmd() *cobra.Command {
return &cobra.Command{
Use: "rm <dir>...",
Short: "Remove shared subfolders from the sync scope (deletes nothing)",
Args: cobra.MinimumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
folder, err := absFolder(nil)
if err != nil {
return err
}
proj, err := mustProject(folder)
if err != nil {
return err
}
kept, err := scopeRemove(proj.Include, args)
if err != nil {
return err
}
if len(kept) == 0 {
return fmt.Errorf("removing the last shared folder would switch to syncing the whole folder; run `bdrive stop` to stop syncing instead")
}
proj.Include = kept
if _, err := config.SaveProject(folder, proj); err != nil {
return err
}
fmt.Println("removed from the sync scope — nothing was deleted, locally or on the hub")
printScope(proj)
return nil
},
}
}
// scopeRemove drops the named dirs from the include list, matching each
// argument both literally and in normalized "dir/" form (hand-edited
// configs may hold arbitrary patterns). Unknown entries are an error.
func scopeRemove(include, args []string) ([]string, error) {
remove := map[string]bool{}
for _, a := range args {
keys := map[string]bool{strings.TrimSpace(a): true}
if norm, err := cleanShared([]string{a}); err == nil {
keys[norm[0]] = true
}
found := false
for _, i := range include {
if keys[i] {
remove[i] = true
found = true
}
}
if !found {
return nil, fmt.Errorf("%q is not in the sync scope (see `bdrive scope`)", a)
}
}
var kept []string
for _, i := range include {
if !remove[i] {
kept = append(kept, i)
}
}
return kept, nil
}
func printScope(proj config.Project) {
if len(proj.Include) == 0 {
fmt.Println("the whole folder syncs (no include list)")
return
}
fmt.Println("syncing only:")
for _, i := range proj.Include {
fmt.Println(" ./" + strings.TrimSuffix(i, "/"))
}
}
+11 -7
View File
@@ -1,6 +1,6 @@
---
description: Start syncing a project in this folder — create a new BearDrive project or connect an existing one, whole folder or a shared subfolder, and start the sync daemon
argument-hint: "[folder] [--name <project> | --project <p-id>] [--shared <dir>]"
description: Start syncing a project in this folder — create a new BearDrive project or connect an existing one, whole folder or shared subfolders, and start the sync daemon
argument-hint: "[folder] [--name <project> | --project <p-id>] [--shared <dirs>]"
---
Start syncing a project with BearDrive. Arguments: `$ARGUMENTS` (optional
@@ -43,17 +43,21 @@ Follow these steps:
4. **Initialize** — two hard rules:
- **Never sync a repo root**: inside a repo, knowledge syncs as a
scoped subfolder via `--shared`. A dedicated knowledge folder
(empty dir, standalone vault) may be the mount itself.
- **Never sync a repo root**: inside a repo, knowledge syncs as
scoped subfolders via `--shared` (one or more — several folders can
share one project when the same people should see all of them;
folders needing different access go in separate projects). A
dedicated knowledge folder (empty dir, standalone vault) may be the
mount itself.
- **One transport per folder**: a git-tracked dir must leave git
tracking before it syncs (`git rm -r --cached <dir>` + gitignore;
stage it, let the user commit). Offer one-way git snapshots if they
want a git record; `bdrive log -p <path>` covers history for most.
```sh
bdrive init --name <project> --yes # dedicated knowledge folder
bdrive init --name <project> --shared wiki # in a repo: only ./wiki syncs
bdrive init --name <project> --yes # dedicated knowledge folder
bdrive init --name <project> --shared wiki # in a repo: only ./wiki syncs
bdrive init --name <project> --shared wiki,docs # several shared subfolders, one project
```
5. **Register agent sync hooks**: run `bdrive hooks install <folder>`. It
+17 -11
View File
@@ -1,6 +1,6 @@
---
description: Set up BearDrive for this project — install the CLI, sign in, create/connect a project, optionally document the shared folder in CLAUDE.md, and register project-level sync hooks so every teammate's files stay fresh during Claude sessions
argument-hint: "[project-name] [--shared <dir>]"
argument-hint: "[project-name] [--shared <dirs>]"
---
Set up BearDrive for the current project, end to end. Work through these
@@ -27,19 +27,22 @@ browser window is coming, then sign in:
## 3. Initialize the project
If `$ARGUMENTS` gives a project name and/or `--shared <dir>`, use them.
If `$ARGUMENTS` gives a project name and/or `--shared <dirs>` (one or more,
comma-separated or repeated), use them.
Otherwise ask the user two questions (or infer from their request):
- **Create a new project or connect an existing one?** (`bdrive init
--name <name>` creates-or-joins by name; `bdrive init --project <p-id>`
connects by id.)
- **Sync the whole folder, or only a shared subfolder?** Hard rule:
**never sync a repo root** — inside a repo, knowledge always syncs as a
scoped subfolder via `--shared`. Whole-folder is only for a dedicated
- **Sync the whole folder, or only shared subfolders?** Hard rule:
**never sync a repo root** — inside a repo, knowledge always syncs as
scoped subfolders via `--shared`. Whole-folder is only for a dedicated
knowledge folder (an empty dir, a standalone vault) that is the mount
itself. Don't ask open-endedly: scan the repo for an existing knowledge
folder (`wiki/`, `docs/`, `notes/`, `handbook/`, an Obsidian vault —
markdown-heavy, not source code) and propose the best candidate for
confirmation, e.g. "I found `./wiki` — sync that?".
itself. Don't ask open-endedly: scan the repo for existing knowledge
folders (`wiki/`, `docs/`, `notes/`, `handbook/`, an Obsidian vault —
markdown-heavy, not source code) and propose the candidates for
confirmation, e.g. "I found `./wiki` and `./docs` — sync both?"
(`--shared wiki,docs` puts them in one project — one membership, one
permission set; folders needing different access go in separate projects).
**One transport per folder.** If the chosen folder is currently git-tracked,
BearDrive and git would both write it — the silent-revert hazard. Get consent,
@@ -50,10 +53,13 @@ Obsidian, symlinks — in the beardrive skill's "Connecting knowledge tooling".)
Then run it non-interactively, e.g.:
```sh
bdrive init --name <project-name> --yes # dedicated knowledge folder
bdrive init --name <project-name> --shared wiki # in a repo: only ./wiki syncs
bdrive init --name <project-name> --shared wiki # in a repo: only ./wiki syncs
bdrive init --name <project-name> --shared wiki,docs # several shared subfolders, one project
```
Re-running `bdrive init --yes` later is always safe: it resumes syncing
(including after the folder was renamed or moved).
(including after the folder was renamed or moved). To add or remove shared
subfolders later, use `bdrive scope add <dir>` / `bdrive scope rm <dir>`
from the mount root — never hand-edit `.bdrive/config.json`.
After init, tell git what's what: add `.bdrive/` to `.gitignore` (per-machine
state, never committed) and COMMIT `.bdriveignore` (on a `--shared` mount the
+6 -5
View File
@@ -13,9 +13,10 @@ Use this skill whenever the user is working with the `bdrive` CLI: initializing
| Action | Command |
|---|---|
| Start syncing a project (create/connect; the front door) | `bdrive init [<folder>]` — interactive on a TTY; flags `--name <x>` / `--project <id>` / `--shared <dir>` / `--yes` for scripts and agents (NEVER prompts without a TTY). Re-run to resume, including after the folder was renamed/moved. Runs the login flow first (against your hub URL) if the device has no session. |
| Start syncing a project (create/connect; the front door) | `bdrive init [<folder>]` — interactive on a TTY; flags `--name <x>` / `--project <id>` / `--shared <dirs>` (comma-separated or repeated) / `--yes` for scripts and agents (NEVER prompts without a TTY). Re-run to resume, including after the folder was renamed/moved. Runs the login flow first (against your hub URL) if the device has no session. |
| Run the daemon in the foreground | `bdrive init -f` |
| Stop syncing | `bdrive stop [<folder>]` — pauses daemon *and* agent hooks; `bdrive init` resumes (`--forget` also unregisters) |
| Show/change which subfolders sync | `bdrive scope` / `bdrive scope add <dirs...>` / `bdrive scope rm <dirs...>` — edits the include list set by `init --shared` (run from the mount root; NEVER hand-edit config.json for this). The daemon applies it within seconds. `rm` stops syncing a folder but deletes nothing, locally or on the hub; removing the last entry is refused (that would flip to whole-folder sync — use `bdrive stop` instead) |
| One sync cycle now | `bdrive sync [<folder>]``--note <text>` stamps session context; `--hook <label>` is the Claude turn-start hook's plumbing (event JSON in, sync + note, gated-link formula out) |
| Register agent sync hooks (Claude Code, Codex, Gemini CLI, Hermes) | `bdrive hooks install [<folder>]` — auto-detects the platforms in use and merges pull/push/session-note/read-tracking hooks into each one's own hook config, idempotently; bare `bdrive hooks` shows the status table |
| Install this skill on another agent (Codex, Gemini CLI, Hermes, Claude Code) | `bdrive skill install [<folder>]` — writes the binary's own copy of this skill to each detected platform's user-level skills dir (`~/.codex/skills/beardrive/SKILL.md` and friends), idempotently; bare `bdrive skill` shows the status table. Then the user asks that agent to set the folder up and it runs `init` + `hooks install` itself |
@@ -47,7 +48,7 @@ Two files at the mount root control a folder's sync behavior:
"id": "m-5a10b713",
"volume": "agent-workspace",
"remote": "https://drive.example.com/p/p-7f3a2c91",
"include": ["shared/"] // optional: sync ONLY these (set by init --shared)
"include": ["shared/"] // optional: sync ONLY these (init --shared; edit with bdrive scope add/rm)
}
```
@@ -72,7 +73,7 @@ Selective-sync semantics — important when advising users:
### Init flow
1. Sign-in happens lazily: `bdrive init` runs the login flow itself when the device has no session, so don't ask users to sign up ahead of time. Bare `bdrive login` targets BearDrive Cloud (beardrive.ai) — signing up in the browser auto-creates a free personal workspace; a pending team invite routes them into that team instead. Self-hosting teams: `bdrive login https://your-hub`.
2. Pick what to sync BEFORE running init. In a repo, look for an existing knowledge folder (`wiki/`, `docs/`, `notes/`, `handbook/`, an Obsidian vault) and propose it as `--shared <dir>`confirm, don't interrogate. Never sync a repo root. Then run `bdrive init` in the folder. Interactive on a TTY (create new / connect existing project; whole folder / shared subfolder); with flags or without a TTY it creates-or-joins a project named after the folder and syncs everything. It:
2. Pick what to sync BEFORE running init. In a repo, look for existing knowledge folders (`wiki/`, `docs/`, `notes/`, `handbook/`, an Obsidian vault) and propose them as `--shared <dirs>`several folders can share one project (`--shared wiki,docs`) when the same people should see all of them; folders needing different access belong in separate projects. Confirm, don't interrogate. Never sync a repo root. Then run `bdrive init` in the folder. Interactive on a TTY (create new / connect existing project; whole folder / shared subfolder); with flags or without a TTY it creates-or-joins a project named after the folder and syncs everything. It:
- writes `<folder>/.bdrive/config.json` (mount id + project + remote) and registers the mount id in `~/.bdrive/mounts.json`,
- seeds a starter `.bdriveignore` (node_modules, build dirs, caches, `.env*`) when none exists,
- opens the volume store under `~/.bdrive/volumes/<mount-id>/`,
@@ -84,7 +85,7 @@ Selective-sync semantics — important when advising users:
- `--name <x>` — project name to create-or-join (default: folder basename).
- `--project <id>` — connect an existing project by id (`p-xxxxxxxx`).
- `--shared <dir>` — sync only this subfolder (becomes the include list; remote paths keep the prefix so all devices see the same layout).
- `--shared <dirs>` — sync only these subfolders (repeatable or comma-separated: `--shared wiki,docs`; becomes the include list; remote paths keep the prefix so all devices see the same layout).
- `--yes, -y` — accept defaults, never prompt.
- `--foreground, -f` — run the daemon in the foreground (systemd/launchd/containers).
@@ -235,7 +236,7 @@ bdrive stop ./notes --forget
When guiding `bdrive init`, detect existing knowledge tooling and connect it instead of blind-syncing the folder. Two rules govern every case:
- **One transport per folder.** Never sync a folder that has another writer. Git-tracked paths: a teammate's `git pull` or branch switch rewrites files with older content, and sync broadcasts that as a fresh edit — silently reverting the team's latest pages. A gbrain brain root: every private capture and overnight enrichment would become team-visible, and each member's cron rewriting the same pages fills the project with conflict copies. Moving a folder from git to beardrive is a **handoff**: `git rm -r --cached <dir>` + add `<dir>/` to `.gitignore`, stage the change but let the user commit it (teammates then pull and re-init; identical content converges with no conflicts). If they want a git record anyway, offer **one-way snapshots** (a scheduled job commits the synced folder's state to an archive branch — git only ever reads the folder) and note that hub history (`bdrive log -p <path>`) usually covers the need.
- **Knowledge syncs as a scoped folder.** Inside a repo, always `--shared <dir>` — never the repo root. A dedicated knowledge folder (an empty dir, a standalone vault) may be the mount itself. The sync scope is per-device (`.bdrive/` never syncs), so recommend the same `--shared <dir>` when each teammate connects.
- **Knowledge syncs as a scoped folder.** Inside a repo, always `--shared <dirs>` (one or more subfolders) — never the repo root. A dedicated knowledge folder (an empty dir, a standalone vault) may be the mount itself. The sync scope is per-device (`.bdrive/` never syncs), so recommend the same `--shared <dirs>` when each teammate connects.
Detection ladder — first match wins; if two rungs match, ask which to connect:
+23 -3
View File
@@ -1,6 +1,6 @@
---
title: Scoping the folder
description: Decide what agents can see — narrow a project to one subfolder, and opt files out with a gitignore-style .bdriveignore.
description: Decide what agents can see — narrow a project to chosen subfolders, change the scope later with bdrive scope, and opt files out with a gitignore-style .bdriveignore.
---
Shared agent memory works better when it's curated. A folder holding
@@ -12,15 +12,18 @@ subfolder, and **`.bdriveignore`** that opts individual paths out. Both are
applied symmetrically — the same filter governs what's read from disk and what's
written back to it.
## Sync only a subfolder
## Sync only subfolders
```sh
bdrive init --shared wiki
bdrive init --shared wiki,docs # several subfolders, one project
```
This is the right shape inside a code repository: sync `wiki/` or `docs/` and
leave the source tree alone. The agent gets a knowledge folder; the code stays
in git where it belongs. The interactive `bdrive init` asks the same question.
in git where it belongs. `--shared` takes one folder or several — comma-separated
or repeated — and they all join the same project, with one membership and one
permission set. The interactive `bdrive init` asks the same question.
The result lands in `.bdrive/config.json` as an include list:
@@ -29,6 +32,23 @@ The result lands in `.bdrive/config.json` as an include list:
"remote": "https://drive.example.com/p/p-7f3a2c91", "include": ["wiki/"] }
```
## Change the scope later
`bdrive scope` shows the include list; `scope add` / `scope rm` edit it — no
JSON editing, and the running daemon applies the change within seconds:
```sh
bdrive scope # what syncs now
bdrive scope add notes # also sync ./notes
bdrive scope rm docs # stop syncing ./docs
```
Removing a folder stops syncing it but deletes nothing — local files stay, and
the hub keeps everything already synced (the same
[non-destructive rule](#opting-out-is-non-destructive) as `.bdriveignore`).
Removing the *last* entry is refused, because an empty include list means the
whole folder syncs; if you want to stop syncing entirely, that's `bdrive stop`.
:::tip
A `--shared` mount is also where the two-file
[`AGENTS.md` pattern](/guides/shared-agent-memory/) earns its keep — the synced
@@ -58,9 +58,11 @@ Init writes `.bdrive/config.json`, seeds a starter `.bdriveignore`
in yet? It runs the login flow first.
:::tip[Working inside a repository]
Sync a subfolder rather than the repo root: `bdrive init --shared docs`. Git
directories are never synced (per-file last-writer-wins would corrupt a
repository), but a narrower scope keeps the sync surface honest.
Sync subfolders rather than the repo root: `bdrive init --shared docs` (or
several at once: `--shared wiki,docs`). Git directories are never synced
(per-file last-writer-wins would corrupt a repository), but a narrower scope
keeps the sync surface honest. Adjust it later with `bdrive scope add`/`rm`
see [Scoping the folder](/guides/scoping/).
:::
## 3. Work normally
+3 -1
View File
@@ -13,6 +13,7 @@ One binary, `bdrive` — the CLI, the sync daemon, and the web server.
| `bdrive logout` | Sign this device out — clear the saved token and account. `--forget` also drops the remembered server |
| `bdrive init [folder]` | Create or connect a project and start syncing. Interactive on a TTY; flags (`--name`, `--project`, `--shared`, `--yes`) for scripts. Re-run to resume |
| `bdrive stop [folder]` | Stop syncing — daemon and agent sync hooks both pause. Files stay on disk; `bdrive init` resumes |
| `bdrive scope [add\|rm <dirs...>]` | Show or change which subfolders sync — the include list set by `init --shared`. Run from the mount root; the daemon picks changes up in seconds. `rm` stops syncing a folder but deletes nothing, locally or on the hub |
| `bdrive url [path]` | Internal hub link for a file or folder — sign-in and membership required. `--sync` pushes first; no argument gives the project home. Computed locally |
| `bdrive share <file>` | Public URL for a synced file. `--list`, `--revoke`, `--expires` |
| `bdrive sync [folder]` | Run one sync cycle now. Refuses folders this device never `init`ed and folders paused by `bdrive stop`. `--note <text>` stamps session context onto changes; `--note-ttl` (default 30m) bounds it. `--hook <label>` is agent-hook plumbing |
@@ -33,7 +34,8 @@ One binary, `bdrive` — the CLI, the sync daemon, and the web server.
The front door. Interactive on a TTY, with survey menus for create-new versus
connect-existing (showing a project list) and whole-folder versus
`--shared <dir>` (which becomes the include list). Full flag bypass with
`--shared <dirs>` (one or more subfolders, repeatable or comma-separated —
`--shared wiki,docs` — which become the include list). Full flag bypass with
`--name`, `--project`, `--shared`, `--yes`, and it never prompts without a TTY.
It runs the login flow first when there is no session, writes
@@ -18,7 +18,9 @@ plus project, remote, and include settings.
```
Written by `bdrive init` and safe to hand-edit — a running daemon picks changes
up automatically.
up automatically. The `include` list (which subfolders sync, set by
`init --shared`) has a friendlier editor: `bdrive scope add`/`rm` from the
mount root.
It is **never synced** and holds **no credentials**; the session token stays in
`~/.bdrive`.