simba-skills 0.6.0 → 0.6.1

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.
package/README.md CHANGED
@@ -7,7 +7,7 @@
7
7
  [![npm version](https://img.shields.io/npm/v/simba-skills)](https://www.npmjs.com/package/simba-skills)
8
8
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
9
9
 
10
- AI skills manager with a central store and symlink-based distribution across 14+ coding agents.
10
+ AI skills manager with a central store and symlink-based distribution across 17+ coding agents.
11
11
 
12
12
  ## Why Simba?
13
13
 
@@ -108,22 +108,11 @@ simba undo
108
108
 
109
109
  ## Supported Agents
110
110
 
111
- | Agent | Global Path | Project Path |
112
- |-------|-------------|--------------|
113
- | Claude Code | `~/.claude/skills` | `.claude/skills` |
114
- | Cursor | `~/.cursor/skills` | `.cursor/skills` |
115
- | Codex | `~/.codex/skills` | `.codex/skills` |
116
- | GitHub Copilot | `~/.copilot/skills` | `.github/skills` |
117
- | Gemini CLI | `~/.gemini/skills` | `.gemini/skills` |
118
- | Windsurf | `~/.codeium/windsurf/skills` | `.windsurf/skills` |
119
- | Amp | `~/.config/agents/skills` | `.agents/skills` |
120
- | Goose | `~/.config/goose/skills` | `.goose/skills` |
121
- | OpenCode | `~/.config/opencode/skill` | `.opencode/skill` |
122
- | Kilo Code | `~/.kilocode/skills` | `.kilocode/skills` |
123
- | Roo Code | `~/.roo/skills` | `.roo/skills` |
124
- | Antigravity | `~/.gemini/antigravity/skills` | `.agent/skills` |
125
- | Clawdbot | `~/.clawdbot/skills` | `skills` |
126
- | Droid | `~/.factory/skills` | `.factory/skills` |
111
+ Supports Claude Code, Codex, OpenCode, Cursor, Gemini CLI, GitHub Copilot, Amp, Kimi Code CLI, Replit, and 30+ others.
112
+
113
+ Includes agents using the `.agents/skills` universal standard, plus agent-specific paths.
114
+
115
+ See full agent definitions and paths in [`src/core/config-store.ts`](./src/core/config-store.ts).
127
116
 
128
117
  ## Architecture
129
118
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "simba-skills",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "description": "AI skills manager - central store with symlink-based distribution across 15+ coding agents",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -56,13 +56,32 @@ export default defineCommand({
56
56
  // Get detected agents
57
57
  const agentRegistry = new AgentRegistry(config.agents)
58
58
  const detected = await agentRegistry.detectAgents()
59
- const detectedAgents = Object.entries(detected).filter(([, a]) => a.detected)
59
+
60
+ const universalFallbackByProjectPath = new Map<string, string>()
61
+ for (const agent of Object.values(detected)) {
62
+ if (!agent.universal || !agent.detected) continue
63
+ if (!universalFallbackByProjectPath.has(agent.projectPath)) {
64
+ universalFallbackByProjectPath.set(agent.projectPath, expandPath(agent.globalPath))
65
+ }
66
+ }
60
67
 
61
68
  const agentPaths: Record<string, string> = {}
62
- for (const [id, agent] of detectedAgents) {
63
- agentPaths[id] = expandPath(agent.globalPath)
69
+ for (const [id, agent] of Object.entries(detected)) {
70
+ if (agent.detected) {
71
+ agentPaths[id] = expandPath(agent.globalPath)
72
+ continue
73
+ }
74
+
75
+ if (agent.universal) {
76
+ const fallback = universalFallbackByProjectPath.get(agent.projectPath)
77
+ if (fallback) {
78
+ agentPaths[id] = fallback
79
+ }
80
+ }
64
81
  }
65
82
 
83
+ const assignableAgents = Object.entries(detected).filter(([id]) => agentPaths[id])
84
+
66
85
  // Interactive mode if args missing
67
86
  let skill = args.skill as string | undefined
68
87
  let agents: string[]
@@ -83,14 +102,14 @@ export default defineCommand({
83
102
  }
84
103
 
85
104
  if (!args.agents) {
86
- if (detectedAgents.length === 0) {
105
+ if (assignableAgents.length === 0) {
87
106
  console.log("No agents detected.")
88
107
  return
89
108
  }
90
109
 
91
110
  const result = await p.multiselect({
92
111
  message: "Select agents to assign to",
93
- options: detectedAgents.map(([id, a]) => ({ value: id, label: a.name })),
112
+ options: assignableAgents.map(([id, a]) => ({ value: id, label: a.name })),
94
113
  required: true,
95
114
  })
96
115
  if (p.isCancel(result)) process.exit(0)
@@ -349,6 +349,28 @@ export interface AssignResult {
349
349
  message?: string
350
350
  }
351
351
 
352
+ function resolveAssignmentPath(
353
+ agentId: string,
354
+ detected: Record<string, import("../core/types").Agent>
355
+ ): string | null {
356
+ const agent = detected[agentId]
357
+ if (!agent) return null
358
+
359
+ if (agent.detected) {
360
+ return expandPath(agent.globalPath)
361
+ }
362
+
363
+ if (!agent.universal) {
364
+ return null
365
+ }
366
+
367
+ const fallback = Object.values(detected).find(
368
+ (candidate) => candidate.universal && candidate.detected && candidate.projectPath === agent.projectPath
369
+ )
370
+
371
+ return fallback ? expandPath(fallback.globalPath) : null
372
+ }
373
+
352
374
  /** Detect agents and create symlinks for each skill's assignments */
353
375
  export async function assignSkillsToAgents(
354
376
  registry: { skills: Record<string, ManagedSkill> },
@@ -366,14 +388,13 @@ export async function assignSkillsToAgents(
366
388
 
367
389
  const assignments = skill.assignments
368
390
  for (const [agentId, assignment] of Object.entries(assignments)) {
369
- const agent = detected[agentId]
370
- if (!agent?.detected) {
391
+ const assignmentPath = resolveAssignmentPath(agentId, detected)
392
+ if (!assignmentPath) {
371
393
  results.push({ skill: skillName, agent: agentId, status: "skipped", message: "agent not detected" })
372
394
  continue
373
395
  }
374
396
 
375
- const agentSkillsDir = expandPath(agent.globalPath)
376
- await skillsStore.assignSkill(skillName, agentSkillsDir, assignment)
397
+ await skillsStore.assignSkill(skillName, assignmentPath, assignment)
377
398
  results.push({ skill: skillName, agent: agentId, status: "assigned" })
378
399
  }
379
400
  }
@@ -51,9 +51,18 @@ export default defineCommand({
51
51
 
52
52
  await configStore.save(config)
53
53
 
54
- // Output results
55
- console.log("\nDetected agents:")
56
- for (const [id, agent] of Object.entries(detected)) {
54
+ // Output results grouped by universal/custom
55
+ const universal = Object.values(detected).filter(a => a.universal)
56
+ const custom = Object.values(detected).filter(a => !a.universal)
57
+
58
+ console.log("\nUniversal (.agents/skills):")
59
+ for (const agent of universal) {
60
+ const status = agent.detected ? "✓" : "─"
61
+ console.log(` ${status} ${agent.name}`)
62
+ }
63
+
64
+ console.log("\nCustom:")
65
+ for (const agent of custom) {
57
66
  const status = agent.detected ? "✓" : "─"
58
67
  console.log(` ${status} ${agent.name}`)
59
68
  }
@@ -31,7 +31,7 @@ interface MarketplaceJson {
31
31
  plugins?: MarketplacePlugin[]
32
32
  }
33
33
 
34
- const SKILL_DIRS = ["skills", ".claude/skills", ".cursor/skills", ".codex/skills"]
34
+ const SKILL_DIRS = ["skills", ".agents/skills", ".claude/skills", ".cursor/skills", ".codex/skills"]
35
35
 
36
36
  interface SubmoduleInfo {
37
37
  path: string
@@ -11,15 +11,20 @@ export class AgentRegistry {
11
11
  const results: Record<string, Agent> = {}
12
12
 
13
13
  for (const [id, agent] of Object.entries(this.agents)) {
14
- const globalPath = expandPath(agent.globalPath)
15
- const parentDir = dirname(globalPath)
14
+ const configuredPaths = agent.detectPaths && agent.detectPaths.length > 0
15
+ ? agent.detectPaths
16
+ : [agent.detectPath ?? dirname(agent.globalPath)]
16
17
 
17
18
  let detected = false
18
- try {
19
- await access(parentDir)
20
- detected = true
21
- } catch {
22
- detected = false
19
+ for (const path of configuredPaths) {
20
+ const detectionPath = expandPath(path)
21
+ try {
22
+ await access(detectionPath)
23
+ detected = true
24
+ break
25
+ } catch {
26
+ continue
27
+ }
23
28
  }
24
29
 
25
30
  results[id] = { ...agent, detected }
@@ -3,32 +3,405 @@ import { readFile, writeFile, mkdir } from "node:fs/promises";
3
3
  import { dirname } from "node:path";
4
4
  import type { Config, Agent } from "./types";
5
5
 
6
- // Add new agents here: [id, name, shortName, globalPath, projectPath]
7
- const AGENT_DEFINITIONS: [string, string, string, string, string][] = [
8
- ["claude", "Claude Code", "Claude", "~/.claude/skills", ".claude/skills"],
9
- ["cursor", "Cursor", "Cursor", "~/.cursor/skills", ".cursor/skills"],
10
- ["codex", "Codex", "Codex", "~/.codex/skills", ".codex/skills"],
11
- ["copilot", "GitHub Copilot", "Copilot", "~/.copilot/skills", ".github/skills"],
12
- ["gemini", "Gemini CLI", "Gemini", "~/.gemini/skills", ".gemini/skills"],
13
- ["windsurf", "Windsurf", "Windsurf", "~/.codeium/windsurf/skills", ".windsurf/skills"],
14
- ["amp", "Amp", "Amp", "~/.config/agents/skills", ".agents/skills"],
15
- ["goose", "Goose", "Goose", "~/.config/goose/skills", ".goose/skills"],
16
- ["opencode", "OpenCode", "OpenCode", "~/.config/opencode/skills", ".opencode/skills"],
17
- ["kilo", "Kilo Code", "Kilo", "~/.kilocode/skills", ".kilocode/skills"],
18
- ["roo", "Roo Code", "Roo", "~/.roo/skills", ".roo/skills"],
19
- ["antigravity", "Antigravity", "Antigrav", "~/.gemini/antigravity/skills", ".agent/skills"],
20
- ["clawdbot", "Clawdbot", "Clawdbot", "~/.clawdbot/skills", "skills"],
21
- ["droid", "Droid", "Droid", "~/.factory/skills", ".factory/skills"],
22
- ["pi", "pi", "pi", "~/.pi/agent/skills", ".pi/skills"],
6
+ interface AgentDefinition {
7
+ id: string;
8
+ name: string;
9
+ shortName: string;
10
+ globalPath: string;
11
+ projectPath: string;
12
+ detectPath?: string;
13
+ detectPaths?: string[];
14
+ universal: boolean;
15
+ }
16
+
17
+ const AGENT_DEFINITIONS: AgentDefinition[] = [
18
+ // Universal agents (use .agents/skills as project path)
19
+ {
20
+ id: "amp",
21
+ name: "Amp",
22
+ shortName: "Amp",
23
+ globalPath: "~/.config/agents/skills",
24
+ projectPath: ".agents/skills",
25
+ detectPath: "~/.config/amp",
26
+ universal: true,
27
+ },
28
+ {
29
+ id: "codex",
30
+ name: "Codex",
31
+ shortName: "Codex",
32
+ globalPath: "~/.codex/skills",
33
+ projectPath: ".agents/skills",
34
+ detectPath: "~/.codex",
35
+ universal: true,
36
+ },
37
+ {
38
+ id: "copilot",
39
+ name: "GitHub Copilot",
40
+ shortName: "Copilot",
41
+ globalPath: "~/.copilot/skills",
42
+ projectPath: ".agents/skills",
43
+ detectPath: "~/.copilot",
44
+ universal: true,
45
+ },
46
+ {
47
+ id: "gemini",
48
+ name: "Gemini CLI",
49
+ shortName: "Gemini",
50
+ globalPath: "~/.gemini/skills",
51
+ projectPath: ".agents/skills",
52
+ detectPath: "~/.gemini",
53
+ universal: true,
54
+ },
55
+ {
56
+ id: "opencode",
57
+ name: "OpenCode",
58
+ shortName: "OpenCode",
59
+ globalPath: "~/.config/opencode/skills",
60
+ projectPath: ".agents/skills",
61
+ detectPath: "~/.config/opencode",
62
+ universal: true,
63
+ },
64
+ {
65
+ id: "kimi",
66
+ name: "Kimi Code CLI",
67
+ shortName: "Kimi",
68
+ globalPath: "~/.config/agents/skills",
69
+ projectPath: ".agents/skills",
70
+ detectPath: "~/.kimi",
71
+ universal: true,
72
+ },
73
+ {
74
+ id: "replit",
75
+ name: "Replit",
76
+ shortName: "Replit",
77
+ globalPath: "~/.config/agents/skills",
78
+ projectPath: ".agents/skills",
79
+ detectPath: ".replit",
80
+ universal: true,
81
+ },
82
+
83
+ // Custom agents
84
+ {
85
+ id: "claude",
86
+ name: "Claude Code",
87
+ shortName: "Claude",
88
+ globalPath: "~/.claude/skills",
89
+ projectPath: ".claude/skills",
90
+ detectPath: "~/.claude",
91
+ universal: false,
92
+ },
93
+ {
94
+ id: "cursor",
95
+ name: "Cursor",
96
+ shortName: "Cursor",
97
+ globalPath: "~/.cursor/skills",
98
+ projectPath: ".cursor/skills",
99
+ detectPath: "~/.cursor",
100
+ universal: false,
101
+ },
102
+ {
103
+ id: "windsurf",
104
+ name: "Windsurf",
105
+ shortName: "Windsurf",
106
+ globalPath: "~/.codeium/windsurf/skills",
107
+ projectPath: ".windsurf/skills",
108
+ detectPath: "~/.codeium/windsurf",
109
+ universal: false,
110
+ },
111
+ {
112
+ id: "goose",
113
+ name: "Goose",
114
+ shortName: "Goose",
115
+ globalPath: "~/.config/goose/skills",
116
+ projectPath: ".goose/skills",
117
+ detectPath: "~/.config/goose",
118
+ universal: false,
119
+ },
120
+ {
121
+ id: "kilo",
122
+ name: "Kilo Code",
123
+ shortName: "Kilo",
124
+ globalPath: "~/.kilocode/skills",
125
+ projectPath: ".kilocode/skills",
126
+ detectPath: "~/.kilocode",
127
+ universal: false,
128
+ },
129
+ {
130
+ id: "roo",
131
+ name: "Roo Code",
132
+ shortName: "Roo",
133
+ globalPath: "~/.roo/skills",
134
+ projectPath: ".roo/skills",
135
+ detectPath: "~/.roo",
136
+ universal: false,
137
+ },
138
+ {
139
+ id: "antigravity",
140
+ name: "Antigravity",
141
+ shortName: "Antigrav",
142
+ globalPath: "~/.gemini/antigravity/skills",
143
+ projectPath: ".agent/skills",
144
+ detectPath: "~/.gemini/antigravity",
145
+ universal: false,
146
+ },
147
+ {
148
+ id: "droid",
149
+ name: "Droid",
150
+ shortName: "Droid",
151
+ globalPath: "~/.factory/skills",
152
+ projectPath: ".factory/skills",
153
+ detectPath: "~/.factory",
154
+ universal: false,
155
+ },
156
+ {
157
+ id: "pi",
158
+ name: "pi",
159
+ shortName: "pi",
160
+ globalPath: "~/.pi/agent/skills",
161
+ projectPath: ".pi/skills",
162
+ detectPath: "~/.pi/agent",
163
+ universal: false,
164
+ },
165
+ {
166
+ id: "openclaw",
167
+ name: "OpenClaw",
168
+ shortName: "OpenClaw",
169
+ globalPath: "~/.openclaw/skills",
170
+ projectPath: "skills",
171
+ detectPath: "~/.openclaw",
172
+ universal: false,
173
+ },
174
+ {
175
+ id: "augment",
176
+ name: "Augment",
177
+ shortName: "Augment",
178
+ globalPath: "~/.augment/skills",
179
+ projectPath: ".augment/skills",
180
+ detectPath: "~/.augment",
181
+ universal: false,
182
+ },
183
+ {
184
+ id: "cline",
185
+ name: "Cline",
186
+ shortName: "Cline",
187
+ globalPath: "~/.cline/skills",
188
+ projectPath: ".cline/skills",
189
+ detectPath: "~/.cline",
190
+ universal: false,
191
+ },
192
+ {
193
+ id: "codebuddy",
194
+ name: "CodeBuddy",
195
+ shortName: "CodeBuddy",
196
+ globalPath: "~/.codebuddy/skills",
197
+ projectPath: ".codebuddy/skills",
198
+ detectPaths: [".codebuddy", "~/.codebuddy"],
199
+ universal: false,
200
+ },
201
+ {
202
+ id: "commandcode",
203
+ name: "Command Code",
204
+ shortName: "CmdCode",
205
+ globalPath: "~/.commandcode/skills",
206
+ projectPath: ".commandcode/skills",
207
+ detectPath: "~/.commandcode",
208
+ universal: false,
209
+ },
210
+ {
211
+ id: "continue",
212
+ name: "Continue",
213
+ shortName: "Continue",
214
+ globalPath: "~/.continue/skills",
215
+ projectPath: ".continue/skills",
216
+ detectPaths: [".continue", "~/.continue"],
217
+ universal: false,
218
+ },
219
+ {
220
+ id: "cortex",
221
+ name: "Cortex Code",
222
+ shortName: "Cortex",
223
+ globalPath: "~/.snowflake/cortex/skills",
224
+ projectPath: ".cortex/skills",
225
+ detectPath: "~/.snowflake/cortex",
226
+ universal: false,
227
+ },
228
+ {
229
+ id: "crush",
230
+ name: "Crush",
231
+ shortName: "Crush",
232
+ globalPath: "~/.config/crush/skills",
233
+ projectPath: ".crush/skills",
234
+ detectPath: "~/.config/crush",
235
+ universal: false,
236
+ },
237
+ {
238
+ id: "junie",
239
+ name: "Junie",
240
+ shortName: "Junie",
241
+ globalPath: "~/.junie/skills",
242
+ projectPath: ".junie/skills",
243
+ detectPath: "~/.junie",
244
+ universal: false,
245
+ },
246
+ {
247
+ id: "iflow",
248
+ name: "iFlow CLI",
249
+ shortName: "iFlow",
250
+ globalPath: "~/.iflow/skills",
251
+ projectPath: ".iflow/skills",
252
+ detectPath: "~/.iflow",
253
+ universal: false,
254
+ },
255
+ {
256
+ id: "kiro",
257
+ name: "Kiro CLI",
258
+ shortName: "Kiro",
259
+ globalPath: "~/.kiro/skills",
260
+ projectPath: ".kiro/skills",
261
+ detectPath: "~/.kiro",
262
+ universal: false,
263
+ },
264
+ {
265
+ id: "kode",
266
+ name: "Kode",
267
+ shortName: "Kode",
268
+ globalPath: "~/.kode/skills",
269
+ projectPath: ".kode/skills",
270
+ detectPath: "~/.kode",
271
+ universal: false,
272
+ },
273
+ {
274
+ id: "mcpjam",
275
+ name: "MCPJam",
276
+ shortName: "MCPJam",
277
+ globalPath: "~/.mcpjam/skills",
278
+ projectPath: ".mcpjam/skills",
279
+ detectPath: "~/.mcpjam",
280
+ universal: false,
281
+ },
282
+ {
283
+ id: "mistralvibe",
284
+ name: "Mistral Vibe",
285
+ shortName: "Vibe",
286
+ globalPath: "~/.vibe/skills",
287
+ projectPath: ".vibe/skills",
288
+ detectPath: "~/.vibe",
289
+ universal: false,
290
+ },
291
+ {
292
+ id: "mux",
293
+ name: "Mux",
294
+ shortName: "Mux",
295
+ globalPath: "~/.mux/skills",
296
+ projectPath: ".mux/skills",
297
+ detectPath: "~/.mux",
298
+ universal: false,
299
+ },
300
+ {
301
+ id: "openhands",
302
+ name: "OpenHands",
303
+ shortName: "OpenHands",
304
+ globalPath: "~/.openhands/skills",
305
+ projectPath: ".openhands/skills",
306
+ detectPath: "~/.openhands",
307
+ universal: false,
308
+ },
309
+ {
310
+ id: "qoder",
311
+ name: "Qoder",
312
+ shortName: "Qoder",
313
+ globalPath: "~/.qoder/skills",
314
+ projectPath: ".qoder/skills",
315
+ detectPath: "~/.qoder",
316
+ universal: false,
317
+ },
318
+ {
319
+ id: "qwen",
320
+ name: "Qwen Code",
321
+ shortName: "Qwen",
322
+ globalPath: "~/.qwen/skills",
323
+ projectPath: ".qwen/skills",
324
+ detectPath: "~/.qwen",
325
+ universal: false,
326
+ },
327
+ {
328
+ id: "trae",
329
+ name: "Trae",
330
+ shortName: "Trae",
331
+ globalPath: "~/.trae/skills",
332
+ projectPath: ".trae/skills",
333
+ detectPath: "~/.trae",
334
+ universal: false,
335
+ },
336
+ {
337
+ id: "traecn",
338
+ name: "Trae CN",
339
+ shortName: "TraeCN",
340
+ globalPath: "~/.trae-cn/skills",
341
+ projectPath: ".trae/skills",
342
+ detectPath: "~/.trae-cn",
343
+ universal: false,
344
+ },
345
+ {
346
+ id: "zencoder",
347
+ name: "Zencoder",
348
+ shortName: "Zencoder",
349
+ globalPath: "~/.zencoder/skills",
350
+ projectPath: ".zencoder/skills",
351
+ detectPath: "~/.zencoder",
352
+ universal: false,
353
+ },
354
+ {
355
+ id: "neovate",
356
+ name: "Neovate",
357
+ shortName: "Neovate",
358
+ globalPath: "~/.neovate/skills",
359
+ projectPath: ".neovate/skills",
360
+ detectPath: "~/.neovate",
361
+ universal: false,
362
+ },
363
+ {
364
+ id: "pochi",
365
+ name: "Pochi",
366
+ shortName: "Pochi",
367
+ globalPath: "~/.pochi/skills",
368
+ projectPath: ".pochi/skills",
369
+ detectPath: "~/.pochi",
370
+ universal: false,
371
+ },
372
+ {
373
+ id: "adal",
374
+ name: "AdaL",
375
+ shortName: "AdaL",
376
+ globalPath: "~/.adal/skills",
377
+ projectPath: ".adal/skills",
378
+ detectPath: "~/.adal",
379
+ universal: false,
380
+ },
23
381
  ];
24
382
 
25
383
  const DEFAULT_AGENTS: Record<string, Agent> = Object.fromEntries(
26
- AGENT_DEFINITIONS.map(([id, name, shortName, globalPath, projectPath]) => [
27
- id,
28
- { id, name, shortName, globalPath, projectPath, detected: false },
384
+ AGENT_DEFINITIONS.map((definition) => [
385
+ definition.id,
386
+ {
387
+ id: definition.id,
388
+ name: definition.name,
389
+ shortName: definition.shortName,
390
+ globalPath: definition.globalPath,
391
+ projectPath: definition.projectPath,
392
+ detectPath: definition.detectPath,
393
+ detectPaths: definition.detectPaths,
394
+ universal: definition.universal,
395
+ detected: false,
396
+ },
29
397
  ]),
30
398
  );
31
399
 
400
+ // Map old agent IDs to new ones for config migration
401
+ const AGENT_RENAMES: Record<string, string> = {
402
+ clawdbot: "openclaw",
403
+ };
404
+
32
405
  function createDefaultConfig(): Config {
33
406
  return {
34
407
  agents: { ...DEFAULT_AGENTS },
@@ -69,17 +442,34 @@ export class ConfigStore {
69
442
  private mergeWithDefaults(parsed: Partial<Config>): Config {
70
443
  const defaults = createDefaultConfig();
71
444
 
72
- // Merge agents and ensure shortName exists for each
445
+ // Merge agents, handling renames and ensuring shortName/universal exist
73
446
  const mergedAgents = { ...defaults.agents };
74
447
  for (const [id, agent] of Object.entries(parsed.agents ?? {})) {
75
- mergedAgents[id] = {
76
- ...defaults.agents[id],
77
- ...agent,
78
- // Ensure shortName exists, fallback to first word of name or id
79
- shortName: agent.shortName ?? defaults.agents[id]?.shortName ?? agent.name?.split(" ")[0] ?? id,
448
+ const newId = AGENT_RENAMES[id] ?? id;
449
+
450
+ // Skip old ID if it was renamed and the new ID already has parsed data
451
+ if (AGENT_RENAMES[id] && parsed.agents?.[newId]) continue;
452
+
453
+ const isRenamed = id !== newId;
454
+ const def = defaults.agents[newId];
455
+ mergedAgents[newId] = {
456
+ ...def,
457
+ ...(isRenamed ? { detected: agent.detected } : agent),
458
+ id: newId,
459
+ // Defaults always win for display name and universal flag
460
+ name: def?.name ?? agent.name ?? newId,
461
+ shortName: def?.shortName ?? agent.shortName ?? agent.name?.split(" ")[0] ?? newId,
462
+ detectPath: def?.detectPath ?? agent.detectPath,
463
+ detectPaths: def?.detectPaths ?? agent.detectPaths,
464
+ universal: def?.universal ?? false,
80
465
  };
81
466
  }
82
467
 
468
+ // Remove stale renamed keys
469
+ for (const oldId of Object.keys(AGENT_RENAMES)) {
470
+ delete mergedAgents[oldId];
471
+ }
472
+
83
473
  return {
84
474
  agents: mergedAgents,
85
475
  sync: { ...defaults.sync, ...parsed.sync },
@@ -2,17 +2,58 @@ import { readFile, writeFile, mkdir } from "node:fs/promises"
2
2
  import { dirname } from "node:path"
3
3
  import type { Registry } from "./types"
4
4
 
5
+ const ASSIGNMENT_RENAMES: Record<string, string> = {
6
+ clawdbot: "openclaw",
7
+ }
8
+
5
9
  function createEmptyRegistry(): Registry {
6
10
  return { version: 1, skills: {} }
7
11
  }
8
12
 
13
+ function migrateAssignmentAgentIds(registry: Registry): Registry {
14
+ let changed = false
15
+ const migratedSkills: Registry["skills"] = {}
16
+
17
+ for (const [skillName, skill] of Object.entries(registry.skills)) {
18
+ const hasOpenClaw = Object.prototype.hasOwnProperty.call(skill.assignments, "openclaw")
19
+ let skillChanged = false
20
+ const migratedAssignments: typeof skill.assignments = {}
21
+
22
+ for (const [agentId, assignment] of Object.entries(skill.assignments)) {
23
+ const newAgentId = ASSIGNMENT_RENAMES[agentId] ?? agentId
24
+
25
+ if (newAgentId !== agentId) {
26
+ skillChanged = true
27
+ }
28
+
29
+ // If both old and new keys exist, keep explicit new key.
30
+ if (newAgentId === "openclaw" && hasOpenClaw && agentId !== "openclaw") {
31
+ continue
32
+ }
33
+
34
+ migratedAssignments[newAgentId] = assignment
35
+ }
36
+
37
+ if (skillChanged) {
38
+ changed = true
39
+ migratedSkills[skillName] = { ...skill, assignments: migratedAssignments }
40
+ } else {
41
+ migratedSkills[skillName] = skill
42
+ }
43
+ }
44
+
45
+ if (!changed) return registry
46
+ return { ...registry, skills: migratedSkills }
47
+ }
48
+
9
49
  export class RegistryStore {
10
50
  constructor(private registryPath: string) {}
11
51
 
12
52
  async load(): Promise<Registry> {
13
53
  try {
14
54
  const content = await readFile(this.registryPath, "utf-8")
15
- return JSON.parse(content) as Registry
55
+ const parsed = JSON.parse(content) as Registry
56
+ return migrateAssignmentAgentIds(parsed)
16
57
  } catch (err) {
17
58
  if ((err as NodeJS.ErrnoException).code === "ENOENT") {
18
59
  return createEmptyRegistry()
package/src/core/types.ts CHANGED
@@ -6,6 +6,9 @@ export interface Agent {
6
6
  shortName: string // Short display name for matrix views (max 8 chars)
7
7
  globalPath: string
8
8
  projectPath: string
9
+ detectPath?: string // CLI-owned config path used for installation detection
10
+ detectPaths?: string[] // Multiple detection markers; detected when any path exists
11
+ universal: boolean // Uses .agents/skills as project path
9
12
  detected: boolean
10
13
  }
11
14