Skip to content

Commit 80daa73

Browse files
committed
feat: skills installer codex
1 parent 8fe04b4 commit 80daa73

File tree

3 files changed

+317
-16
lines changed

3 files changed

+317
-16
lines changed

experimental/aitools/lib/agents/agents.go

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,14 @@ type Agent struct {
2323
// ProjectConfigDir is the config directory name relative to a project root
2424
// (e.g., ".claude"). Only used when SupportsProjectScope is true.
2525
ProjectConfigDir string
26+
// PreferSkillCopy when true, installs each skill as a full directory copy
27+
// instead of a symlink. Codex uses this so agents/openai.yaml and assets/
28+
// resolve reliably under its config dir.
29+
PreferSkillCopy bool
30+
// InstallAgentMetadata when true, includes agent-specific UI metadata
31+
// (agents/openai.yaml, assets/) during installation. Agents that don't
32+
// consume these files skip them to keep their skills dirs clean.
33+
InstallAgentMetadata bool
2634
}
2735

2836
// Detected returns true if the agent is installed on the system.
@@ -87,9 +95,11 @@ var Registry = []Agent{
8795
ProjectConfigDir: ".cursor",
8896
},
8997
{
90-
Name: "codex",
91-
DisplayName: "Codex CLI",
92-
ConfigDir: homeSubdir(".codex"),
98+
Name: "codex",
99+
DisplayName: "Codex CLI",
100+
ConfigDir: homeSubdir(".codex"),
101+
PreferSkillCopy: true,
102+
InstallAgentMetadata: true,
93103
},
94104
{
95105
Name: "opencode",

experimental/aitools/lib/installer/installer.go

Lines changed: 62 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ const (
2525
skillsRepoOwner = "databricks"
2626
skillsRepoName = "databricks-agent-skills"
2727
skillsRepoPath = "skills"
28-
defaultSkillsRepoRef = "v0.1.3"
28+
defaultSkillsRepoRef = "v0.1.4"
2929
)
3030

3131
// fetchFileFn is the function used to download individual skill files.
@@ -73,9 +73,20 @@ func FetchManifest(ctx context.Context) (*Manifest, error) {
7373
return src.FetchManifest(ctx, ref)
7474
}
7575

76+
// sharedFilePrefix marks files in the manifest that live at the repo root
77+
// rather than under skills/<name>/. The CLI strips this prefix when writing
78+
// to disk and adjusts the download URL accordingly.
79+
const sharedFilePrefix = "@root:"
80+
7681
func fetchSkillFile(ctx context.Context, ref, skillName, filePath string) ([]byte, error) {
77-
url := fmt.Sprintf("https://raw.githubusercontent.com/%s/%s/%s/%s/%s/%s",
78-
skillsRepoOwner, skillsRepoName, ref, skillsRepoPath, skillName, filePath)
82+
var url string
83+
if rootPath, ok := strings.CutPrefix(filePath, sharedFilePrefix); ok {
84+
url = fmt.Sprintf("https://raw.githubusercontent.com/%s/%s/%s/%s",
85+
skillsRepoOwner, skillsRepoName, ref, rootPath)
86+
} else {
87+
url = fmt.Sprintf("https://raw.githubusercontent.com/%s/%s/%s/%s/%s/%s",
88+
skillsRepoOwner, skillsRepoName, ref, skillsRepoPath, skillName, filePath)
89+
}
7990

8091
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
8192
if err != nil {
@@ -390,14 +401,33 @@ type installParams struct {
390401
ref string
391402
}
392403

404+
// agentMetadataDirs lists subdirectory prefixes that are agent-specific UI
405+
// metadata (marketplace icons, display names). Only agents with
406+
// InstallAgentMetadata=true receive these during a copy install.
407+
var agentMetadataDirs = []string{"agents", "assets"}
408+
409+
// isAgentMetadataPath reports whether rel (forward-slash separated) falls
410+
// under one of the agent metadata directories.
411+
func isAgentMetadataPath(rel string) bool {
412+
normalized := filepath.ToSlash(rel)
413+
for _, dir := range agentMetadataDirs {
414+
if normalized == dir || strings.HasPrefix(normalized, dir+"/") {
415+
return true
416+
}
417+
}
418+
return false
419+
}
420+
393421
func installSkillForAgents(ctx context.Context, skillName string, files []string, detectedAgents []*agents.Agent, params installParams) error {
394422
canonicalDir := filepath.Join(params.baseDir, skillName)
395423
if err := installSkillToDir(ctx, params.ref, skillName, canonicalDir, files); err != nil {
396424
return err
397425
}
398426

399427
// For project scope, always symlink. For global, symlink when multiple agents.
400-
useSymlinks := params.scope == ScopeProject || len(detectedAgents) > 1
428+
// Agents with PreferSkillCopy always get a full directory copy so
429+
// marketplace metadata under agents/ and assets/ stays under their config dir.
430+
symlinkEligible := params.scope == ScopeProject || len(detectedAgents) > 1
401431

402432
for _, agent := range detectedAgents {
403433
agentSkillDir, err := agentSkillsDirForScope(ctx, agent, params.scope, params.cwd)
@@ -413,7 +443,8 @@ func installSkillForAgents(ctx context.Context, skillName string, files []string
413443
continue
414444
}
415445

416-
if useSymlinks {
446+
useSymlink := symlinkEligible && !agent.PreferSkillCopy
447+
if useSymlink {
417448
symlinkTarget := canonicalDir
418449
// For project scope, use relative symlinks so they work for teammates.
419450
if params.scope == ScopeProject {
@@ -431,8 +462,10 @@ func installSkillForAgents(ctx context.Context, skillName string, files []string
431462
}
432463
log.Debugf(ctx, "Installed %q for %s (symlinked)", skillName, agent.DisplayName)
433464
} else {
434-
// Copy from canonical dir instead of re-downloading.
435-
if err := copyDir(canonicalDir, destDir); err != nil {
465+
// Copy from canonical dir. Skip agent metadata dirs for agents
466+
// that don't consume them.
467+
skipMetadata := !agent.InstallAgentMetadata
468+
if err := copyDirFiltered(canonicalDir, destDir, skipMetadata); err != nil {
436469
log.Warnf(ctx, "Failed to install for %s: %v", agent.DisplayName, err)
437470
continue
438471
}
@@ -506,7 +539,13 @@ func installSkillToDir(ctx context.Context, ref, skillName, destDir string, file
506539
return err
507540
}
508541

509-
destPath := filepath.Join(destDir, file)
542+
// Strip the @root: prefix so shared assets land at a local path
543+
// (e.g. "@root:assets/databricks.svg" → "assets/databricks.svg").
544+
destFile := file
545+
if rootPath, ok := strings.CutPrefix(file, sharedFilePrefix); ok {
546+
destFile = rootPath
547+
}
548+
destPath := filepath.Join(destDir, destFile)
510549

511550
if err := os.MkdirAll(filepath.Dir(destPath), 0o755); err != nil {
512551
return fmt.Errorf("failed to create directory: %w", err)
@@ -523,6 +562,13 @@ func installSkillToDir(ctx context.Context, ref, skillName, destDir string, file
523562

524563
// copyDir copies all files from src to dest, recreating the directory structure.
525564
func copyDir(src, dest string) error {
565+
return copyDirFiltered(src, dest, false)
566+
}
567+
568+
// copyDirFiltered copies files from src to dest. When skipAgentMetadata is
569+
// true, subdirectories matching agentMetadataDirs (agents/, assets/) are
570+
// skipped so non-Codex agents don't receive marketplace UI files.
571+
func copyDirFiltered(src, dest string, skipAgentMetadata bool) error {
526572
if err := os.RemoveAll(dest); err != nil {
527573
return fmt.Errorf("failed to remove existing path: %w", err)
528574
}
@@ -536,6 +582,14 @@ func copyDir(src, dest string) error {
536582
if err != nil {
537583
return err
538584
}
585+
586+
if skipAgentMetadata && rel != "." && isAgentMetadataPath(rel) {
587+
if info.IsDir() {
588+
return filepath.SkipDir
589+
}
590+
return nil
591+
}
592+
539593
target := filepath.Join(dest, rel)
540594

541595
if info.IsDir() {

0 commit comments

Comments
 (0)