mirror of
https://github.com/runbear-io/beardrive.git
synced 2026-08-25 08:08:08 +02:00
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>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
9c7fc34ac8
commit
5c6099177b
@@ -4,6 +4,19 @@ Notable changes per release. Format loosely follows
|
||||
[Keep a Changelog](https://keepachangelog.com/); BearDrive is pre-1.0, so
|
||||
minor versions may ship breaking changes (see [SemVer §4](https://semver.org/#spec-item-4)).
|
||||
|
||||
## Unreleased
|
||||
|
||||
- **`bdrive skill install`** — the binary now carries the `beardrive`
|
||||
skill and installs it into any agent that reads `SKILL.md`
|
||||
(`~/.claude|.codex|.gemini|.hermes/skills/beardrive/`), idempotently;
|
||||
bare `bdrive skill` prints the detection table.
|
||||
- **Hub install guide, Codex and Hermes tabs: one paste, no terminal** —
|
||||
the same shape as the Claude tab. The pasted prompt has the agent install
|
||||
the CLI, keep the skill, sign in (`login --device`, so it can relay the
|
||||
code instead of hoping a browser opened), `bdrive init`, and
|
||||
`bdrive hooks install` — the step hand-copied setups routinely skipped.
|
||||
The plain commands moved into an "or run it yourself" fallback.
|
||||
|
||||
## v0.8.0 — 2026-07-16
|
||||
|
||||
- **Gated links on every mentioned file path**: Claude Code's turn-start
|
||||
|
||||
@@ -143,6 +143,7 @@ hub's own storage, never something a syncing client points at directly:
|
||||
| `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 |
|
||||
| `bdrive hooks [install]` | Register turn-boundary sync hooks with detected agent platforms (Claude Code, Codex, Gemini CLI, Hermes) — pull each turn, push after edits, session-note stamping, agent-read tracking; idempotent (`--agent` overrides detection) |
|
||||
| `bdrive skill [install]` | Install the `beardrive` skill into detected agent platforms (`~/.codex/skills/beardrive/SKILL.md` and friends) so the agent can do the setup itself — sign in, `bdrive init`, and register the sync hooks; idempotent (`--agent` overrides detection) |
|
||||
| `bdrive read-log [folder]` | Hook plumbing: queue agent file reads from a hook event (JSON on stdin) for the hub's read heatmap — native reads, grep matches, and files named in shell commands; drained on the next sync. Registered by `bdrive hooks install` |
|
||||
| `bdrive status [folder]` | Projects, daemon state, pending changes |
|
||||
| `bdrive log [folder] [-p path] [-n N]` | Change history: account, device, time, file |
|
||||
@@ -431,6 +432,38 @@ The plugin sets up everything at once:
|
||||
selective sync, and troubleshooting. Working in a clone of this repo
|
||||
picks the same skill up automatically via `.claude/skills/`.
|
||||
|
||||
## Other agents: Codex, Gemini CLI, Hermes
|
||||
|
||||
No terminal needed here either — the setup is one paste. Start the agent in
|
||||
the folder you want the files and give it:
|
||||
|
||||
```
|
||||
Set up BearDrive in this folder.
|
||||
1. If `bdrive` is missing, install it: brew install runbear-io/tap/beardrive
|
||||
(no Homebrew? grab the release binary for this OS/arch from
|
||||
https://github.com/runbear-io/beardrive/releases)
|
||||
2. bdrive skill install # so you know the CLI next time
|
||||
3. bdrive login --device https://your-hub # show me the code and the URL
|
||||
4. bdrive init --project <project-id>
|
||||
5. bdrive hooks install # don't skip this - it's what syncs every turn
|
||||
Then tell me what got set up.
|
||||
```
|
||||
|
||||
The commands ride inside the prompt because these agents ship no BearDrive
|
||||
knowledge — but the user copies one thing, and the agent handles every
|
||||
deviation (already installed, no Homebrew, sign-in, wrong folder). Step 2 is
|
||||
the durable part: `SKILL.md` is a cross-agent format, and `bdrive skill
|
||||
install` writes the very skill the Claude plugin ships to each detected
|
||||
platform's user-level skills directory (`~/.codex/skills/beardrive/SKILL.md`,
|
||||
`~/.gemini/…`, `~/.hermes/…`, `~/.claude/…`), so from then on "share this
|
||||
file" or "what changed?" just works. Step 5 is the one people skip when they
|
||||
copy commands by hand, which is exactly why the agent runs it.
|
||||
|
||||
A project's home page in the web UI shows this with the hub URL and project
|
||||
id already filled in (plus the plain-terminal version). `bdrive skill` and
|
||||
`bdrive hooks` print what's set up on this machine; re-run either after a CLI
|
||||
upgrade to refresh.
|
||||
|
||||
## How it works
|
||||
|
||||
```
|
||||
|
||||
@@ -39,6 +39,7 @@ everything keeps working offline; changes sync when the remote is reachable.`,
|
||||
syncCmd(),
|
||||
readLogCmd(),
|
||||
hooksCmd(),
|
||||
skillCmd(),
|
||||
statusCmd(),
|
||||
logCmd(),
|
||||
webCmd(),
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/runbear-io/beardrive/internal/agentskills"
|
||||
)
|
||||
|
||||
// bdrive skill — install the `beardrive` skill into whatever agent platforms
|
||||
// the user works with (Claude Code, Codex, Gemini CLI, Hermes), so the agent
|
||||
// can do the setup itself: sign in, `bdrive init`, and — the part people miss
|
||||
// when they copy commands by hand — `bdrive hooks install`.
|
||||
func skillCmd() *cobra.Command {
|
||||
c := &cobra.Command{
|
||||
Use: "skill",
|
||||
Short: "Show which AI agent platforms have the beardrive skill installed",
|
||||
Args: cobra.NoArgs,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
folder, err := absFolder(nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
detected := map[string]bool{}
|
||||
for _, a := range agentskills.Detect(folder) {
|
||||
detected[a] = true
|
||||
}
|
||||
for _, a := range agentskills.Agents {
|
||||
state := "not detected"
|
||||
if detected[a] {
|
||||
state = "detected, skill not installed"
|
||||
if agentskills.Installed(a) {
|
||||
state = "skill installed"
|
||||
}
|
||||
}
|
||||
fmt.Printf(" %-8s %-32s %s\n", a, state, agentskills.Path(a))
|
||||
}
|
||||
fmt.Println("\ninstall with: bdrive skill install [--agent claude,codex,gemini,hermes]")
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var agentsFlag string
|
||||
install := &cobra.Command{
|
||||
Use: "install [folder]",
|
||||
Short: "Install the beardrive skill for detected agent platforms (or --agent list)",
|
||||
Long: "Writes the beardrive skill to each agent's user-level skills directory\n" +
|
||||
"(~/.codex/skills/beardrive/SKILL.md and friends), so the agent knows the\n" +
|
||||
"CLI everywhere — ask it to set up a folder and it runs login, init, and\n" +
|
||||
"`bdrive hooks install` for you. Re-running refreshes the copy shipped\n" +
|
||||
"with this binary.",
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
folder, err := absFolder(args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var agents []string
|
||||
if agentsFlag != "" && agentsFlag != "auto" {
|
||||
agents = strings.Split(agentsFlag, ",")
|
||||
}
|
||||
results, err := agentskills.Install(folder, agents)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(results) == 0 {
|
||||
fmt.Println("no agent platforms detected (looked for .claude/, .codex/, .gemini/, .hermes/ here or in ~)")
|
||||
fmt.Println("pick explicitly: bdrive skill install --agent claude,codex,gemini,hermes")
|
||||
return nil
|
||||
}
|
||||
for _, r := range results {
|
||||
state := "already current"
|
||||
if r.Changed {
|
||||
state = "installed"
|
||||
}
|
||||
fmt.Printf(" %-8s %s → %s\n", r.Agent, state, r.Path)
|
||||
}
|
||||
fmt.Println("\nnow ask your agent to set up the folder — it will run init and register sync hooks")
|
||||
return nil
|
||||
},
|
||||
}
|
||||
install.Flags().StringVar(&agentsFlag, "agent", "auto", "comma-separated platforms (claude,codex,gemini,hermes) or auto")
|
||||
c.AddCommand(install)
|
||||
return c
|
||||
}
|
||||
@@ -66,9 +66,11 @@ as bearer tokens. For containers, the repo ships a `Dockerfile`
|
||||
sync only that subfolder.
|
||||
3. Invite a teammate: sidebar footer → **Manage** → **New invite** —
|
||||
the join link both creates their account and adds them to your org.
|
||||
4. Connect agents: the project's home page in the web UI shows
|
||||
copy-paste setup for Claude Code/Cowork, Hermes, and Codex; or run
|
||||
`bdrive hooks install` in the folder.
|
||||
4. Connect agents: the project's home page in the web UI shows one-paste
|
||||
setup for Claude Code/Cowork, Hermes, and Codex — hub URL and project
|
||||
id already filled in. Teammates paste it into their own agent, which
|
||||
installs the CLI, keeps the beardrive skill (`bdrive skill install`),
|
||||
signs in, mounts the project, and registers the sync hooks.
|
||||
|
||||
## Authentication reference
|
||||
|
||||
@@ -147,7 +149,8 @@ drivers are pure Go, so the binary stays a CGO-free static build.
|
||||
`brew upgrade beardrive` (clients and hub are the same binary — keep
|
||||
them roughly in step; the sync protocol is append-only journals + blobs,
|
||||
which old clients read forward). After upgrading a client, re-run
|
||||
`bdrive hooks install` once per project to pick up any hook improvements.
|
||||
`bdrive hooks install` once per project to pick up any hook improvements,
|
||||
and `bdrive skill install` once per machine to refresh the agent skill.
|
||||
|
||||
## Backup
|
||||
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
// Package agentskills installs the `beardrive` skill into the agent
|
||||
// platforms a user works with, so their agent knows how to drive the CLI —
|
||||
// including running `bdrive init` and `bdrive hooks install` itself, which is
|
||||
// how hooks end up registered correctly without the user hand-copying
|
||||
// commands.
|
||||
//
|
||||
// SKILL.md is a cross-agent standard: every supported platform discovers
|
||||
// skills the same way — a directory per skill under the platform's config
|
||||
// dir, holding a SKILL.md with `name` + `description` frontmatter:
|
||||
//
|
||||
// claude ~/.claude/skills/beardrive/SKILL.md
|
||||
// codex ~/.codex/skills/beardrive/SKILL.md
|
||||
// gemini ~/.gemini/skills/beardrive/SKILL.md
|
||||
// hermes ~/.hermes/skills/beardrive/SKILL.md
|
||||
//
|
||||
// Installs are user-level on purpose: the skill is about the CLI, not about
|
||||
// one folder, and a synced project folder should never carry it. The skill
|
||||
// is the binary's own copy (embedded at build time), so upgrading bdrive and
|
||||
// re-running install refreshes it — writes are idempotent and report whether
|
||||
// anything changed.
|
||||
package agentskills
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/runbear-io/beardrive/internal/store"
|
||||
"github.com/runbear-io/beardrive/plugin"
|
||||
)
|
||||
|
||||
// Agents is the supported platforms, in the order they are reported.
|
||||
var Agents = []string{"claude", "codex", "gemini", "hermes"}
|
||||
|
||||
// Result reports what Install did for one agent platform.
|
||||
type Result struct {
|
||||
Agent string
|
||||
Path string // SKILL.md written (or already current)
|
||||
Changed bool // false = the installed copy already matched
|
||||
}
|
||||
|
||||
// Path returns where an agent reads (or would read) the beardrive skill.
|
||||
// Empty if the agent is unknown or the home directory is undiscoverable.
|
||||
func Path(agent string) string {
|
||||
if !supported(agent) {
|
||||
return ""
|
||||
}
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil || home == "" {
|
||||
return ""
|
||||
}
|
||||
return filepath.Join(home, "."+agent, "skills", "beardrive", "SKILL.md")
|
||||
}
|
||||
|
||||
// Detect reports which agent platforms are in use, judged — like
|
||||
// agenthooks.Detect — by their config dirs existing in the project or the
|
||||
// home directory.
|
||||
func Detect(folder string) []string {
|
||||
home, _ := os.UserHomeDir()
|
||||
var found []string
|
||||
for _, a := range Agents {
|
||||
dir := "." + a
|
||||
if dirExists(filepath.Join(folder, dir)) || (home != "" && dirExists(filepath.Join(home, dir))) {
|
||||
found = append(found, a)
|
||||
}
|
||||
}
|
||||
return found
|
||||
}
|
||||
|
||||
// Installed reports whether an agent already has the current skill.
|
||||
func Installed(agent string) bool {
|
||||
path := Path(agent)
|
||||
if path == "" {
|
||||
return false
|
||||
}
|
||||
data, err := os.ReadFile(path)
|
||||
return err == nil && string(data) == plugin.SkillMD
|
||||
}
|
||||
|
||||
// Install writes the skill for the given agents ("auto"/empty = every
|
||||
// detected platform). An outdated copy is overwritten — the file is ours.
|
||||
func Install(folder string, agents []string) ([]Result, error) {
|
||||
if len(agents) == 0 || (len(agents) == 1 && agents[0] == "auto") {
|
||||
agents = Detect(folder)
|
||||
}
|
||||
var out []Result
|
||||
for _, a := range agents {
|
||||
if !supported(a) {
|
||||
return out, fmt.Errorf("unknown agent %q (supported: %s)", a, strings.Join(Agents, ", "))
|
||||
}
|
||||
path := Path(a)
|
||||
if path == "" {
|
||||
return out, fmt.Errorf("%s: cannot locate home directory", a)
|
||||
}
|
||||
if Installed(a) {
|
||||
out = append(out, Result{Agent: a, Path: path})
|
||||
continue
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||
return out, fmt.Errorf("%s: %w", a, err)
|
||||
}
|
||||
if err := store.WriteFileAtomic(path, []byte(plugin.SkillMD), 0o644); err != nil {
|
||||
return out, fmt.Errorf("%s: %w", a, err)
|
||||
}
|
||||
out = append(out, Result{Agent: a, Path: path, Changed: true})
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func supported(agent string) bool {
|
||||
for _, a := range Agents {
|
||||
if a == agent {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func dirExists(p string) bool {
|
||||
fi, err := os.Stat(p)
|
||||
return err == nil && fi.IsDir()
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package agentskills
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/runbear-io/beardrive/plugin"
|
||||
)
|
||||
|
||||
func TestDetect(t *testing.T) {
|
||||
home := t.TempDir()
|
||||
t.Setenv("HOME", home)
|
||||
folder := t.TempDir()
|
||||
|
||||
if got := Detect(folder); len(got) != 0 {
|
||||
t.Fatalf("nothing configured, detected %v", got)
|
||||
}
|
||||
os.MkdirAll(filepath.Join(folder, ".codex"), 0o755) // project-level
|
||||
os.MkdirAll(filepath.Join(home, ".hermes"), 0o755) // user-level
|
||||
if got := strings.Join(Detect(folder), ","); got != "codex,hermes" {
|
||||
t.Fatalf("detected %q, want codex,hermes", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInstallWritesSkillPerAgent(t *testing.T) {
|
||||
home := t.TempDir()
|
||||
t.Setenv("HOME", home)
|
||||
|
||||
results, err := Install(t.TempDir(), []string{"codex", "hermes"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(results) != 2 {
|
||||
t.Fatalf("got %d results, want 2", len(results))
|
||||
}
|
||||
for _, r := range results {
|
||||
if !r.Changed {
|
||||
t.Fatalf("%s: fresh install reported unchanged", r.Agent)
|
||||
}
|
||||
data, err := os.ReadFile(r.Path)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: %v", r.Agent, err)
|
||||
}
|
||||
if string(data) != plugin.SkillMD {
|
||||
t.Fatalf("%s: written skill differs from the embedded one", r.Agent)
|
||||
}
|
||||
// Frontmatter is what makes a SKILL.md discoverable on every platform.
|
||||
if !strings.HasPrefix(string(data), "---\nname: beardrive\n") {
|
||||
t.Fatalf("%s: skill lacks name frontmatter", r.Agent)
|
||||
}
|
||||
}
|
||||
if got := results[0].Path; got != filepath.Join(home, ".codex", "skills", "beardrive", "SKILL.md") {
|
||||
t.Fatalf("codex path = %s", got)
|
||||
}
|
||||
|
||||
// Re-running is idempotent...
|
||||
results, err = Install(t.TempDir(), []string{"codex"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if results[0].Changed {
|
||||
t.Fatal("second install reported a change")
|
||||
}
|
||||
if !Installed("codex") {
|
||||
t.Fatal("Installed() false right after installing")
|
||||
}
|
||||
|
||||
// ...but a stale copy is refreshed to the binary's own.
|
||||
os.WriteFile(results[0].Path, []byte("--- old skill ---\n"), 0o644)
|
||||
if Installed("codex") {
|
||||
t.Fatal("Installed() true for a stale copy")
|
||||
}
|
||||
results, _ = Install(t.TempDir(), []string{"codex"})
|
||||
if !results[0].Changed {
|
||||
t.Fatal("stale copy was not refreshed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestInstallAutoDetects(t *testing.T) {
|
||||
home := t.TempDir()
|
||||
t.Setenv("HOME", home)
|
||||
os.MkdirAll(filepath.Join(home, ".claude"), 0o755)
|
||||
|
||||
results, err := Install(t.TempDir(), nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(results) != 1 || results[0].Agent != "claude" {
|
||||
t.Fatalf("auto-detect installed %v", results)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInstallUnknownAgent(t *testing.T) {
|
||||
t.Setenv("HOME", t.TempDir())
|
||||
if _, err := Install(t.TempDir(), []string{"cursor"}); err == nil {
|
||||
t.Fatal("unknown agent accepted")
|
||||
}
|
||||
}
|
||||
@@ -36,15 +36,25 @@ test("claude tab: plugin flow with real hub origin and project id, no raw CLI",
|
||||
await expect(page.locator(".gd-body")).toContainText("Cowork");
|
||||
});
|
||||
|
||||
test("codex tab keeps the full CLI flow", async ({ page }) => {
|
||||
test("codex tab is one paste that hands the whole setup to the agent", async ({ page }) => {
|
||||
await login(page);
|
||||
const pid = await wikiId(page);
|
||||
await page.click('.gd-tab[data-key="codex"]');
|
||||
const codes = await page.$$eval(".gd-code code", (els) => els.map((e) => e.textContent).join("\n"));
|
||||
expect(codes).toContain("brew install runbear-io/tap/beardrive");
|
||||
expect(codes).toContain("bdrive login http://localhost:8993");
|
||||
expect(codes).toContain(`bdrive init --project ${pid}`);
|
||||
expect(codes).toContain("bdrive hooks install --agent codex");
|
||||
await expect(page.locator(".gd-step")).toHaveCount(1);
|
||||
const prompt = await page.$$eval(".gd-step .gd-code code", (els) =>
|
||||
els.map((e) => e.textContent).join("\n"),
|
||||
);
|
||||
// Everything the agent needs rides inside the pasted prompt.
|
||||
expect(prompt).toContain("brew install runbear-io/tap/beardrive");
|
||||
expect(prompt).toContain("bdrive skill install --agent codex");
|
||||
expect(prompt).toContain("bdrive login --device http://localhost:8993");
|
||||
expect(prompt).toContain(`bdrive init --project ${pid}`);
|
||||
expect(prompt).toContain("bdrive hooks install");
|
||||
const manual = await page.$$eval(".gd-manual .gd-code code", (els) =>
|
||||
els.map((e) => e.textContent).join("\n"),
|
||||
);
|
||||
expect(manual).toContain("bdrive login http://localhost:8993");
|
||||
expect(manual).toContain("bdrive hooks install --agent codex");
|
||||
await page.click('.gd-tab[data-key="claude"]');
|
||||
});
|
||||
|
||||
|
||||
@@ -10,9 +10,9 @@ import { copyText } from "../util";
|
||||
interface GuideAgent {
|
||||
key: string;
|
||||
label: string;
|
||||
hook?: string;
|
||||
note?: string;
|
||||
extra?: string;
|
||||
agent?: string; // --agent value for `bdrive skill install`
|
||||
skillDir?: string; // where that agent reads the skill from
|
||||
extra?: string; // platform-specific caveat after the last step
|
||||
}
|
||||
|
||||
const GUIDE_AGENTS: GuideAgent[] = [
|
||||
@@ -20,19 +20,17 @@ const GUIDE_AGENTS: GuideAgent[] = [
|
||||
{
|
||||
key: "hermes",
|
||||
label: "Hermes",
|
||||
hook: "hermes",
|
||||
note:
|
||||
"Registers BearDrive's hooks in Hermes's config: pull before every turn, push after edits " +
|
||||
"with a session note, and report file reads to Insights.",
|
||||
agent: "hermes",
|
||||
skillDir: "~/.hermes/skills/beardrive/",
|
||||
},
|
||||
{
|
||||
key: "codex",
|
||||
label: "Codex",
|
||||
hook: "codex",
|
||||
note: "Registers hooks in .codex/hooks.json.",
|
||||
agent: "codex",
|
||||
skillDir: "~/.codex/skills/beardrive/",
|
||||
extra:
|
||||
"Run /hooks inside Codex once to trust the project's .codex layer — after that every turn " +
|
||||
"pulls, edits push automatically, and reads are reported to Insights.",
|
||||
"Codex asks once to trust the project's .codex hooks layer — answer yes (or run /hooks) and " +
|
||||
"from then on every turn pulls, edits push automatically, and reads are reported to Insights.",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -71,35 +69,69 @@ function guideSteps(agent: GuideAgent, project: Project): Step[] {
|
||||
];
|
||||
}
|
||||
|
||||
const slug =
|
||||
(project.name || "project").toLowerCase().replace(/[^a-z0-9._-]+/g, "-") || "project";
|
||||
// Every other agent: one paste, no terminal — the same shape as the Claude
|
||||
// tab. The commands ride INSIDE the prompt because these agents ship no
|
||||
// BearDrive knowledge (Claude's tab can be terse only because the plugin
|
||||
// carries it); the user still copies one thing, and the agent handles every
|
||||
// deviation — already installed, no Homebrew, sign-in, wrong folder. Step 2
|
||||
// of the prompt installs the skill, so every later session is conversational.
|
||||
return [
|
||||
{
|
||||
title: "Install the BearDrive CLI",
|
||||
desc: "One static binary. Homebrew on macOS and Linux; releases and `go install` also work.",
|
||||
code: "brew install runbear-io/tap/beardrive",
|
||||
},
|
||||
{
|
||||
title: "Sign in to this hub",
|
||||
title: "Paste this into " + agent.label,
|
||||
desc:
|
||||
"Opens the browser once and stores a device token on this machine — the synced folder itself never holds credentials.",
|
||||
code: "bdrive login " + origin,
|
||||
},
|
||||
{
|
||||
title: "Mount the project into a local folder",
|
||||
desc:
|
||||
"Run it where you want the files. An existing folder works too — contents merge, and re-running init later (or after moving the folder) just resumes.",
|
||||
code: "mkdir -p ~/" + slug + " && cd ~/" + slug + "\nbdrive init --project " + pid,
|
||||
},
|
||||
{
|
||||
title: "Connect " + agent.label,
|
||||
desc: agent.note,
|
||||
code: "bdrive hooks install --agent " + agent.hook,
|
||||
extra: agent.extra,
|
||||
"Start " +
|
||||
agent.label +
|
||||
" in the folder where you want the files (an existing folder works too — contents merge), " +
|
||||
"then paste:",
|
||||
code: setupPrompt(agent, project),
|
||||
extra:
|
||||
"Approve the shell commands when it asks. It installs the CLI, signs this machine in (it " +
|
||||
"hands you a code and a URL — the folder itself never holds credentials), mounts the " +
|
||||
"project, and registers the sync hooks: pull before every turn, push after edits stamped " +
|
||||
"with the session that made them, file reads into Insights. It also keeps the beardrive " +
|
||||
"skill in " +
|
||||
agent.skillDir +
|
||||
", so from here on you can just ask." + (agent.extra ? " " + agent.extra : ""),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
// The prompt the user pastes. Numbered, exact commands: an agent with no
|
||||
// BearDrive knowledge follows this reliably, and knows what to do when a step
|
||||
// is already done. `login --device` on purpose — a browser-callback sign-in is
|
||||
// invisible to an agent mid-turn, while the device flow gives it a code and a
|
||||
// URL it can hand back in chat.
|
||||
function setupPrompt(agent: GuideAgent, project: Project): string {
|
||||
return [
|
||||
"Set up BearDrive in this folder.",
|
||||
"1. If `bdrive` is missing, install it: brew install runbear-io/tap/beardrive",
|
||||
" (no Homebrew? grab the release binary for this OS/arch from",
|
||||
" https://github.com/runbear-io/beardrive/releases)",
|
||||
"2. bdrive skill install --agent " + agent.agent + " # so you know the CLI next time",
|
||||
"3. bdrive login --device " + window.location.origin + " # show me the code and the URL",
|
||||
"4. bdrive init --project " + project.id,
|
||||
"5. bdrive hooks install # don't skip this - it's what syncs every turn",
|
||||
"Then tell me what got set up.",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
// For anyone who would rather not hand the setup to an agent. Skipping
|
||||
// `hooks install` is the one thing that silently costs you turn-boundary
|
||||
// syncing, so it is spelled out here.
|
||||
function manualCommands(agent: GuideAgent, project: Project): string {
|
||||
return (
|
||||
"brew install runbear-io/tap/beardrive" +
|
||||
"\nbdrive skill install --agent " +
|
||||
agent.agent +
|
||||
"\nbdrive login " +
|
||||
window.location.origin +
|
||||
"\nbdrive init --project " +
|
||||
project.id +
|
||||
"\nbdrive hooks install --agent " +
|
||||
agent.agent
|
||||
);
|
||||
}
|
||||
|
||||
function savedAgent(): string {
|
||||
try {
|
||||
return localStorage.getItem("bdrive-guide-agent") || "claude";
|
||||
@@ -113,6 +145,7 @@ export function ConnectGuide({ project }: { project: Project }) {
|
||||
// A stale saved key (e.g. a tab that no longer exists) falls back to the
|
||||
// first tab rather than rendering nothing.
|
||||
const agent = GUIDE_AGENTS.find((a) => a.key === agentKey) || GUIDE_AGENTS[0];
|
||||
const steps = guideSteps(agent, project);
|
||||
|
||||
return (
|
||||
<div className="guide">
|
||||
@@ -142,10 +175,11 @@ export function ConnectGuide({ project }: { project: Project }) {
|
||||
))}
|
||||
</div>
|
||||
<div className="gd-body">
|
||||
{guideSteps(agent, project).map((s, i) => (
|
||||
<div className="gd-step" key={i}>
|
||||
{steps.map((s, i) => (
|
||||
<div className={"gd-step" + (steps.length > 1 ? "" : " gd-solo")} key={i}>
|
||||
<div className="gd-step-head">
|
||||
<span className="gd-num">{i + 1}</span>
|
||||
{/* A lone step is not a sequence — no "1." badge for it. */}
|
||||
{steps.length > 1 && <span className="gd-num">{i + 1}</span>}
|
||||
<span className="gd-step-title">{s.title}</span>
|
||||
</div>
|
||||
{s.desc && <p className="gd-desc">{s.desc}</p>}
|
||||
@@ -153,6 +187,16 @@ export function ConnectGuide({ project }: { project: Project }) {
|
||||
{s.extra && <p className="gd-desc gd-extra">{s.extra}</p>}
|
||||
</div>
|
||||
))}
|
||||
{agent.agent && (
|
||||
<details className="gd-manual">
|
||||
<summary>Or run it yourself</summary>
|
||||
<p className="gd-desc">
|
||||
Same result, in the folder you want the files. Don't skip the last line — the hooks
|
||||
are what keep every turn starting from the latest state.
|
||||
</p>
|
||||
<GuideCode code={manualCommands(agent, project)} />
|
||||
</details>
|
||||
)}
|
||||
<p className="gd-done">
|
||||
That's it — the folder now syncs on its own. Every agent turn starts from the latest
|
||||
state, edits appear here (and on every teammate's mount) within seconds, and what your
|
||||
|
||||
@@ -366,6 +366,12 @@ button, input, a.btn { font-family: inherit; }
|
||||
.gd-code { position: relative; margin: 6px 0 6px 32px; padding: 10px 72px 10px 12px; background: var(--bg-raise); border: 1px solid var(--border); border-radius: var(--r-card); font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 12.5px; line-height: 1.6; color: var(--text); overflow-x: auto; white-space: pre; }
|
||||
.gd-copy { position: absolute; top: 7px; right: 7px; font: inherit; font-family: inherit; font-size: 11px; font-weight: 600; padding: 3px 9px; border-radius: 6px; border: 1px solid var(--border-2); background: var(--bg-raise); color: var(--text-faint); cursor: pointer; box-shadow: -14px 0 12px -6px var(--bg-raise); }
|
||||
.gd-copy:hover { color: var(--accent-bright); border-color: var(--accent-dim); }
|
||||
/* A single unnumbered step has no badge to indent under. */
|
||||
.gd-solo .gd-desc, .gd-solo .gd-code { margin-left: 0; }
|
||||
.gd-manual { margin: 10px 0 0; }
|
||||
.gd-manual > summary { display: inline-block; font-size: 12.5px; font-weight: 600; color: var(--text-faint); cursor: pointer; padding: 4px 0; }
|
||||
.gd-manual > summary:hover { color: var(--text); }
|
||||
.gd-manual .gd-desc, .gd-manual .gd-code { margin-left: 0; }
|
||||
.gd-done { margin: 22px 0 8px; padding: 12px 14px; border: 1px solid var(--border); border-radius: var(--r-card); background: var(--bg-side); color: var(--text-faint); font-size: 13px; line-height: 1.5; }
|
||||
.home-insights { margin-top: 30px; padding-top: 22px; border-top: 1px solid var(--border); }
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+26
-22
File diff suppressed because one or more lines are too long
@@ -5,8 +5,8 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>BearDrive</title>
|
||||
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🐻</text></svg>">
|
||||
<script type="module" crossorigin src="/assets/index-QXAy2YSg.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-9WanC3kA.css">
|
||||
<script type="module" crossorigin src="/assets/index-BaEUl3mb.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-BZPW21Cg.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -133,6 +133,24 @@ Heads-up before installing: Hermes hooks are PER-USER (`~/.hermes/config.yaml`,
|
||||
outside the repo) — mention that when it's among the targets, and skip it
|
||||
unless the user actually uses Hermes.
|
||||
|
||||
If the user mentions teammates on other agents (Codex, Gemini CLI, Hermes),
|
||||
tell them those teammates need no terminal either — they paste one prompt
|
||||
into their own agent (the hub's project home page shows it filled in):
|
||||
|
||||
```
|
||||
Set up BearDrive in this folder.
|
||||
1. If `bdrive` is missing, install it: brew install runbear-io/tap/beardrive
|
||||
2. bdrive skill install # so you know the CLI next time
|
||||
3. bdrive login --device <hub-url> # show me the code and the URL
|
||||
4. bdrive init --project <project-id>
|
||||
5. bdrive hooks install # don't skip this - it's what syncs every turn
|
||||
```
|
||||
|
||||
Step 2 leaves the beardrive skill in that agent's skills dir
|
||||
(`~/.codex/skills/beardrive/` and friends) so their later sessions are
|
||||
conversational. Handing teammates loose commands is how the hooks step gets
|
||||
skipped.
|
||||
|
||||
## 6. Verify and summarize
|
||||
|
||||
Run `bdrive status` and confirm the daemon is running and pending is 0.
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
// Package plugin embeds the shipped agent assets — currently the `beardrive`
|
||||
// skill — so the CLI can install them into any agent that reads SKILL.md.
|
||||
// The file embedded here is the same one the Claude Code plugin ships
|
||||
// (plugin/skills/beardrive/SKILL.md): one canonical copy, no drift.
|
||||
package plugin
|
||||
|
||||
import _ "embed"
|
||||
|
||||
// SkillMD is the beardrive skill: YAML frontmatter (name + description) plus
|
||||
// the instructions body, the cross-agent SKILL.md format Claude Code, Codex,
|
||||
// Gemini CLI, and Hermes all read.
|
||||
//
|
||||
//go:embed skills/beardrive/SKILL.md
|
||||
var SkillMD string
|
||||
@@ -18,6 +18,7 @@ Use this skill whenever the user is working with the `bdrive` CLI: initializing
|
||||
| Stop syncing | `bdrive stop [<folder>]` (`--forget` also unregisters) |
|
||||
| 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 |
|
||||
| Record agent file reads (hook plumbing) | `bdrive read-log [<folder>]` — parses a hook event JSON from stdin and queues in-project reads locally (native reads, grep matches, and files named in shell commands); drained to the hub on the next sync as agent traffic in the read heatmap. Registered automatically by `bdrive hooks install`; rarely run by hand |
|
||||
| Mounts + daemon + pending state | `bdrive status [<folder>]` |
|
||||
| Change history | `bdrive log [<folder>] [-p path] [-n N]` |
|
||||
@@ -157,6 +158,40 @@ the binary); `--agent claude,codex,gemini,hermes` overrides
|
||||
detection; bare `bdrive hooks` prints the detection/registration table.
|
||||
Project-level configs ride the repo, so hooks reach the whole team.
|
||||
|
||||
**When a teammate is setting up on a non-Claude agent**, point them at
|
||||
`bdrive skill install` (see below) rather than a list of commands: the
|
||||
agent then runs `init` + `hooks install` itself, which is exactly the step
|
||||
hand-copied setups miss.
|
||||
|
||||
### Installing this skill on other agents
|
||||
|
||||
`SKILL.md` is a cross-agent format, and the bdrive binary carries this very
|
||||
file: `bdrive skill install [<folder>]` writes it to the user-level skills
|
||||
directory of every platform it detects — `~/.claude/skills/beardrive/`,
|
||||
`~/.codex/skills/beardrive/`, `~/.gemini/skills/beardrive/`,
|
||||
`~/.hermes/skills/beardrive/`. `--agent claude,codex,gemini,hermes`
|
||||
overrides detection; bare `bdrive skill` prints the table; re-running after
|
||||
a CLI upgrade refreshes an outdated copy (idempotent otherwise). Installs
|
||||
are user-level on purpose — the skill is about the CLI, not one folder, and
|
||||
a synced project folder should never carry it.
|
||||
|
||||
The intended flow for a new machine is one paste into that agent — the
|
||||
commands ride inside the prompt because it has no BearDrive knowledge yet:
|
||||
|
||||
```
|
||||
Set up BearDrive in this folder.
|
||||
1. If `bdrive` is missing, install it: brew install runbear-io/tap/beardrive
|
||||
2. bdrive skill install # so you know the CLI next time
|
||||
3. bdrive login --device <hub-url> # show me the code and the URL
|
||||
4. bdrive init --project <project-id>
|
||||
5. bdrive hooks install # don't skip this - it's what syncs every turn
|
||||
```
|
||||
|
||||
Use `login --device` when an agent is driving: a browser-callback sign-in is
|
||||
invisible to it mid-turn, while the device flow yields a code and URL it can
|
||||
hand back in chat. The hub's project home page renders this with the URL and
|
||||
id filled in.
|
||||
|
||||
### Read heat (who actually reads what)
|
||||
|
||||
Hubs aggregate reads per file — viewer opens and downloads count as human
|
||||
|
||||
Reference in New Issue
Block a user