Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
23 changes: 6 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
[![npm version](https://img.shields.io/npm/v/simba-skills)](https://www.npmjs.com/package/simba-skills)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)

AI skills manager with a central store and symlink-based distribution across 14+ coding agents.
AI skills manager with a central store and symlink-based distribution across 17+ coding agents.

## Why Simba?

Expand Down Expand Up @@ -108,22 +108,11 @@ simba undo

## Supported Agents

| Agent | Global Path | Project Path |
|-------|-------------|--------------|
| Claude Code | `~/.claude/skills` | `.claude/skills` |
| Cursor | `~/.cursor/skills` | `.cursor/skills` |
| Codex | `~/.codex/skills` | `.codex/skills` |
| GitHub Copilot | `~/.copilot/skills` | `.github/skills` |
| Gemini CLI | `~/.gemini/skills` | `.gemini/skills` |
| Windsurf | `~/.codeium/windsurf/skills` | `.windsurf/skills` |
| Amp | `~/.config/agents/skills` | `.agents/skills` |
| Goose | `~/.config/goose/skills` | `.goose/skills` |
| OpenCode | `~/.config/opencode/skill` | `.opencode/skill` |
| Kilo Code | `~/.kilocode/skills` | `.kilocode/skills` |
| Roo Code | `~/.roo/skills` | `.roo/skills` |
| Antigravity | `~/.gemini/antigravity/skills` | `.agent/skills` |
| Clawdbot | `~/.clawdbot/skills` | `skills` |
| Droid | `~/.factory/skills` | `.factory/skills` |
Supports Claude Code, Codex, OpenCode, Cursor, Gemini CLI, GitHub Copilot, Amp, Kimi Code CLI, Replit, and 30+ others.

Includes agents using the `.agents/skills` universal standard, plus agent-specific paths.

See full agent definitions and paths in [`src/core/config-store.ts`](./src/core/config-store.ts).

## Architecture

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "simba-skills",
"version": "0.6.0",
"version": "0.6.1",
"description": "AI skills manager - central store with symlink-based distribution across 15+ coding agents",
"publishConfig": {
"access": "public"
Expand Down
29 changes: 24 additions & 5 deletions src/commands/assign.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,13 +56,32 @@ export default defineCommand({
// Get detected agents
const agentRegistry = new AgentRegistry(config.agents)
const detected = await agentRegistry.detectAgents()
const detectedAgents = Object.entries(detected).filter(([, a]) => a.detected)

const universalFallbackByProjectPath = new Map<string, string>()
for (const agent of Object.values(detected)) {
if (!agent.universal || !agent.detected) continue
if (!universalFallbackByProjectPath.has(agent.projectPath)) {
universalFallbackByProjectPath.set(agent.projectPath, expandPath(agent.globalPath))
}
}

const agentPaths: Record<string, string> = {}
for (const [id, agent] of detectedAgents) {
agentPaths[id] = expandPath(agent.globalPath)
for (const [id, agent] of Object.entries(detected)) {
if (agent.detected) {
agentPaths[id] = expandPath(agent.globalPath)
continue
}

if (agent.universal) {
const fallback = universalFallbackByProjectPath.get(agent.projectPath)
if (fallback) {
agentPaths[id] = fallback
}
}
}

const assignableAgents = Object.entries(detected).filter(([id]) => agentPaths[id])

// Interactive mode if args missing
let skill = args.skill as string | undefined
let agents: string[]
Expand All @@ -83,14 +102,14 @@ export default defineCommand({
}

if (!args.agents) {
if (detectedAgents.length === 0) {
if (assignableAgents.length === 0) {
console.log("No agents detected.")
return
}

const result = await p.multiselect({
message: "Select agents to assign to",
options: detectedAgents.map(([id, a]) => ({ value: id, label: a.name })),
options: assignableAgents.map(([id, a]) => ({ value: id, label: a.name })),
required: true,
})
if (p.isCancel(result)) process.exit(0)
Expand Down
29 changes: 25 additions & 4 deletions src/commands/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,28 @@ export interface AssignResult {
message?: string
}

function resolveAssignmentPath(
agentId: string,
detected: Record<string, import("../core/types").Agent>
): string | null {
const agent = detected[agentId]
if (!agent) return null

if (agent.detected) {
return expandPath(agent.globalPath)
}

if (!agent.universal) {
return null
}

const fallback = Object.values(detected).find(
(candidate) => candidate.universal && candidate.detected && candidate.projectPath === agent.projectPath
)

return fallback ? expandPath(fallback.globalPath) : null
}

/** Detect agents and create symlinks for each skill's assignments */
export async function assignSkillsToAgents(
registry: { skills: Record<string, ManagedSkill> },
Expand All @@ -366,14 +388,13 @@ export async function assignSkillsToAgents(

const assignments = skill.assignments
for (const [agentId, assignment] of Object.entries(assignments)) {
const agent = detected[agentId]
if (!agent?.detected) {
const assignmentPath = resolveAssignmentPath(agentId, detected)
if (!assignmentPath) {
results.push({ skill: skillName, agent: agentId, status: "skipped", message: "agent not detected" })
continue
}

const agentSkillsDir = expandPath(agent.globalPath)
await skillsStore.assignSkill(skillName, agentSkillsDir, assignment)
await skillsStore.assignSkill(skillName, assignmentPath, assignment)
results.push({ skill: skillName, agent: agentId, status: "assigned" })
}
}
Expand Down
15 changes: 12 additions & 3 deletions src/commands/detect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,18 @@ export default defineCommand({

await configStore.save(config)

// Output results
console.log("\nDetected agents:")
for (const [id, agent] of Object.entries(detected)) {
// Output results grouped by universal/custom
const universal = Object.values(detected).filter(a => a.universal)
const custom = Object.values(detected).filter(a => !a.universal)

console.log("\nUniversal (.agents/skills):")
for (const agent of universal) {
const status = agent.detected ? "✓" : "─"
console.log(` ${status} ${agent.name}`)
}

console.log("\nCustom:")
for (const agent of custom) {
const status = agent.detected ? "✓" : "─"
console.log(` ${status} ${agent.name}`)
}
Expand Down
2 changes: 1 addition & 1 deletion src/commands/install.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ interface MarketplaceJson {
plugins?: MarketplacePlugin[]
}

const SKILL_DIRS = ["skills", ".claude/skills", ".cursor/skills", ".codex/skills"]
const SKILL_DIRS = ["skills", ".agents/skills", ".claude/skills", ".cursor/skills", ".codex/skills"]

interface SubmoduleInfo {
path: string
Expand Down
19 changes: 12 additions & 7 deletions src/core/agent-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,20 @@ export class AgentRegistry {
const results: Record<string, Agent> = {}

for (const [id, agent] of Object.entries(this.agents)) {
const globalPath = expandPath(agent.globalPath)
const parentDir = dirname(globalPath)
const configuredPaths = agent.detectPaths && agent.detectPaths.length > 0
? agent.detectPaths
: [agent.detectPath ?? dirname(agent.globalPath)]

let detected = false
try {
await access(parentDir)
detected = true
} catch {
detected = false
for (const path of configuredPaths) {
const detectionPath = expandPath(path)
try {
await access(detectionPath)
detected = true
break
} catch {
continue
}
}

results[id] = { ...agent, detected }
Expand Down
Loading