@orderful/droid 0.10.4 → 0.11.0

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.
Files changed (167) hide show
  1. package/.claude/CLAUDE.md +13 -6
  2. package/CHANGELOG.md +21 -0
  3. package/README.md +23 -24
  4. package/dist/bin/droid.js +9 -9
  5. package/dist/bin/droid.js.map +1 -1
  6. package/dist/commands/install.d.ts +1 -1
  7. package/dist/commands/install.d.ts.map +1 -1
  8. package/dist/commands/install.js +24 -23
  9. package/dist/commands/install.js.map +1 -1
  10. package/dist/commands/setup.d.ts +3 -3
  11. package/dist/commands/setup.d.ts.map +1 -1
  12. package/dist/commands/setup.js +32 -28
  13. package/dist/commands/setup.js.map +1 -1
  14. package/dist/commands/skills.d.ts.map +1 -1
  15. package/dist/commands/skills.js +60 -53
  16. package/dist/commands/skills.js.map +1 -1
  17. package/dist/commands/tui.d.ts.map +1 -1
  18. package/dist/commands/tui.js +213 -319
  19. package/dist/commands/tui.js.map +1 -1
  20. package/dist/commands/uninstall.d.ts +1 -1
  21. package/dist/commands/uninstall.d.ts.map +1 -1
  22. package/dist/commands/uninstall.js +15 -6
  23. package/dist/commands/uninstall.js.map +1 -1
  24. package/dist/commands/update.d.ts +2 -2
  25. package/dist/commands/update.d.ts.map +1 -1
  26. package/dist/commands/update.js +9 -9
  27. package/dist/commands/update.js.map +1 -1
  28. package/dist/lib/agents.d.ts +11 -10
  29. package/dist/lib/agents.d.ts.map +1 -1
  30. package/dist/lib/agents.js +52 -54
  31. package/dist/lib/agents.js.map +1 -1
  32. package/dist/lib/config.d.ts +1 -0
  33. package/dist/lib/config.d.ts.map +1 -1
  34. package/dist/lib/config.js +42 -5
  35. package/dist/lib/config.js.map +1 -1
  36. package/dist/lib/platforms.d.ts +41 -0
  37. package/dist/lib/platforms.d.ts.map +1 -0
  38. package/dist/lib/platforms.js +52 -0
  39. package/dist/lib/platforms.js.map +1 -0
  40. package/dist/lib/skills.d.ts +19 -11
  41. package/dist/lib/skills.d.ts.map +1 -1
  42. package/dist/lib/skills.js +127 -106
  43. package/dist/lib/skills.js.map +1 -1
  44. package/dist/lib/tools.d.ts +30 -0
  45. package/dist/lib/tools.d.ts.map +1 -0
  46. package/dist/lib/tools.js +116 -0
  47. package/dist/lib/tools.js.map +1 -0
  48. package/dist/lib/types.d.ts +45 -2
  49. package/dist/lib/types.d.ts.map +1 -1
  50. package/dist/lib/types.js +24 -5
  51. package/dist/lib/types.js.map +1 -1
  52. package/dist/tools/brain/TOOL.yaml +27 -0
  53. package/dist/tools/coach/TOOL.yaml +21 -0
  54. package/dist/tools/code-review/TOOL.yaml +18 -0
  55. package/dist/{skills → tools}/code-review/commands/code-review.md +23 -1
  56. package/dist/tools/comments/TOOL.yaml +27 -0
  57. package/dist/tools/project/TOOL.yaml +26 -0
  58. package/package.json +2 -2
  59. package/src/bin/droid.ts +9 -9
  60. package/src/commands/install.ts +24 -23
  61. package/src/commands/setup.test.ts +2 -2
  62. package/src/commands/setup.ts +33 -29
  63. package/src/commands/skills.ts +63 -64
  64. package/src/commands/tui.tsx +432 -578
  65. package/src/commands/uninstall.ts +17 -6
  66. package/src/commands/update.ts +10 -10
  67. package/src/lib/agents.ts +58 -58
  68. package/src/lib/config.test.ts +0 -10
  69. package/src/lib/config.ts +47 -5
  70. package/src/lib/platforms.ts +59 -0
  71. package/src/lib/skills.test.ts +53 -28
  72. package/src/lib/skills.ts +136 -109
  73. package/src/lib/tools.ts +140 -0
  74. package/src/lib/types.test.ts +15 -7
  75. package/src/lib/types.ts +63 -2
  76. package/src/tools/brain/TOOL.yaml +27 -0
  77. package/src/tools/coach/TOOL.yaml +21 -0
  78. package/src/tools/code-review/TOOL.yaml +18 -0
  79. package/src/{skills → tools}/code-review/commands/code-review.md +23 -1
  80. package/src/tools/comments/TOOL.yaml +27 -0
  81. package/src/tools/project/TOOL.yaml +26 -0
  82. package/dist/agents/README.md +0 -137
  83. package/src/agents/README.md +0 -137
  84. /package/dist/{skills → tools}/README.md +0 -0
  85. /package/dist/{skills → tools}/brain/commands/README.md +0 -0
  86. /package/dist/{skills → tools}/brain/commands/brain.md +0 -0
  87. /package/dist/{skills → tools}/brain/commands/scratchpad.md +0 -0
  88. /package/dist/{skills → tools/brain/skills}/brain/SKILL.md +0 -0
  89. /package/dist/{skills → tools/brain/skills}/brain/SKILL.yaml +0 -0
  90. /package/dist/{skills → tools/brain/skills}/brain/references/metadata.md +0 -0
  91. /package/dist/{skills → tools/brain/skills}/brain/references/naming.md +0 -0
  92. /package/dist/{skills → tools/brain/skills}/brain/references/templates.md +0 -0
  93. /package/dist/{skills → tools/brain/skills}/brain/references/workflows.md +0 -0
  94. /package/dist/{skills → tools/brain/skills}/brain-obsidian/SKILL.md +0 -0
  95. /package/dist/{skills → tools/brain/skills}/brain-obsidian/SKILL.yaml +0 -0
  96. /package/dist/{skills → tools/brain/skills}/brain-obsidian/references/templates.md +0 -0
  97. /package/dist/{skills → tools/brain/skills}/brain-obsidian/references/workflows.md +0 -0
  98. /package/dist/{skills → tools}/coach/commands/README.md +0 -0
  99. /package/dist/{skills → tools}/coach/commands/coach.md +0 -0
  100. /package/dist/{skills → tools/coach/skills}/coach/SKILL.md +0 -0
  101. /package/dist/{skills → tools/coach/skills}/coach/SKILL.yaml +0 -0
  102. /package/dist/{skills → tools}/code-review/agents/edi-standards-reviewer/AGENT.md +0 -0
  103. /package/dist/{skills → tools}/code-review/agents/edi-standards-reviewer/AGENT.yaml +0 -0
  104. /package/dist/{skills → tools}/code-review/agents/error-handling-reviewer/AGENT.md +0 -0
  105. /package/dist/{skills → tools}/code-review/agents/error-handling-reviewer/AGENT.yaml +0 -0
  106. /package/dist/{skills → tools}/code-review/agents/test-coverage-analyzer/AGENT.md +0 -0
  107. /package/dist/{skills → tools}/code-review/agents/test-coverage-analyzer/AGENT.yaml +0 -0
  108. /package/dist/{skills → tools}/code-review/agents/type-reviewer/AGENT.md +0 -0
  109. /package/dist/{skills → tools}/code-review/agents/type-reviewer/AGENT.yaml +0 -0
  110. /package/dist/{skills → tools/code-review/skills}/code-review/SKILL.md +0 -0
  111. /package/dist/{skills → tools/code-review/skills}/code-review/SKILL.yaml +0 -0
  112. /package/dist/{skills → tools}/comments/commands/README.md +0 -0
  113. /package/dist/{skills → tools}/comments/commands/comments.md +0 -0
  114. /package/dist/{skills → tools/comments/skills}/comments/SKILL.md +0 -0
  115. /package/dist/{skills → tools/comments/skills}/comments/SKILL.yaml +0 -0
  116. /package/dist/{skills → tools}/project/commands/README.md +0 -0
  117. /package/dist/{skills → tools}/project/commands/project.md +0 -0
  118. /package/dist/{skills → tools/project/skills}/project/SKILL.md +0 -0
  119. /package/dist/{skills → tools/project/skills}/project/SKILL.yaml +0 -0
  120. /package/dist/{skills → tools/project/skills}/project/references/changelog.md +0 -0
  121. /package/dist/{skills → tools/project/skills}/project/references/creating.md +0 -0
  122. /package/dist/{skills → tools/project/skills}/project/references/loading.md +0 -0
  123. /package/dist/{skills → tools/project/skills}/project/references/templates.md +0 -0
  124. /package/dist/{skills → tools/project/skills}/project/references/updating.md +0 -0
  125. /package/dist/{skills → tools/project/skills}/project/references/versioning.md +0 -0
  126. /package/src/{skills → tools}/README.md +0 -0
  127. /package/src/{skills → tools}/brain/commands/README.md +0 -0
  128. /package/src/{skills → tools}/brain/commands/brain.md +0 -0
  129. /package/src/{skills → tools}/brain/commands/scratchpad.md +0 -0
  130. /package/src/{skills → tools/brain/skills}/brain/SKILL.md +0 -0
  131. /package/src/{skills → tools/brain/skills}/brain/SKILL.yaml +0 -0
  132. /package/src/{skills → tools/brain/skills}/brain/references/metadata.md +0 -0
  133. /package/src/{skills → tools/brain/skills}/brain/references/naming.md +0 -0
  134. /package/src/{skills → tools/brain/skills}/brain/references/templates.md +0 -0
  135. /package/src/{skills → tools/brain/skills}/brain/references/workflows.md +0 -0
  136. /package/src/{skills → tools/brain/skills}/brain-obsidian/SKILL.md +0 -0
  137. /package/src/{skills → tools/brain/skills}/brain-obsidian/SKILL.yaml +0 -0
  138. /package/src/{skills → tools/brain/skills}/brain-obsidian/references/templates.md +0 -0
  139. /package/src/{skills → tools/brain/skills}/brain-obsidian/references/workflows.md +0 -0
  140. /package/src/{skills → tools}/coach/commands/README.md +0 -0
  141. /package/src/{skills → tools}/coach/commands/coach.md +0 -0
  142. /package/src/{skills → tools/coach/skills}/coach/SKILL.md +0 -0
  143. /package/src/{skills → tools/coach/skills}/coach/SKILL.yaml +0 -0
  144. /package/src/{skills → tools}/code-review/agents/edi-standards-reviewer/AGENT.md +0 -0
  145. /package/src/{skills → tools}/code-review/agents/edi-standards-reviewer/AGENT.yaml +0 -0
  146. /package/src/{skills → tools}/code-review/agents/error-handling-reviewer/AGENT.md +0 -0
  147. /package/src/{skills → tools}/code-review/agents/error-handling-reviewer/AGENT.yaml +0 -0
  148. /package/src/{skills → tools}/code-review/agents/test-coverage-analyzer/AGENT.md +0 -0
  149. /package/src/{skills → tools}/code-review/agents/test-coverage-analyzer/AGENT.yaml +0 -0
  150. /package/src/{skills → tools}/code-review/agents/type-reviewer/AGENT.md +0 -0
  151. /package/src/{skills → tools}/code-review/agents/type-reviewer/AGENT.yaml +0 -0
  152. /package/src/{skills → tools/code-review/skills}/code-review/SKILL.md +0 -0
  153. /package/src/{skills → tools/code-review/skills}/code-review/SKILL.yaml +0 -0
  154. /package/src/{skills → tools}/comments/commands/README.md +0 -0
  155. /package/src/{skills → tools}/comments/commands/comments.md +0 -0
  156. /package/src/{skills → tools/comments/skills}/comments/SKILL.md +0 -0
  157. /package/src/{skills → tools/comments/skills}/comments/SKILL.yaml +0 -0
  158. /package/src/{skills → tools}/project/commands/README.md +0 -0
  159. /package/src/{skills → tools}/project/commands/project.md +0 -0
  160. /package/src/{skills → tools/project/skills}/project/SKILL.md +0 -0
  161. /package/src/{skills → tools/project/skills}/project/SKILL.yaml +0 -0
  162. /package/src/{skills → tools/project/skills}/project/references/changelog.md +0 -0
  163. /package/src/{skills → tools/project/skills}/project/references/creating.md +0 -0
  164. /package/src/{skills → tools/project/skills}/project/references/loading.md +0 -0
  165. /package/src/{skills → tools/project/skills}/project/references/templates.md +0 -0
  166. /package/src/{skills → tools/project/skills}/project/references/updating.md +0 -0
  167. /package/src/{skills → tools/project/skills}/project/references/versioning.md +0 -0
package/src/lib/skills.ts CHANGED
@@ -1,67 +1,52 @@
1
1
  import { existsSync, readdirSync, readFileSync, mkdirSync, writeFileSync, rmSync } from 'fs';
2
2
  import { join, dirname } from 'path';
3
- import { homedir } from 'os';
4
3
  import { fileURLToPath } from 'url';
5
4
  import YAML from 'yaml';
6
5
  import { loadConfig, saveConfig } from './config.js';
7
- import { AITool, SkillStatus, type SkillManifest, type InstalledSkill } from './types.js';
6
+ import { Platform, SkillStatus, type SkillManifest, type InstalledSkill, getPlatformTools, setPlatformTools } from './types.js';
8
7
  import { getInstalledAgentsDir, installAgentFromPath, uninstallAgent, isAgentInstalled } from './agents.js';
8
+ import { getSkillsPath, getCommandsPath, getConfigPath } from './platforms.js';
9
9
 
10
10
  // Marker comments for CLAUDE.md skill registration
11
11
  const DROID_SKILLS_START = '<!-- droid-skills-start -->';
12
12
  const DROID_SKILLS_END = '<!-- droid-skills-end -->';
13
13
 
14
14
  const __dirname = dirname(fileURLToPath(import.meta.url));
15
- const BUNDLED_SKILLS_DIR = join(__dirname, '../skills');
15
+ const BUNDLED_SKILLS_DIR = join(__dirname, '../tools');
16
16
 
17
17
  /**
18
- * Get the path to bundled skills directory
18
+ * Get the path to bundled tools directory
19
19
  */
20
20
  export function getBundledSkillsDir(): string {
21
21
  return BUNDLED_SKILLS_DIR;
22
22
  }
23
23
 
24
24
  /**
25
- * Get the installation path for skills based on AI tool
25
+ * Get the installation path for skills based on platform
26
26
  */
27
- export function getSkillsInstallPath(aiTool: AITool): string {
28
- switch (aiTool) {
29
- case AITool.ClaudeCode:
30
- return join(homedir(), '.claude', 'skills');
31
- case AITool.OpenCode:
32
- return join(homedir(), '.config', 'opencode', 'skills');
33
- }
27
+ export function getSkillsInstallPath(platform: Platform): string {
28
+ return getSkillsPath(platform);
34
29
  }
35
30
 
36
31
  /**
37
- * Get the commands installation path based on AI tool
32
+ * Get the commands installation path based on platform
38
33
  */
39
- export function getCommandsInstallPath(aiTool: AITool): string {
40
- switch (aiTool) {
41
- case AITool.ClaudeCode:
42
- return join(homedir(), '.claude', 'commands');
43
- case AITool.OpenCode:
44
- return join(homedir(), '.config', 'opencode', 'command');
45
- }
34
+ export function getCommandsInstallPath(platform: Platform): string {
35
+ return getCommandsPath(platform);
46
36
  }
47
37
 
48
38
  /**
49
- * Get the path to the AI tool's main config markdown file
39
+ * Get the path to the platform's main config markdown file
50
40
  */
51
- export function getAIConfigPath(aiTool: AITool): string {
52
- switch (aiTool) {
53
- case AITool.ClaudeCode:
54
- return join(homedir(), '.claude', 'CLAUDE.md');
55
- case AITool.OpenCode:
56
- return join(homedir(), '.config', 'opencode', 'AGENTS.md');
57
- }
41
+ export function getPlatformConfigPath(platform: Platform): string {
42
+ return getConfigPath(platform);
58
43
  }
59
44
 
60
45
  /**
61
- * Update the AI tool's config file with skill registrations
46
+ * Update the platform's config file with skill registrations
62
47
  */
63
- export function updateAIConfigSkills(aiTool: AITool, installedSkills: string[]): void {
64
- const configPath = getAIConfigPath(aiTool);
48
+ export function updatePlatformConfigSkills(platform: Platform, installedSkills: string[]): void {
49
+ const configPath = getPlatformConfigPath(platform);
65
50
 
66
51
  let content = '';
67
52
  if (existsSync(configPath)) {
@@ -118,23 +103,61 @@ export function loadSkillManifest(skillDir: string): SkillManifest | null {
118
103
  }
119
104
 
120
105
  /**
121
- * Get all bundled skills
106
+ * Find the path to a skill within the tools directory structure
107
+ * Returns { toolDir, skillDir } or null if not found
108
+ */
109
+ export function findSkillPath(skillName: string): { toolDir: string; skillDir: string } | null {
110
+ if (!existsSync(BUNDLED_SKILLS_DIR)) {
111
+ return null;
112
+ }
113
+
114
+ const toolDirs = readdirSync(BUNDLED_SKILLS_DIR, { withFileTypes: true })
115
+ .filter((dirent) => dirent.isDirectory())
116
+ .map((dirent) => dirent.name);
117
+
118
+ for (const toolName of toolDirs) {
119
+ const skillsDir = join(BUNDLED_SKILLS_DIR, toolName, 'skills');
120
+ if (!existsSync(skillsDir)) continue;
121
+
122
+ const skillDir = join(skillsDir, skillName);
123
+ if (existsSync(skillDir) && existsSync(join(skillDir, 'SKILL.yaml'))) {
124
+ return {
125
+ toolDir: join(BUNDLED_SKILLS_DIR, toolName),
126
+ skillDir,
127
+ };
128
+ }
129
+ }
130
+
131
+ return null;
132
+ }
133
+
134
+ /**
135
+ * Get all bundled skills from all tools
122
136
  */
123
137
  export function getBundledSkills(): SkillManifest[] {
124
138
  if (!existsSync(BUNDLED_SKILLS_DIR)) {
125
139
  return [];
126
140
  }
127
141
 
128
- const skillDirs = readdirSync(BUNDLED_SKILLS_DIR, { withFileTypes: true })
142
+ const toolDirs = readdirSync(BUNDLED_SKILLS_DIR, { withFileTypes: true })
129
143
  .filter((dirent) => dirent.isDirectory())
130
144
  .map((dirent) => dirent.name);
131
145
 
132
146
  const skills: SkillManifest[] = [];
133
147
 
134
- for (const skillDir of skillDirs) {
135
- const manifest = loadSkillManifest(join(BUNDLED_SKILLS_DIR, skillDir));
136
- if (manifest) {
137
- skills.push(manifest);
148
+ for (const toolName of toolDirs) {
149
+ const skillsDir = join(BUNDLED_SKILLS_DIR, toolName, 'skills');
150
+ if (!existsSync(skillsDir)) continue;
151
+
152
+ const skillSubdirs = readdirSync(skillsDir, { withFileTypes: true })
153
+ .filter((dirent) => dirent.isDirectory())
154
+ .map((dirent) => dirent.name);
155
+
156
+ for (const skillName of skillSubdirs) {
157
+ const manifest = loadSkillManifest(join(skillsDir, skillName));
158
+ if (manifest) {
159
+ skills.push(manifest);
160
+ }
138
161
  }
139
162
  }
140
163
 
@@ -146,7 +169,8 @@ export function getBundledSkills(): SkillManifest[] {
146
169
  */
147
170
  export function isSkillInstalled(skillName: string): boolean {
148
171
  const config = loadConfig();
149
- return skillName in config.skills;
172
+ const tools = getPlatformTools(config);
173
+ return skillName in tools;
150
174
  }
151
175
 
152
176
  /**
@@ -154,7 +178,8 @@ export function isSkillInstalled(skillName: string): boolean {
154
178
  */
155
179
  export function getInstalledSkill(skillName: string): InstalledSkill | null {
156
180
  const config = loadConfig();
157
- return config.skills[skillName] || null;
181
+ const tools = getPlatformTools(config);
182
+ return tools[skillName] || null;
158
183
  }
159
184
 
160
185
  /**
@@ -166,8 +191,8 @@ export function getSkillUpdateStatus(skillName: string): {
166
191
  bundledVersion: string | null;
167
192
  } {
168
193
  const installed = getInstalledSkill(skillName);
169
- const bundledSkillDir = join(BUNDLED_SKILLS_DIR, skillName);
170
- const manifest = existsSync(bundledSkillDir) ? loadSkillManifest(bundledSkillDir) : null;
194
+ const skillPath = findSkillPath(skillName);
195
+ const manifest = skillPath ? loadSkillManifest(skillPath.skillDir) : null;
171
196
 
172
197
  if (!installed || !manifest) {
173
198
  return {
@@ -193,9 +218,10 @@ export function getSkillsWithUpdates(): Array<{
193
218
  bundledVersion: string;
194
219
  }> {
195
220
  const config = loadConfig();
221
+ const tools = getPlatformTools(config);
196
222
  const updates: Array<{ name: string; installedVersion: string; bundledVersion: string }> = [];
197
223
 
198
- for (const skillName of Object.keys(config.skills)) {
224
+ for (const skillName of Object.keys(tools)) {
199
225
  const status = getSkillUpdateStatus(skillName);
200
226
  if (status.hasUpdate && status.installedVersion && status.bundledVersion) {
201
227
  updates.push({
@@ -248,13 +274,14 @@ export function updateAllSkills(): {
248
274
  upToDate: number;
249
275
  } {
250
276
  const config = loadConfig();
277
+ const tools = getPlatformTools(config);
251
278
  const result = {
252
279
  updated: [] as Array<{ name: string; from: string; to: string }>,
253
280
  failed: [] as Array<{ name: string; error: string }>,
254
281
  upToDate: 0,
255
282
  };
256
283
 
257
- for (const skillName of Object.keys(config.skills)) {
284
+ for (const skillName of Object.keys(tools)) {
258
285
  const status = getSkillUpdateStatus(skillName);
259
286
 
260
287
  if (!status.hasUpdate) {
@@ -285,13 +312,14 @@ export function updateAllSkills(): {
285
312
  */
286
313
  export function installSkill(skillName: string): { success: boolean; message: string } {
287
314
  const config = loadConfig();
288
- const bundledSkillDir = join(BUNDLED_SKILLS_DIR, skillName);
315
+ const skillPath = findSkillPath(skillName);
289
316
 
290
- if (!existsSync(bundledSkillDir)) {
317
+ if (!skillPath) {
291
318
  return { success: false, message: `Skill '${skillName}' not found` };
292
319
  }
293
320
 
294
- const manifest = loadSkillManifest(bundledSkillDir);
321
+ const { toolDir, skillDir } = skillPath;
322
+ const manifest = loadSkillManifest(skillDir);
295
323
  if (!manifest) {
296
324
  return { success: false, message: `Invalid skill manifest for '${skillName}'` };
297
325
  }
@@ -308,22 +336,20 @@ export function installSkill(skillName: string): { success: boolean; message: st
308
336
  }
309
337
  }
310
338
 
311
- const skillsPath = getSkillsInstallPath(config.ai_tool);
339
+ const skillsPath = getSkillsInstallPath(config.platform);
312
340
  const targetSkillDir = join(skillsPath, skillName);
313
- const commandsPath = getCommandsInstallPath(config.ai_tool);
341
+ const commandsPath = getCommandsInstallPath(config.platform);
342
+ const tools = getPlatformTools(config);
314
343
 
315
- // Check for collisions BEFORE installing (only if not already installed by droid)
316
- if (!config.skills[skillName]) {
317
- // Check skill folder collision
318
- if (existsSync(targetSkillDir)) {
319
- return {
320
- success: false,
321
- message: `Cannot install: skill folder '${skillName}' already exists at ${targetSkillDir}`,
322
- };
323
- }
344
+ // Commands and agents are at the tool level, not skill level
345
+ const commandsSource = join(toolDir, 'commands');
346
+ const agentsSource = join(toolDir, 'agents');
324
347
 
325
- // Check command file collisions
326
- const commandsSource = join(bundledSkillDir, 'commands');
348
+ // Check for collisions BEFORE installing (only if not already installed by droid)
349
+ // Note: If skill folder exists but skill isn't in config, we allow overwriting (stale state
350
+ // from platform switch or manual cleanup). Command/agent collisions still checked.
351
+ if (!tools[skillName]) {
352
+ // Check command file collisions (these could conflict with other skills)
327
353
  if (existsSync(commandsSource)) {
328
354
  const commandFiles = readdirSync(commandsSource).filter(f => f.endsWith('.md') && f.toLowerCase() !== 'readme.md');
329
355
  for (const file of commandFiles) {
@@ -339,7 +365,6 @@ export function installSkill(skillName: string): { success: boolean; message: st
339
365
  }
340
366
 
341
367
  // Check bundled agent collisions
342
- const agentsSource = join(bundledSkillDir, 'agents');
343
368
  if (existsSync(agentsSource)) {
344
369
  const agentDirs = readdirSync(agentsSource, { withFileTypes: true })
345
370
  .filter(dirent => dirent.isDirectory())
@@ -361,7 +386,7 @@ export function installSkill(skillName: string): { success: boolean; message: st
361
386
  }
362
387
 
363
388
  // Copy SKILL.md (the actual skill file for Claude Code / OpenCode)
364
- const skillMdSource = join(bundledSkillDir, 'SKILL.md');
389
+ const skillMdSource = join(skillDir, 'SKILL.md');
365
390
  if (existsSync(skillMdSource)) {
366
391
  if (!existsSync(targetSkillDir)) {
367
392
  mkdirSync(targetSkillDir, { recursive: true });
@@ -372,7 +397,7 @@ export function installSkill(skillName: string): { success: boolean; message: st
372
397
  }
373
398
 
374
399
  // Copy references if present (skill documentation files)
375
- const referencesSource = join(bundledSkillDir, 'references');
400
+ const referencesSource = join(skillDir, 'references');
376
401
  if (existsSync(referencesSource)) {
377
402
  const targetReferencesDir = join(targetSkillDir, 'references');
378
403
  if (!existsSync(targetReferencesDir)) {
@@ -387,8 +412,7 @@ export function installSkill(skillName: string): { success: boolean; message: st
387
412
  }
388
413
  }
389
414
 
390
- // Copy commands if present
391
- const commandsSource = join(bundledSkillDir, 'commands');
415
+ // Copy commands if present (from tool level)
392
416
  if (existsSync(commandsSource)) {
393
417
  if (!existsSync(commandsPath)) {
394
418
  mkdirSync(commandsPath, { recursive: true });
@@ -402,16 +426,14 @@ export function installSkill(skillName: string): { success: boolean; message: st
402
426
  }
403
427
  }
404
428
 
405
- // Install bundled agents if present
429
+ // Install bundled agents if present (from tool level)
406
430
  const installedAgents: string[] = [];
407
- const agentsSource = join(bundledSkillDir, 'agents');
408
431
  if (existsSync(agentsSource)) {
409
432
  const agentDirs = readdirSync(agentsSource, { withFileTypes: true })
410
433
  .filter(dirent => dirent.isDirectory())
411
434
  .map(dirent => dirent.name);
412
435
 
413
436
  for (const agentName of agentDirs) {
414
- // Use the skill's bundled agent path instead of global agents
415
437
  const agentDir = join(agentsSource, agentName);
416
438
  const result = installAgentFromPath(agentDir, agentName);
417
439
  if (result.success) {
@@ -421,16 +443,20 @@ export function installSkill(skillName: string): { success: boolean; message: st
421
443
  }
422
444
 
423
445
  // Update config
424
- config.skills[skillName] = {
425
- version: manifest.version,
426
- installed_at: new Date().toISOString(),
427
- ...(installedAgents.length > 0 && { bundled_agents: installedAgents }),
446
+ const updatedTools = {
447
+ ...tools,
448
+ [skillName]: {
449
+ version: manifest.version,
450
+ installed_at: new Date().toISOString(),
451
+ ...(installedAgents.length > 0 && { bundled_agents: installedAgents }),
452
+ },
428
453
  };
454
+ setPlatformTools(config, updatedTools);
429
455
  saveConfig(config);
430
456
 
431
- // Update AI tool's config file with skill reference
432
- const installedSkillNames = Object.keys(config.skills);
433
- updateAIConfigSkills(config.ai_tool, installedSkillNames);
457
+ // Update platform's config file with skill reference
458
+ const installedSkillNames = Object.keys(updatedTools);
459
+ updatePlatformConfigSkills(config.platform, installedSkillNames);
434
460
 
435
461
  return { success: true, message: `Installed ${skillName} v${manifest.version}` };
436
462
  }
@@ -440,23 +466,24 @@ export function installSkill(skillName: string): { success: boolean; message: st
440
466
  */
441
467
  export function uninstallSkill(skillName: string): { success: boolean; message: string } {
442
468
  const config = loadConfig();
469
+ const tools = getPlatformTools(config);
443
470
 
444
471
  if (!isSkillInstalled(skillName)) {
445
472
  return { success: false, message: `Skill '${skillName}' is not installed` };
446
473
  }
447
474
 
448
- // Remove skill files from AI tool location
449
- const skillsPath = getSkillsInstallPath(config.ai_tool);
475
+ // Remove skill files from platform location
476
+ const skillsPath = getSkillsInstallPath(config.platform);
450
477
  const skillDir = join(skillsPath, skillName);
451
478
  if (existsSync(skillDir)) {
452
479
  rmSync(skillDir, { recursive: true });
453
480
  }
454
481
 
455
- // Remove command files if they exist
456
- const bundledSkillDir = join(BUNDLED_SKILLS_DIR, skillName);
457
- const commandsSource = join(bundledSkillDir, 'commands');
458
- const commandsPath = getCommandsInstallPath(config.ai_tool);
459
- if (existsSync(commandsSource)) {
482
+ // Remove command files if they exist (commands are at tool level)
483
+ const skillPath = findSkillPath(skillName);
484
+ const commandsPath = getCommandsInstallPath(config.platform);
485
+ const commandsSource = skillPath ? join(skillPath.toolDir, 'commands') : null;
486
+ if (commandsSource && existsSync(commandsSource)) {
460
487
  const commandFiles = readdirSync(commandsSource).filter(f => f.endsWith('.md') && f.toLowerCase() !== 'readme.md');
461
488
  for (const file of commandFiles) {
462
489
  const commandPath = join(commandsPath, file);
@@ -467,20 +494,22 @@ export function uninstallSkill(skillName: string): { success: boolean; message:
467
494
  }
468
495
 
469
496
  // Remove bundled agents if they were installed with this skill
470
- const installedSkillInfo = config.skills[skillName];
497
+ const installedSkillInfo = tools[skillName];
471
498
  if (installedSkillInfo?.bundled_agents) {
472
499
  for (const agentName of installedSkillInfo.bundled_agents) {
473
500
  uninstallAgent(agentName);
474
501
  }
475
502
  }
476
503
 
477
- // Remove from config
478
- delete config.skills[skillName];
504
+ // Remove from config (destructure to omit the skill being removed)
505
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
506
+ const { [skillName]: removed, ...remainingTools } = tools;
507
+ setPlatformTools(config, remainingTools);
479
508
  saveConfig(config);
480
509
 
481
- // Update AI tool's config file to remove skill reference
482
- const installedSkillNames = Object.keys(config.skills);
483
- updateAIConfigSkills(config.ai_tool, installedSkillNames);
510
+ // Update platform's config file to remove skill reference
511
+ const installedSkillNames = Object.keys(remainingTools);
512
+ updatePlatformConfigSkills(config.platform, installedSkillNames);
484
513
 
485
514
  return { success: true, message: `Uninstalled ${skillName}` };
486
515
  }
@@ -513,7 +542,7 @@ export function isCommandInstalled(commandName: string, skillName: string): bool
513
542
 
514
543
  // Otherwise check if command was installed standalone
515
544
  const config = loadConfig();
516
- const commandsPath = getCommandsInstallPath(config.ai_tool);
545
+ const commandsPath = getCommandsInstallPath(config.platform);
517
546
 
518
547
  // Command filename is derived from the command name after the skill prefix
519
548
  // e.g., "comments check" from skill "comments" → "check.md"
@@ -534,31 +563,23 @@ export function installCommand(
534
563
  ): { success: boolean; message: string } {
535
564
  const config = loadConfig();
536
565
 
566
+ // Find the skill to get its tool directory (commands are at tool level)
567
+ const skillPath = findSkillPath(skillName);
568
+ if (!skillPath) {
569
+ return { success: false, message: `Skill '${skillName}' not found` };
570
+ }
571
+
572
+ const commandsDir = join(skillPath.toolDir, 'commands');
573
+ if (!existsSync(commandsDir)) {
574
+ return { success: false, message: `No commands found for skill '${skillName}'` };
575
+ }
576
+
537
577
  // Find the source command file
538
578
  const cmdPart = commandName.startsWith(skillName + ' ')
539
579
  ? commandName.slice(skillName.length + 1)
540
580
  : commandName;
541
- const filename = cmdPart.replace(/\s+/g, '-') + '.md';
542
- const sourcePath = join(BUNDLED_SKILLS_DIR, skillName, 'commands', filename);
543
-
544
- if (!existsSync(sourcePath)) {
545
- // Try without hyphen conversion (original filename)
546
- const altFilename = cmdPart + '.md';
547
- const altSourcePath = join(BUNDLED_SKILLS_DIR, skillName, 'commands', altFilename);
548
- if (!existsSync(altSourcePath)) {
549
- return { success: false, message: `Command file not found: ${filename}` };
550
- }
551
- }
552
-
553
- // Ensure commands directory exists
554
- const commandsPath = getCommandsInstallPath(config.ai_tool);
555
- if (!existsSync(commandsPath)) {
556
- mkdirSync(commandsPath, { recursive: true });
557
- }
558
581
 
559
- // Find the actual source file
560
- const commandsDir = join(BUNDLED_SKILLS_DIR, skillName, 'commands');
561
- const files = readdirSync(commandsDir);
582
+ const files = readdirSync(commandsDir).filter(f => f.endsWith('.md') && f.toLowerCase() !== 'readme.md');
562
583
  const sourceFile = files.find(f => {
563
584
  const base = f.replace('.md', '');
564
585
  return base === cmdPart || base === cmdPart.replace(/\s+/g, '-');
@@ -568,6 +589,12 @@ export function installCommand(
568
589
  return { success: false, message: `Command file not found for: ${commandName}` };
569
590
  }
570
591
 
592
+ // Ensure commands directory exists
593
+ const commandsPath = getCommandsInstallPath(config.platform);
594
+ if (!existsSync(commandsPath)) {
595
+ mkdirSync(commandsPath, { recursive: true });
596
+ }
597
+
571
598
  const actualSourcePath = join(commandsDir, sourceFile);
572
599
  const targetPath = join(commandsPath, sourceFile);
573
600
 
@@ -597,7 +624,7 @@ export function uninstallCommand(
597
624
  };
598
625
  }
599
626
 
600
- const commandsPath = getCommandsInstallPath(config.ai_tool);
627
+ const commandsPath = getCommandsInstallPath(config.platform);
601
628
 
602
629
  // Find the installed command file
603
630
  const cmdPart = commandName.startsWith(skillName + ' ')
@@ -0,0 +1,140 @@
1
+ import { existsSync, readdirSync, readFileSync } from 'fs';
2
+ import { join, dirname } from 'path';
3
+ import { fileURLToPath } from 'url';
4
+ import YAML from 'yaml';
5
+ import { loadConfig } from './config.js';
6
+ import { type ToolManifest, type ToolIncludes, getPlatformTools } from './types.js';
7
+
8
+ const __dirname = dirname(fileURLToPath(import.meta.url));
9
+ const BUNDLED_TOOLS_DIR = join(__dirname, '../tools');
10
+
11
+ /**
12
+ * Get the path to bundled tools directory
13
+ */
14
+ export function getBundledToolsDir(): string {
15
+ return BUNDLED_TOOLS_DIR;
16
+ }
17
+
18
+ /**
19
+ * Load a tool manifest from a tool directory
20
+ */
21
+ export function loadToolManifest(toolDir: string): ToolManifest | null {
22
+ const manifestPath = join(toolDir, 'TOOL.yaml');
23
+
24
+ if (!existsSync(manifestPath)) {
25
+ return null;
26
+ }
27
+
28
+ try {
29
+ const content = readFileSync(manifestPath, 'utf-8');
30
+ const parsed = YAML.parse(content);
31
+
32
+ // Normalize includes structure
33
+ const includes: ToolIncludes = {
34
+ skills: parsed.includes?.skills || [],
35
+ commands: parsed.includes?.commands || [],
36
+ agents: parsed.includes?.agents || [],
37
+ };
38
+
39
+ return {
40
+ ...parsed,
41
+ includes,
42
+ } as ToolManifest;
43
+ } catch {
44
+ return null;
45
+ }
46
+ }
47
+
48
+ /**
49
+ * Get all bundled tools
50
+ */
51
+ export function getBundledTools(): ToolManifest[] {
52
+ if (!existsSync(BUNDLED_TOOLS_DIR)) {
53
+ return [];
54
+ }
55
+
56
+ const toolDirs = readdirSync(BUNDLED_TOOLS_DIR, { withFileTypes: true })
57
+ .filter((dirent) => dirent.isDirectory())
58
+ .map((dirent) => dirent.name);
59
+
60
+ const tools: ToolManifest[] = [];
61
+
62
+ for (const toolName of toolDirs) {
63
+ const manifest = loadToolManifest(join(BUNDLED_TOOLS_DIR, toolName));
64
+ if (manifest) {
65
+ tools.push(manifest);
66
+ }
67
+ }
68
+
69
+ return tools;
70
+ }
71
+
72
+ /**
73
+ * Check if a tool is installed (any of its required skills are installed)
74
+ */
75
+ export function isToolInstalled(toolName: string): boolean {
76
+ const config = loadConfig();
77
+ const installedTools = getPlatformTools(config);
78
+
79
+ // A tool is installed if any of its required skills are in the installed tools
80
+ const tool = getBundledTools().find(t => t.name === toolName);
81
+ if (!tool) return false;
82
+
83
+ const requiredSkills = tool.includes.skills
84
+ .filter(s => s.required)
85
+ .map(s => s.name);
86
+
87
+ // Tool is installed if at least one required skill is installed
88
+ return requiredSkills.some(skillName => skillName in installedTools);
89
+ }
90
+
91
+ /**
92
+ * Get the installed version of a tool (from its primary skill)
93
+ */
94
+ export function getInstalledToolVersion(toolName: string): string | null {
95
+ const config = loadConfig();
96
+ const installedTools = getPlatformTools(config);
97
+
98
+ const tool = getBundledTools().find(t => t.name === toolName);
99
+ if (!tool) return null;
100
+
101
+ // Get version from the first installed required skill
102
+ const requiredSkills = tool.includes.skills
103
+ .filter(s => s.required)
104
+ .map(s => s.name);
105
+
106
+ for (const skillName of requiredSkills) {
107
+ if (installedTools[skillName]) {
108
+ return installedTools[skillName].version;
109
+ }
110
+ }
111
+
112
+ return null;
113
+ }
114
+
115
+ /**
116
+ * Check if a tool has an update available
117
+ */
118
+ export function getToolUpdateStatus(toolName: string): {
119
+ hasUpdate: boolean;
120
+ installedVersion: string | null;
121
+ bundledVersion: string | null;
122
+ } {
123
+ const installedVersion = getInstalledToolVersion(toolName);
124
+ const tool = getBundledTools().find(t => t.name === toolName);
125
+ const bundledVersion = tool?.version || null;
126
+
127
+ if (!installedVersion || !bundledVersion) {
128
+ return {
129
+ hasUpdate: false,
130
+ installedVersion,
131
+ bundledVersion,
132
+ };
133
+ }
134
+
135
+ return {
136
+ hasUpdate: bundledVersion !== installedVersion,
137
+ installedVersion,
138
+ bundledVersion,
139
+ };
140
+ }
@@ -1,5 +1,6 @@
1
1
  import { describe, it, expect } from 'bun:test';
2
2
  import {
3
+ Platform,
3
4
  AITool,
4
5
  BuiltInOutput,
5
6
  SkillStatus,
@@ -7,21 +8,21 @@ import {
7
8
  getAITag,
8
9
  } from './types.js';
9
10
 
10
- describe('AITool enum', () => {
11
+ describe('Platform enum', () => {
11
12
  it('should have correct values', () => {
12
- expect(AITool.ClaudeCode).toBe('claude-code');
13
- expect(AITool.OpenCode).toBe('opencode');
13
+ expect(Platform.ClaudeCode).toBe('claude-code');
14
+ expect(Platform.OpenCode).toBe('opencode');
14
15
  });
15
16
 
16
17
  it('should be usable in switch statements', () => {
17
- const tool = AITool.ClaudeCode;
18
+ const platform = Platform.ClaudeCode;
18
19
  let result = '';
19
20
 
20
- switch (tool) {
21
- case AITool.ClaudeCode:
21
+ switch (platform) {
22
+ case Platform.ClaudeCode:
22
23
  result = 'claude';
23
24
  break;
24
- case AITool.OpenCode:
25
+ case Platform.OpenCode:
25
26
  result = 'opencode';
26
27
  break;
27
28
  }
@@ -30,6 +31,13 @@ describe('AITool enum', () => {
30
31
  });
31
32
  });
32
33
 
34
+ describe('AITool backward compatibility', () => {
35
+ it('should be an alias for Platform', () => {
36
+ expect(AITool.ClaudeCode).toBe(Platform.ClaudeCode);
37
+ expect(AITool.OpenCode).toBe(Platform.OpenCode);
38
+ });
39
+ });
40
+
33
41
  describe('BuiltInOutput enum', () => {
34
42
  it('should have correct values', () => {
35
43
  expect(BuiltInOutput.Terminal).toBe('terminal');