simba-skills 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,63 @@
1
+ import { defineCommand } from "citty"
2
+ import { ConfigStore } from "../core/config-store"
3
+ import { AgentRegistry } from "../core/agent-registry"
4
+ import { getConfigPath } from "../utils/paths"
5
+
6
+ export default defineCommand({
7
+ meta: {
8
+ name: "detect",
9
+ description: "Scan for installed agents and skills",
10
+ },
11
+ args: {
12
+ refresh: {
13
+ type: "boolean",
14
+ description: "Force rescan even if already detected",
15
+ default: false,
16
+ },
17
+ },
18
+ async run({ args }) {
19
+ const configStore = new ConfigStore(getConfigPath())
20
+ const config = await configStore.load()
21
+
22
+ const registry = new AgentRegistry(config.agents)
23
+ const detected = await registry.detectAgents()
24
+
25
+ // Update config with detection results
26
+ for (const [id, agent] of Object.entries(detected)) {
27
+ config.agents[id] = agent
28
+ }
29
+
30
+ // Scan skills from detected agents
31
+ let totalSkills = 0
32
+ for (const [id, agent] of Object.entries(detected)) {
33
+ if (!agent.detected) continue
34
+
35
+ const skills = await registry.listSkills(id)
36
+ totalSkills += skills.length
37
+
38
+ for (const skill of skills) {
39
+ config.skills[skill.name] = {
40
+ ...config.skills[skill.name],
41
+ ...skill,
42
+ agents: [
43
+ ...new Set([
44
+ ...(config.skills[skill.name]?.agents ?? []),
45
+ id,
46
+ ]),
47
+ ],
48
+ }
49
+ }
50
+ }
51
+
52
+ await configStore.save(config)
53
+
54
+ // Output results
55
+ console.log("\nDetected agents:")
56
+ for (const [id, agent] of Object.entries(detected)) {
57
+ const status = agent.detected ? "✓" : "─"
58
+ console.log(` ${status} ${agent.name}`)
59
+ }
60
+
61
+ console.log(`\nTotal skills found: ${totalSkills}`)
62
+ },
63
+ })
@@ -0,0 +1,169 @@
1
+ import { defineCommand } from "citty"
2
+ import { access } from "node:fs/promises"
3
+ import { join } from "node:path"
4
+ import * as p from "@clack/prompts"
5
+ import { ConfigStore } from "../core/config-store"
6
+ import { RegistryStore } from "../core/registry-store"
7
+ import { AgentRegistry } from "../core/agent-registry"
8
+ import { getConfigPath, getSkillsDir, getRegistryPath, expandPath } from "../utils/paths"
9
+ import { isSymlink, getSymlinkTarget, createSymlink, removeSymlink } from "../utils/symlinks"
10
+ import type { Agent } from "../core/types"
11
+
12
+ interface BrokenLink {
13
+ skill: string
14
+ agent: string
15
+ path: string
16
+ reason: string
17
+ }
18
+
19
+ interface RogueFile {
20
+ skill: string
21
+ agent: string
22
+ path: string
23
+ }
24
+
25
+ export interface DoctorResults {
26
+ healthy: string[]
27
+ broken: BrokenLink[]
28
+ rogue: RogueFile[]
29
+ }
30
+
31
+ export interface DoctorOptions {
32
+ skillsDir: string
33
+ registryPath: string
34
+ agents: Record<string, Agent>
35
+ }
36
+
37
+ export async function runDoctor(options: DoctorOptions): Promise<DoctorResults> {
38
+ const registryStore = new RegistryStore(options.registryPath)
39
+ const registry = await registryStore.load()
40
+
41
+ const results: DoctorResults = {
42
+ healthy: [],
43
+ broken: [],
44
+ rogue: [],
45
+ }
46
+
47
+ for (const [skillName, skill] of Object.entries(registry.skills)) {
48
+ let skillHealthy = true
49
+
50
+ for (const [agentId, assignment] of Object.entries(skill.assignments)) {
51
+ const agent = options.agents[agentId]
52
+ if (!agent || !agent.detected) continue
53
+
54
+ const agentSkillsDir = expandPath(agent.globalPath)
55
+ const expectedPath = join(agentSkillsDir, skillName)
56
+ const expectedTarget = join(options.skillsDir, skillName)
57
+
58
+ const pathIsSymlink = await isSymlink(expectedPath)
59
+
60
+ if (!pathIsSymlink) {
61
+ try {
62
+ await access(expectedPath)
63
+ results.rogue.push({ skill: skillName, agent: agentId, path: expectedPath })
64
+ skillHealthy = false
65
+ } catch {
66
+ results.broken.push({
67
+ skill: skillName,
68
+ agent: agentId,
69
+ path: expectedPath,
70
+ reason: "symlink missing",
71
+ })
72
+ skillHealthy = false
73
+ }
74
+ continue
75
+ }
76
+
77
+ const target = await getSymlinkTarget(expectedPath)
78
+
79
+ try {
80
+ await access(target!)
81
+ } catch {
82
+ results.broken.push({
83
+ skill: skillName,
84
+ agent: agentId,
85
+ path: expectedPath,
86
+ reason: "target missing",
87
+ })
88
+ skillHealthy = false
89
+ continue
90
+ }
91
+
92
+ if (target !== expectedTarget) {
93
+ results.broken.push({
94
+ skill: skillName,
95
+ agent: agentId,
96
+ path: expectedPath,
97
+ reason: `wrong target: ${target}`,
98
+ })
99
+ skillHealthy = false
100
+ }
101
+ }
102
+
103
+ if (skillHealthy) {
104
+ results.healthy.push(skillName)
105
+ }
106
+ }
107
+
108
+ return results
109
+ }
110
+
111
+ export default defineCommand({
112
+ meta: { name: "doctor", description: "Verify symlink integrity" },
113
+ args: {
114
+ fix: { type: "boolean", description: "Automatically fix issues", default: false },
115
+ },
116
+ async run({ args }) {
117
+ const configStore = new ConfigStore(getConfigPath())
118
+ const config = await configStore.load()
119
+
120
+ const agentRegistry = new AgentRegistry(config.agents)
121
+ const detected = await agentRegistry.detectAgents()
122
+
123
+ console.log("\nChecking symlink integrity...\n")
124
+
125
+ const results = await runDoctor({
126
+ skillsDir: getSkillsDir(),
127
+ registryPath: getRegistryPath(),
128
+ agents: detected,
129
+ })
130
+
131
+ for (const skill of results.healthy) {
132
+ console.log(`✓ ${skill}`)
133
+ }
134
+
135
+ for (const broken of results.broken) {
136
+ console.log(`✗ ${broken.skill}`)
137
+ console.log(` └─ ${broken.agent}: BROKEN (${broken.reason})`)
138
+ }
139
+
140
+ for (const rogue of results.rogue) {
141
+ console.log(`⚠ ${rogue.skill}`)
142
+ console.log(` └─ ${rogue.agent}: ROGUE (real file, not symlink)`)
143
+ }
144
+
145
+ console.log(`\nSummary: ${results.broken.length} broken, ${results.rogue.length} rogue, ${results.healthy.length} healthy`)
146
+
147
+ if (results.broken.length === 0 && results.rogue.length === 0) {
148
+ console.log("\nAll symlinks healthy!")
149
+ return
150
+ }
151
+
152
+ if (!args.fix) {
153
+ const shouldFix = await p.confirm({ message: "Fix issues?" })
154
+ if (p.isCancel(shouldFix) || !shouldFix) return
155
+ }
156
+
157
+ for (const broken of results.broken) {
158
+ const agent = detected[broken.agent]
159
+ if (!agent) continue
160
+
161
+ const expectedTarget = join(getSkillsDir(), broken.skill)
162
+ await removeSymlink(broken.path)
163
+ await createSymlink(expectedTarget, broken.path)
164
+ console.log(`Fixed: ${broken.skill} (${broken.agent})`)
165
+ }
166
+
167
+ console.log("\nRepairs complete!")
168
+ },
169
+ })
@@ -0,0 +1,99 @@
1
+ import { defineCommand } from "citty"
2
+ import { ConfigStore } from "../core/config-store"
3
+ import { AgentRegistry } from "../core/agent-registry"
4
+ import { getConfigPath } from "../utils/paths"
5
+ import { selectFromList, selectAgent } from "../utils/prompts"
6
+ import { mkdir, access } from "node:fs/promises"
7
+ import { join } from "node:path"
8
+
9
+ export default defineCommand({
10
+ meta: {
11
+ name: "import",
12
+ description: "Copy a global skill into current project",
13
+ },
14
+ args: {
15
+ skill: {
16
+ type: "positional",
17
+ description: "Skill name to import",
18
+ },
19
+ to: {
20
+ type: "string",
21
+ description: "Target directory (defaults to detected agent's project path)",
22
+ },
23
+ agent: {
24
+ type: "string",
25
+ description: "Source agent (defaults to first detected with skill)",
26
+ },
27
+ },
28
+ async run({ args }) {
29
+ const configStore = new ConfigStore(getConfigPath())
30
+ const config = await configStore.load()
31
+ const registry = new AgentRegistry(config.agents)
32
+
33
+ let skillName = args.skill
34
+ let agentId = args.agent
35
+
36
+ // Interactive agent selection if not provided
37
+ if (!agentId) {
38
+ agentId = await selectAgent(config.agents, "Select source agent")
39
+ }
40
+
41
+ const agent = config.agents[agentId]
42
+ if (!agent || !agent.detected) {
43
+ console.error(`Agent not found or not detected: ${agentId}`)
44
+ process.exit(1)
45
+ }
46
+
47
+ // Get available skills from selected agent
48
+ const availableSkills = await registry.listSkills(agentId)
49
+
50
+ if (availableSkills.length === 0) {
51
+ console.log(`No skills found in ${agent.name}.`)
52
+ return
53
+ }
54
+
55
+ // Interactive skill selection if not provided
56
+ if (!skillName) {
57
+ skillName = await selectFromList(
58
+ "Select skill to import:",
59
+ availableSkills.map((s) => ({ value: s.name, label: s.name }))
60
+ )
61
+ }
62
+
63
+ // Find skill source
64
+ const sourcePath = registry.getSkillPath(skillName, agentId)
65
+ try {
66
+ await access(join(sourcePath, "SKILL.md"))
67
+ } catch {
68
+ console.error(`Skill not found in ${agentId}: ${skillName}`)
69
+ process.exit(1)
70
+ }
71
+
72
+ // Determine target path
73
+ let targetPath: string
74
+
75
+ if (args.to) {
76
+ targetPath = join(args.to, skillName)
77
+ } else {
78
+ // Use project path of source agent
79
+ targetPath = join(process.cwd(), agent.projectPath, skillName)
80
+ }
81
+
82
+ // Check if target exists
83
+ try {
84
+ await access(targetPath)
85
+ console.error(`Skill already exists at: ${targetPath}`)
86
+ process.exit(1)
87
+ } catch {
88
+ // Good, doesn't exist
89
+ }
90
+
91
+ // Copy skill
92
+ await mkdir(join(targetPath, ".."), { recursive: true })
93
+ await Bun.$`cp -r ${sourcePath} ${targetPath}`
94
+
95
+ console.log(`\nImported: ${skillName}`)
96
+ console.log(`From: ${agent.name}`)
97
+ console.log(`To: ${targetPath}`)
98
+ },
99
+ })