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,175 @@
1
+ import { defineCommand } from "citty"
2
+ import { ConfigStore } from "../core/config-store"
3
+ import { AgentRegistry } from "../core/agent-registry"
4
+ import { SnapshotManager } from "../core/snapshot"
5
+ import { getConfigPath, getSnapshotsDir, expandPath } from "../utils/paths"
6
+ import { selectFromList, selectAgent, inputText } from "../utils/prompts"
7
+ import { mkdir, readFile } from "node:fs/promises"
8
+ import { join, dirname } from "node:path"
9
+ import * as tar from "tar"
10
+
11
+ export default defineCommand({
12
+ meta: {
13
+ name: "restore",
14
+ description: "Restore skills from backup",
15
+ },
16
+ args: {
17
+ path: {
18
+ type: "positional",
19
+ description: "Backup path (.tar.gz)",
20
+ },
21
+ to: {
22
+ type: "string",
23
+ description: "Restore to specific agent only",
24
+ },
25
+ snapshot: {
26
+ type: "string",
27
+ description: "Restore from snapshot ID instead of backup file",
28
+ },
29
+ dryRun: {
30
+ type: "boolean",
31
+ alias: "n",
32
+ description: "Preview changes without applying",
33
+ default: false,
34
+ },
35
+ },
36
+ async run({ args }) {
37
+ const configStore = new ConfigStore(getConfigPath())
38
+ const config = await configStore.load()
39
+
40
+ let snapshotId = args.snapshot
41
+ let backupPath = args.path
42
+
43
+ // Interactive mode if no path or snapshot provided
44
+ if (!backupPath && !snapshotId) {
45
+ const mode = await selectFromList("Restore from:", [
46
+ { value: "snapshot", label: "Snapshot", hint: "restore from auto-saved snapshot" },
47
+ { value: "backup", label: "Backup file", hint: "restore from .tar.gz archive" },
48
+ ])
49
+
50
+ if (mode === "snapshot") {
51
+ const snapshots = new SnapshotManager(getSnapshotsDir(), config.snapshots.maxCount)
52
+ const list = await snapshots.listSnapshots()
53
+
54
+ if (list.length === 0) {
55
+ console.log("No snapshots available.")
56
+ return
57
+ }
58
+
59
+ snapshotId = await selectFromList(
60
+ "Select snapshot:",
61
+ list.map((s) => ({
62
+ value: s.id,
63
+ label: s.id,
64
+ hint: `${s.reason} (${s.skills.length} skills)`,
65
+ }))
66
+ )
67
+ } else {
68
+ backupPath = await inputText("Backup file path:", { placeholder: "./backup.tar.gz" })
69
+ }
70
+ }
71
+
72
+ // Handle snapshot restore
73
+ if (snapshotId) {
74
+ const snapshots = new SnapshotManager(
75
+ getSnapshotsDir(),
76
+ config.snapshots.maxCount
77
+ )
78
+ const list = await snapshots.listSnapshots()
79
+ const snapshot = list.find((s) => s.id === snapshotId)
80
+
81
+ if (!snapshot) {
82
+ console.error(`Snapshot not found: ${snapshotId}`)
83
+ process.exit(1)
84
+ }
85
+
86
+ console.log(`\nRestoring from snapshot: ${snapshot.id}`)
87
+ console.log(`Skills: ${snapshot.skills.join(", ")}`)
88
+
89
+ if (args.dryRun) {
90
+ console.log("\n(dry run - no changes made)")
91
+ return
92
+ }
93
+
94
+ // Restore to all detected agents or specific one
95
+ const targetAgents = args.to
96
+ ? [args.to]
97
+ : Object.entries(config.agents)
98
+ .filter(([_, a]) => a.detected)
99
+ .map(([id]) => id)
100
+
101
+ for (const agentId of targetAgents) {
102
+ const agent = config.agents[agentId]
103
+ if (!agent) continue
104
+ await snapshots.restore(snapshotId, expandPath(agent.globalPath))
105
+ console.log(`Restored to ${agent.name}`)
106
+ }
107
+
108
+ console.log("\nRestore complete!")
109
+ return
110
+ }
111
+
112
+ // Handle backup file restore
113
+ if (!backupPath) {
114
+ console.error("No backup path provided.")
115
+ process.exit(1)
116
+ }
117
+
118
+ const tempDir = join(dirname(backupPath), `.simba-restore-${Date.now()}`)
119
+ await mkdir(tempDir, { recursive: true })
120
+
121
+ // Extract backup
122
+ await tar.extract({
123
+ file: backupPath,
124
+ cwd: tempDir,
125
+ })
126
+
127
+ // Read manifest
128
+ const manifest = JSON.parse(
129
+ await readFile(join(tempDir, "manifest.json"), "utf-8")
130
+ )
131
+
132
+ console.log(`\nRestoring from backup: ${backupPath}`)
133
+ console.log(`Created: ${manifest.created}`)
134
+ console.log(`Skills: ${Object.keys(manifest.skills).length}`)
135
+
136
+ if (args.dryRun) {
137
+ console.log("\nWould restore:")
138
+ for (const skillName of Object.keys(manifest.skills)) {
139
+ console.log(` ${skillName}`)
140
+ }
141
+ console.log("\n(dry run - no changes made)")
142
+ await Bun.$`rm -rf ${tempDir}`
143
+ return
144
+ }
145
+
146
+ // Determine target agents
147
+ const targetAgents = args.to
148
+ ? [args.to]
149
+ : Object.entries(config.agents)
150
+ .filter(([_, a]) => a.detected)
151
+ .map(([id]) => id)
152
+
153
+ // Copy skills to targets
154
+ for (const agentId of targetAgents) {
155
+ const agent = config.agents[agentId]
156
+ if (!agent) continue
157
+
158
+ const skillsPath = expandPath(agent.globalPath)
159
+ await mkdir(skillsPath, { recursive: true })
160
+
161
+ for (const skillName of Object.keys(manifest.skills)) {
162
+ const sourcePath = join(tempDir, "skills", skillName)
163
+ const destPath = join(skillsPath, skillName)
164
+ await Bun.$`cp -r ${sourcePath} ${destPath}`
165
+ }
166
+
167
+ console.log(`Restored to ${agent.name}`)
168
+ }
169
+
170
+ // Cleanup temp
171
+ await Bun.$`rm -rf ${tempDir}`
172
+
173
+ console.log("\nRestore complete!")
174
+ },
175
+ })
@@ -0,0 +1,39 @@
1
+ import { defineCommand } from "citty"
2
+ import { ConfigStore } from "../core/config-store"
3
+ import { SnapshotManager } from "../core/snapshot"
4
+ import { getConfigPath, getSnapshotsDir } from "../utils/paths"
5
+
6
+ export default defineCommand({
7
+ meta: {
8
+ name: "snapshots",
9
+ description: "List available snapshots",
10
+ },
11
+ async run() {
12
+ const configStore = new ConfigStore(getConfigPath())
13
+ const config = await configStore.load()
14
+
15
+ const snapshots = new SnapshotManager(
16
+ getSnapshotsDir(),
17
+ config.snapshots.maxCount
18
+ )
19
+
20
+ const list = await snapshots.listSnapshots()
21
+
22
+ if (list.length === 0) {
23
+ console.log("No snapshots available.")
24
+ return
25
+ }
26
+
27
+ console.log("\nAvailable snapshots:\n")
28
+
29
+ for (const snapshot of list) {
30
+ console.log(` ${snapshot.id}`)
31
+ console.log(` Reason: ${snapshot.reason}`)
32
+ console.log(` Skills: ${snapshot.skills.length}`)
33
+ console.log("")
34
+ }
35
+
36
+ console.log(`Total: ${list.length} snapshots`)
37
+ console.log(`\nUse 'simba restore --snapshot <id>' to restore`)
38
+ },
39
+ })
@@ -0,0 +1,70 @@
1
+ import { defineCommand } from "citty"
2
+ import { ConfigStore } from "../core/config-store"
3
+ import { AgentRegistry } from "../core/agent-registry"
4
+ import { SkillManager } from "../core/skill-manager"
5
+ import { getConfigPath } from "../utils/paths"
6
+
7
+ export default defineCommand({
8
+ meta: {
9
+ name: "status",
10
+ description: "Show skill matrix across agents",
11
+ },
12
+ args: {
13
+ agent: {
14
+ type: "string",
15
+ description: "Filter to specific agent",
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 manager = new SkillManager(registry, config.agents)
24
+
25
+ const matrix = await manager.buildMatrix()
26
+
27
+ // Get detected agents
28
+ const detectedAgents = Object.entries(config.agents)
29
+ .filter(([_, a]) => a.detected)
30
+ .filter(([id]) => !args.agent || id === args.agent)
31
+
32
+ if (detectedAgents.length === 0) {
33
+ console.log("No agents detected. Run 'simba detect' first.")
34
+ return
35
+ }
36
+
37
+ // Print header
38
+ const agentNames = detectedAgents.map(([_, a]) => a.shortName.padEnd(8))
39
+ console.log(`\n${"Skill".padEnd(24)} ${agentNames.join(" ")}`)
40
+ console.log("─".repeat(24 + agentNames.length * 9))
41
+
42
+ // Print matrix
43
+ const statusSymbols = {
44
+ synced: "✓",
45
+ conflict: "⚠",
46
+ unique: "●",
47
+ missing: "─",
48
+ }
49
+
50
+ for (const row of matrix) {
51
+ const cells = detectedAgents.map(([id]) => {
52
+ const cell = row.agents[id]
53
+ if (!cell?.present) return "─".padStart(4).padEnd(8)
54
+ if (row.status === "conflict") return "⚠".padStart(4).padEnd(8)
55
+ return "✓".padStart(4).padEnd(8)
56
+ })
57
+
58
+ const skillName = row.skillName.slice(0, 23).padEnd(24)
59
+ console.log(`${skillName} ${cells.join(" ")}`)
60
+ }
61
+
62
+ // Summary
63
+ const synced = matrix.filter((m) => m.status === "synced").length
64
+ const conflicts = matrix.filter((m) => m.status === "conflict").length
65
+ const unique = matrix.filter((m) => m.status === "unique").length
66
+
67
+ console.log("\n" + "─".repeat(24 + agentNames.length * 9))
68
+ console.log(`✓ synced: ${synced} ⚠ conflict: ${conflicts} ● unique: ${unique}`)
69
+ },
70
+ })
@@ -0,0 +1,108 @@
1
+ import { defineCommand } from "citty"
2
+ import { ConfigStore } from "../core/config-store"
3
+ import { AgentRegistry } from "../core/agent-registry"
4
+ import { SkillManager } from "../core/skill-manager"
5
+ import { SnapshotManager } from "../core/snapshot"
6
+ import { getConfigPath, getSnapshotsDir } from "../utils/paths"
7
+ import * as readline from "node:readline"
8
+
9
+ export default defineCommand({
10
+ meta: {
11
+ name: "sync",
12
+ description: "Sync skills across agents (union merge)",
13
+ },
14
+ args: {
15
+ source: {
16
+ type: "string",
17
+ description: "Source of truth agent (one-way sync)",
18
+ },
19
+ dryRun: {
20
+ type: "boolean",
21
+ alias: "n",
22
+ description: "Preview changes without applying",
23
+ default: false,
24
+ },
25
+ },
26
+ async run({ args }) {
27
+ const configStore = new ConfigStore(getConfigPath())
28
+ const config = await configStore.load()
29
+
30
+ const registry = new AgentRegistry(config.agents)
31
+ const manager = new SkillManager(registry, config.agents)
32
+ const snapshots = new SnapshotManager(
33
+ getSnapshotsDir(),
34
+ config.snapshots.maxCount
35
+ )
36
+
37
+ const matrix = await manager.buildMatrix()
38
+
39
+ const unique = matrix.filter((m) => m.status === "unique")
40
+ const conflicts = matrix.filter((m) => m.status === "conflict")
41
+
42
+ if (unique.length === 0 && conflicts.length === 0) {
43
+ console.log("All skills are synced!")
44
+ return
45
+ }
46
+
47
+ // Show what will happen
48
+ if (unique.length > 0) {
49
+ console.log("\nWill copy:")
50
+ for (const skill of unique) {
51
+ const source = Object.entries(skill.agents).find(([_, v]) => v.present)?.[0]
52
+ const targets = Object.entries(skill.agents)
53
+ .filter(([_, v]) => !v.present)
54
+ .map(([id]) => id)
55
+ console.log(` ${skill.skillName} → ${targets.join(", ")}`)
56
+ }
57
+ }
58
+
59
+ if (conflicts.length > 0 && !args.source) {
60
+ console.log("\nConflicts (resolve manually):")
61
+ for (const skill of conflicts) {
62
+ const agents = Object.entries(skill.agents)
63
+ .filter(([_, v]) => v.present)
64
+ .map(([id]) => id)
65
+ console.log(` ${skill.skillName}: ${agents.join(" ≠ ")}`)
66
+ }
67
+ }
68
+
69
+ if (args.dryRun) {
70
+ console.log("\n(dry run - no changes made)")
71
+ return
72
+ }
73
+
74
+ // Create snapshot before changes
75
+ if (config.snapshots.autoSnapshot && (unique.length > 0 || conflicts.length > 0)) {
76
+ const skillPaths = [...unique, ...conflicts].flatMap((skill) =>
77
+ Object.entries(skill.agents)
78
+ .filter(([_, v]) => v.present)
79
+ .map(([agentId]) => registry.getSkillPath(skill.skillName, agentId))
80
+ )
81
+ await snapshots.createSnapshot(skillPaths, "pre-sync")
82
+ console.log("\nSnapshot created.")
83
+ }
84
+
85
+ // Sync unique skills
86
+ for (const skill of unique) {
87
+ const source = Object.entries(skill.agents).find(([_, v]) => v.present)?.[0]
88
+ if (!source) continue
89
+
90
+ const synced = await manager.syncUnique(skill.skillName, source)
91
+ console.log(`Synced ${skill.skillName} to ${synced.join(", ")}`)
92
+ }
93
+
94
+ // Handle conflicts with --source flag
95
+ if (args.source && conflicts.length > 0) {
96
+ for (const skill of conflicts) {
97
+ const losers = Object.entries(skill.agents)
98
+ .filter(([id, v]) => v.present && id !== args.source)
99
+ .map(([id]) => id)
100
+
101
+ await manager.resolveConflict(skill.skillName, args.source, losers)
102
+ console.log(`Resolved ${skill.skillName} using ${args.source}`)
103
+ }
104
+ }
105
+
106
+ console.log("\nSync complete!")
107
+ },
108
+ })
@@ -0,0 +1,68 @@
1
+ import { defineCommand } from "citty"
2
+ import { RegistryStore } from "../core/registry-store"
3
+ import { SkillsStore } from "../core/skills-store"
4
+ import { ConfigStore } from "../core/config-store"
5
+ import { getSkillsDir, getRegistryPath, getConfigPath, expandPath } from "../utils/paths"
6
+
7
+ export interface UnassignOptions {
8
+ skill: string
9
+ agents: string[]
10
+ skillsDir: string
11
+ registryPath: string
12
+ agentPaths: Record<string, string>
13
+ }
14
+
15
+ export async function runUnassign(options: UnassignOptions): Promise<void> {
16
+ const skillsStore = new SkillsStore(options.skillsDir, options.registryPath)
17
+ const registryStore = new RegistryStore(options.registryPath)
18
+ const registry = await registryStore.load()
19
+
20
+ const skill = registry.skills[options.skill]
21
+ if (!skill) {
22
+ console.error(`Skill not found: ${options.skill}`)
23
+ process.exit(1)
24
+ }
25
+
26
+ for (const agentId of options.agents) {
27
+ const agentPath = options.agentPaths[agentId]
28
+ if (!agentPath) {
29
+ console.error(`Unknown agent: ${agentId}`)
30
+ continue
31
+ }
32
+
33
+ await skillsStore.unassignSkill(options.skill, agentPath)
34
+ delete skill.assignments[agentId]
35
+ console.log(`Unassigned ${options.skill} from ${agentId}`)
36
+ }
37
+
38
+ await registryStore.save(registry)
39
+ }
40
+
41
+ export default defineCommand({
42
+ meta: { name: "unassign", description: "Remove a skill from agents" },
43
+ args: {
44
+ skill: { type: "positional", description: "Skill name", required: true },
45
+ agents: { type: "positional", description: "Agent IDs (comma-separated)", required: true },
46
+ },
47
+ async run({ args }) {
48
+ const configStore = new ConfigStore(getConfigPath())
49
+ const config = await configStore.load()
50
+
51
+ const agentPaths: Record<string, string> = {}
52
+ for (const [id, agent] of Object.entries(config.agents)) {
53
+ if (agent.detected) {
54
+ agentPaths[id] = expandPath(agent.globalPath)
55
+ }
56
+ }
57
+
58
+ const agents = (args.agents as string).split(",").map(a => a.trim())
59
+
60
+ await runUnassign({
61
+ skill: args.skill,
62
+ agents,
63
+ skillsDir: getSkillsDir(),
64
+ registryPath: getRegistryPath(),
65
+ agentPaths,
66
+ })
67
+ },
68
+ })
@@ -0,0 +1,57 @@
1
+ import { defineCommand } from "citty"
2
+ import { ConfigStore } from "../core/config-store"
3
+ import { SnapshotManager } from "../core/snapshot"
4
+ import { getConfigPath, getSnapshotsDir, expandPath } from "../utils/paths"
5
+
6
+ export default defineCommand({
7
+ meta: {
8
+ name: "undo",
9
+ description: "Restore from most recent snapshot",
10
+ },
11
+ args: {
12
+ dryRun: {
13
+ type: "boolean",
14
+ alias: "n",
15
+ description: "Preview changes without applying",
16
+ default: false,
17
+ },
18
+ },
19
+ async run({ args }) {
20
+ const configStore = new ConfigStore(getConfigPath())
21
+ const config = await configStore.load()
22
+
23
+ const snapshots = new SnapshotManager(
24
+ getSnapshotsDir(),
25
+ config.snapshots.maxCount
26
+ )
27
+
28
+ const latest = await snapshots.getLatestSnapshot()
29
+
30
+ if (!latest) {
31
+ console.log("No snapshots available.")
32
+ return
33
+ }
34
+
35
+ console.log(`\nLatest snapshot: ${latest.id}`)
36
+ console.log(`Reason: ${latest.reason}`)
37
+ console.log(`Created: ${latest.created}`)
38
+ console.log(`Skills: ${latest.skills.join(", ")}`)
39
+
40
+ if (args.dryRun) {
41
+ console.log("\n(dry run - no changes made)")
42
+ return
43
+ }
44
+
45
+ // Restore to all detected agents
46
+ const targetAgents = Object.entries(config.agents)
47
+ .filter(([_, a]) => a.detected)
48
+ .map(([id, a]) => ({ id, path: expandPath(a.globalPath) }))
49
+
50
+ for (const { id, path } of targetAgents) {
51
+ await snapshots.restore(latest.id, path)
52
+ console.log(`Restored to ${config.agents[id].name}`)
53
+ }
54
+
55
+ console.log("\nUndo complete!")
56
+ },
57
+ })
@@ -0,0 +1,134 @@
1
+ import { defineCommand } from "citty"
2
+ import { rm } from "node:fs/promises"
3
+ import { join } from "node:path"
4
+ import * as p from "@clack/prompts"
5
+ import { RegistryStore } from "../core/registry-store"
6
+ import { SkillsStore } from "../core/skills-store"
7
+ import { ConfigStore } from "../core/config-store"
8
+ import { AgentRegistry } from "../core/agent-registry"
9
+ import { getSkillsDir, getRegistryPath, getConfigPath, expandPath } from "../utils/paths"
10
+
11
+ export interface UninstallOptions {
12
+ skills: string[]
13
+ skillsDir: string
14
+ registryPath: string
15
+ agentPaths: Record<string, string>
16
+ deleteFiles: boolean
17
+ }
18
+
19
+ export async function runUninstall(options: UninstallOptions): Promise<void> {
20
+ const skillsStore = new SkillsStore(options.skillsDir, options.registryPath)
21
+ const registryStore = new RegistryStore(options.registryPath)
22
+ const registry = await registryStore.load()
23
+
24
+ for (const name of options.skills) {
25
+ const skill = registry.skills[name]
26
+ if (!skill) {
27
+ console.log(` Skill not found: ${name}`)
28
+ continue
29
+ }
30
+
31
+ // Remove symlinks from all assigned agents
32
+ for (const agentId of Object.keys(skill.assignments)) {
33
+ const agentPath = options.agentPaths[agentId]
34
+ if (agentPath) {
35
+ try {
36
+ await rm(join(agentPath, name), { recursive: true, force: true })
37
+ console.log(` Removed from ${agentId}`)
38
+ } catch {
39
+ // Ignore errors if symlink doesn't exist
40
+ }
41
+ }
42
+ }
43
+
44
+ // Remove from store if requested
45
+ if (options.deleteFiles) {
46
+ await rm(join(options.skillsDir, name), { recursive: true, force: true })
47
+ console.log(` Deleted files: ${name}`)
48
+ }
49
+
50
+ // Remove from registry
51
+ delete registry.skills[name]
52
+
53
+ console.log(` Uninstalled: ${name}`)
54
+ }
55
+
56
+ await registryStore.save(registry)
57
+ console.log("\nUninstall complete!")
58
+ }
59
+
60
+ export default defineCommand({
61
+ meta: { name: "uninstall", description: "Remove a skill from Simba's store" },
62
+ args: {
63
+ skill: { type: "positional", description: "Skill name to uninstall", required: false },
64
+ },
65
+ async run({ args }) {
66
+ const registryStore = new RegistryStore(getRegistryPath())
67
+ const registry = await registryStore.load()
68
+
69
+ const configStore = new ConfigStore(getConfigPath())
70
+ const config = await configStore.load()
71
+
72
+ // Get agent paths
73
+ const agentRegistry = new AgentRegistry(config.agents)
74
+ const detected = await agentRegistry.detectAgents()
75
+
76
+ const agentPaths: Record<string, string> = {}
77
+ for (const [id, agent] of Object.entries(detected)) {
78
+ agentPaths[id] = expandPath(agent.globalPath)
79
+ }
80
+
81
+ let skills: string[]
82
+
83
+ if (args.skill) {
84
+ skills = [args.skill as string]
85
+ } else {
86
+ // Interactive mode
87
+ const managedSkills = Object.keys(registry.skills)
88
+
89
+ if (managedSkills.length === 0) {
90
+ console.log("No skills managed.")
91
+ return
92
+ }
93
+
94
+ const result = await p.multiselect({
95
+ message: "Select skills to uninstall",
96
+ options: managedSkills.map((s) => ({ value: s, label: s })),
97
+ required: true,
98
+ })
99
+
100
+ if (p.isCancel(result)) process.exit(0)
101
+ skills = result as string[]
102
+ }
103
+
104
+ // Confirm
105
+ const confirm = await p.confirm({
106
+ message: `Uninstall ${skills.length} skill(s)? This will remove them from all agents.`,
107
+ initialValue: false,
108
+ })
109
+
110
+ if (p.isCancel(confirm) || !confirm) {
111
+ console.log("Cancelled.")
112
+ return
113
+ }
114
+
115
+ // Ask about deleting files
116
+ const deleteFiles = await p.confirm({
117
+ message: "Also delete skill files from Simba's store?",
118
+ initialValue: true,
119
+ })
120
+
121
+ if (p.isCancel(deleteFiles)) {
122
+ console.log("Cancelled.")
123
+ return
124
+ }
125
+
126
+ await runUninstall({
127
+ skills,
128
+ skillsDir: getSkillsDir(),
129
+ registryPath: getRegistryPath(),
130
+ agentPaths,
131
+ deleteFiles: deleteFiles as boolean,
132
+ })
133
+ },
134
+ })