Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .nextchanges/cli/aitools-goose.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
`databricks aitools install` now supports Goose, installing Databricks agent skills into its skills directory.
5 changes: 3 additions & 2 deletions cmd/aitools/install_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,8 +149,8 @@ func TestAgentChoicesOnlyOffersActionableAgents(t *testing.T) {
ctx := cmdio.MockDiscard(t.Context())

// Project scope: agents that support project-scoped skills are offered (Claude
// via plugin; Pi/Gemini via skills). User-only plugin agents and global-only
// files agents are not.
// via plugin; Pi/Gemini/Goose via skills). User-only plugin agents and
// global-only files agents are not.
choices := agentChoices(ctx, installer.ScopeProject, false)
var names []string
for _, c := range choices {
Expand All @@ -159,6 +159,7 @@ func TestAgentChoicesOnlyOffersActionableAgents(t *testing.T) {
assert.Contains(t, names, agents.NameClaudeCode)
assert.Contains(t, names, agents.NamePi)
assert.Contains(t, names, agents.NameGemini)
assert.Contains(t, names, agents.NameGoose)
assert.NotContains(t, names, agents.NameCursor)
assert.NotContains(t, names, agents.NameCodex)
assert.NotContains(t, names, agents.NameOpenCode)
Expand Down
2 changes: 2 additions & 0 deletions cmd/aitools/telemetry.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ func agentType(name string) protos.AitoolsAgentType {
return protos.AitoolsAgentTypePi
case agents.NameGemini:
return protos.AitoolsAgentTypeGemini
case agents.NameGoose:
return protos.AitoolsAgentTypeGoose
default:
return protos.AitoolsAgentTypeUnspecified
}
Expand Down
2 changes: 2 additions & 0 deletions cmd/aitools/update_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,7 @@ func TestUpdateProjectIncludesProjectSkillAgents(t *testing.T) {
// home-based detection would miss these, so update must use DetectProjectInstalled.
require.NoError(t, os.MkdirAll(filepath.Join(projectRoot, ".pi", "skills", "databricks-core"), 0o755))
require.NoError(t, os.MkdirAll(filepath.Join(projectRoot, ".gemini", "skills", "databricks-core"), 0o755))
require.NoError(t, os.MkdirAll(filepath.Join(projectRoot, ".goose", "skills", "databricks-core"), 0o755))

ctx := cmdio.MockDiscard(t.Context())
dir, err := installer.ProjectSkillsDir(ctx)
Expand Down Expand Up @@ -431,4 +432,5 @@ func TestUpdateProjectIncludesProjectSkillAgents(t *testing.T) {
require.NoError(t, cmd.Execute())
assert.Contains(t, names, agents.NamePi)
assert.Contains(t, names, agents.NameGemini)
assert.Contains(t, names, agents.NameGoose)
}
40 changes: 40 additions & 0 deletions libs/aitools/agents/agents.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ const (
NameAntigravity = "antigravity"
NamePi = "pi"
NameGemini = "gemini"
NameGoose = "goose"
)

// Databricks plugin identity, shared across the agents that ship a plugin.
Expand Down Expand Up @@ -246,6 +247,16 @@ var Registry = []*Agent{
// and installation_id is not reliable, so detection uses this Gemini-only file.
MandatoryFile: "projects.json",
},
{
Name: NameGoose,
DisplayName: "Goose",
ConfigDir: gooseConfigDir,
SupportsProjectScope: true,
ProjectConfigDir: ".goose",
Binary: "goose",
// Goose reads agent skills (SKILL.md) but has no databricks plugin, so it is
// skills-only (Plugin nil).
},
}

// piConfigDir returns Pi's agent config directory: PI_CODING_AGENT_DIR when set,
Expand Down Expand Up @@ -312,6 +323,35 @@ func openCodeConfigDir(ctx context.Context) (string, error) {
return filepath.Join(xdg, "opencode"), nil
}

// gooseConfigDir returns Goose's config directory, matching how Goose resolves it
// so skills land where it reads them, including under a relocated root. The Windows
// path keeps the legacy "Block" segment for backwards compatibility.
// See crates/goose/src/config/paths.rs (etcetera crate). https://block.github.io/goose/
func gooseConfigDir(ctx context.Context) (string, error) {
if root := env.Get(ctx, "GOOSE_PATH_ROOT"); filepath.IsAbs(root) {
return filepath.Join(root, "config"), nil
}
if runtime.GOOS == "windows" {
if appData := env.Get(ctx, "APPDATA"); appData != "" {
return filepath.Join(appData, "Block", "goose", "config"), nil
}
home, err := env.UserHomeDir(ctx)
if err != nil {
return "", err
}
return filepath.Join(home, "AppData", "Roaming", "Block", "goose", "config"), nil
}
home, err := env.UserHomeDir(ctx)
if err != nil {
return "", err
}
xdg := env.Get(ctx, "XDG_CONFIG_HOME")
if !filepath.IsAbs(xdg) {
xdg = filepath.Join(home, ".config")
}
return filepath.Join(xdg, "goose"), nil
}

// ByName returns the registry agent with the given name, or nil if not found.
func ByName(name string) *Agent {
for _, a := range Registry {
Expand Down
1 change: 1 addition & 0 deletions libs/aitools/agents/agents_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ func TestSkillsOnlyNamesMatchesRegistry(t *testing.T) {
// Skills-only agents (Plugin nil) are listed; plugin agents are not.
assert.Contains(t, names, "Pi")
assert.Contains(t, names, "Gemini CLI")
assert.Contains(t, names, "Goose")
assert.NotContains(t, names, "Claude Code")
for _, a := range Registry {
if a.Plugin != nil {
Expand Down
71 changes: 71 additions & 0 deletions libs/aitools/agents/detect_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,77 @@ func TestPiConfigDir(t *testing.T) {
})
}

func TestGooseConfigDir(t *testing.T) {
Comment thread
lennartkats-db marked this conversation as resolved.
ctx := t.Context()
home := t.TempDir()
t.Setenv("HOME", home)
t.Setenv("USERPROFILE", home)
t.Setenv("APPDATA", "")

t.Run("GOOSE_PATH_ROOT override wins and appends config", func(t *testing.T) {
root := t.TempDir()
t.Setenv("GOOSE_PATH_ROOT", root)
dir, err := gooseConfigDir(ctx)
require.NoError(t, err)
assert.Equal(t, filepath.Join(root, "config"), dir)
})

t.Run("relative GOOSE_PATH_ROOT is ignored", func(t *testing.T) {
// Goose only honors an absolute path root; a relative value falls through.
t.Setenv("GOOSE_PATH_ROOT", "relative/root")
t.Setenv("XDG_CONFIG_HOME", "")
dir, err := gooseConfigDir(ctx)
require.NoError(t, err)
if runtime.GOOS == "windows" {
assert.Equal(t, filepath.Join(home, "AppData", "Roaming", "Block", "goose", "config"), dir)
} else {
assert.Equal(t, filepath.Join(home, ".config", "goose"), dir)
}
})

t.Setenv("GOOSE_PATH_ROOT", "")

if runtime.GOOS == "windows" {
t.Run("honors APPDATA", func(t *testing.T) {
appData := t.TempDir()
t.Setenv("APPDATA", appData)
dir, err := gooseConfigDir(ctx)
require.NoError(t, err)
assert.Equal(t, filepath.Join(appData, "Block", "goose", "config"), dir)
})

t.Run("defaults to USERPROFILE when APPDATA is unset", func(t *testing.T) {
t.Setenv("APPDATA", "")
dir, err := gooseConfigDir(ctx)
require.NoError(t, err)
assert.Equal(t, filepath.Join(home, "AppData", "Roaming", "Block", "goose", "config"), dir)
})
return
}

t.Run("honors XDG_CONFIG_HOME", func(t *testing.T) {
xdg := t.TempDir()
t.Setenv("XDG_CONFIG_HOME", xdg)
dir, err := gooseConfigDir(ctx)
require.NoError(t, err)
assert.Equal(t, filepath.Join(xdg, "goose"), dir)
})

t.Run("ignores relative XDG_CONFIG_HOME", func(t *testing.T) {
t.Setenv("XDG_CONFIG_HOME", "relative/config")
dir, err := gooseConfigDir(ctx)
require.NoError(t, err)
assert.Equal(t, filepath.Join(home, ".config", "goose"), dir)
})

t.Run("defaults to ~/.config when XDG is unset", func(t *testing.T) {
t.Setenv("XDG_CONFIG_HOME", "")
dir, err := gooseConfigDir(ctx)
require.NoError(t, err)
assert.Equal(t, filepath.Join(home, ".config", "goose"), dir)
})
}

func TestGeminiConfigDir(t *testing.T) {
ctx := t.Context()
home := t.TempDir()
Expand Down
12 changes: 10 additions & 2 deletions libs/aitools/agents/registry_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package agents
import (
"os"
"path/filepath"
"runtime"
"testing"

"github.com/databricks/cli/libs/env"
Expand All @@ -15,6 +16,12 @@ func TestSkillAgentRegistryPaths(t *testing.T) {
cwd := t.TempDir()
ctx := env.WithUserHomeDir(t.Context(), home)
ctx = env.Set(ctx, "XDG_CONFIG_HOME", filepath.Join(home, ".config"))
ctx = env.Set(ctx, "APPDATA", "")

gooseGlobalDir := filepath.Join(home, ".config", "goose", "skills")
if runtime.GOOS == "windows" {
gooseGlobalDir = filepath.Join(home, "AppData", "Roaming", "Block", "goose", "config", "skills")
}

tests := []struct {
name string
Expand All @@ -25,6 +32,7 @@ func TestSkillAgentRegistryPaths(t *testing.T) {
}{
{NamePi, "pi", "Pi", filepath.Join(home, ".pi", "agent", "skills"), filepath.Join(cwd, ".pi", "skills")},
{NameGemini, "gemini", "Gemini CLI", filepath.Join(home, ".gemini", "skills"), filepath.Join(cwd, ".gemini", "skills")},
{NameGoose, "goose", "Goose", gooseGlobalDir, filepath.Join(cwd, ".goose", "skills")},
}

for _, tc := range tests {
Expand All @@ -46,7 +54,7 @@ func TestSkillAgentRegistryPaths(t *testing.T) {

func TestDetectProjectInstalled(t *testing.T) {
cwd := t.TempDir()
for _, name := range []string{NamePi, NameGemini} {
for _, name := range []string{NamePi, NameGemini, NameGoose} {
dir := filepath.Join(ByName(name).ProjectSkillsDir(cwd), "databricks-core")
require.NoError(t, os.MkdirAll(dir, 0o755))
}
Expand All @@ -57,5 +65,5 @@ func TestDetectProjectInstalled(t *testing.T) {
for _, a := range DetectProjectInstalled(cwd) {
names = append(names, a.Name)
}
assert.ElementsMatch(t, []string{NamePi, NameGemini}, names)
assert.ElementsMatch(t, []string{NamePi, NameGemini, NameGoose}, names)
}
1 change: 1 addition & 0 deletions libs/aitools/installer/installer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1031,6 +1031,7 @@ func TestSupportsProjectScopeSetCorrectly(t *testing.T) {
"antigravity": false,
"pi": true,
"gemini": true,
"goose": true,
}

for _, agent := range agents.Registry {
Expand Down
1 change: 1 addition & 0 deletions libs/telemetry/protos/aitools_install.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const (
AitoolsAgentTypeAntigravity AitoolsAgentType = "ANTIGRAVITY"
AitoolsAgentTypePi AitoolsAgentType = "PI"
AitoolsAgentTypeGemini AitoolsAgentType = "GEMINI_CLI"
AitoolsAgentTypeGoose AitoolsAgentType = "GOOSE"
)

// AitoolsInstallScope mirrors AitoolsInstallScope.Type in the databricks_cli
Expand Down
Loading