@orderful/droid 0.10.5 → 0.11.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.
Files changed (167) hide show
  1. package/.claude/CLAUDE.md +13 -6
  2. package/CHANGELOG.md +19 -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 +125 -99
  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/tools/comments/TOOL.yaml +27 -0
  56. package/dist/{skills → tools/comments/skills}/comments/SKILL.md +12 -3
  57. package/dist/{skills → tools/comments/skills}/comments/SKILL.yaml +1 -1
  58. package/dist/tools/project/TOOL.yaml +26 -0
  59. package/package.json +2 -2
  60. package/src/bin/droid.ts +9 -9
  61. package/src/commands/install.ts +24 -23
  62. package/src/commands/setup.test.ts +2 -2
  63. package/src/commands/setup.ts +33 -29
  64. package/src/commands/skills.ts +63 -64
  65. package/src/commands/tui.tsx +432 -578
  66. package/src/commands/uninstall.ts +17 -6
  67. package/src/commands/update.ts +10 -10
  68. package/src/lib/agents.ts +58 -58
  69. package/src/lib/config.test.ts +0 -10
  70. package/src/lib/config.ts +47 -5
  71. package/src/lib/platforms.ts +59 -0
  72. package/src/lib/skills.test.ts +53 -28
  73. package/src/lib/skills.ts +134 -101
  74. package/src/lib/tools.ts +140 -0
  75. package/src/lib/types.test.ts +15 -7
  76. package/src/lib/types.ts +63 -2
  77. package/src/tools/brain/TOOL.yaml +27 -0
  78. package/src/tools/coach/TOOL.yaml +21 -0
  79. package/src/tools/code-review/TOOL.yaml +18 -0
  80. package/src/tools/comments/TOOL.yaml +27 -0
  81. package/src/{skills → tools/comments/skills}/comments/SKILL.md +12 -3
  82. package/src/{skills → tools/comments/skills}/comments/SKILL.yaml +1 -1
  83. package/src/tools/project/TOOL.yaml +26 -0
  84. package/dist/agents/README.md +0 -137
  85. package/src/agents/README.md +0 -137
  86. /package/dist/{skills → tools}/README.md +0 -0
  87. /package/dist/{skills → tools}/brain/commands/README.md +0 -0
  88. /package/dist/{skills → tools}/brain/commands/brain.md +0 -0
  89. /package/dist/{skills → tools}/brain/commands/scratchpad.md +0 -0
  90. /package/dist/{skills → tools/brain/skills}/brain/SKILL.md +0 -0
  91. /package/dist/{skills → tools/brain/skills}/brain/SKILL.yaml +0 -0
  92. /package/dist/{skills → tools/brain/skills}/brain/references/metadata.md +0 -0
  93. /package/dist/{skills → tools/brain/skills}/brain/references/naming.md +0 -0
  94. /package/dist/{skills → tools/brain/skills}/brain/references/templates.md +0 -0
  95. /package/dist/{skills → tools/brain/skills}/brain/references/workflows.md +0 -0
  96. /package/dist/{skills → tools/brain/skills}/brain-obsidian/SKILL.md +0 -0
  97. /package/dist/{skills → tools/brain/skills}/brain-obsidian/SKILL.yaml +0 -0
  98. /package/dist/{skills → tools/brain/skills}/brain-obsidian/references/templates.md +0 -0
  99. /package/dist/{skills → tools/brain/skills}/brain-obsidian/references/workflows.md +0 -0
  100. /package/dist/{skills → tools}/coach/commands/README.md +0 -0
  101. /package/dist/{skills → tools}/coach/commands/coach.md +0 -0
  102. /package/dist/{skills → tools/coach/skills}/coach/SKILL.md +0 -0
  103. /package/dist/{skills → tools/coach/skills}/coach/SKILL.yaml +0 -0
  104. /package/dist/{skills → tools}/code-review/agents/edi-standards-reviewer/AGENT.md +0 -0
  105. /package/dist/{skills → tools}/code-review/agents/edi-standards-reviewer/AGENT.yaml +0 -0
  106. /package/dist/{skills → tools}/code-review/agents/error-handling-reviewer/AGENT.md +0 -0
  107. /package/dist/{skills → tools}/code-review/agents/error-handling-reviewer/AGENT.yaml +0 -0
  108. /package/dist/{skills → tools}/code-review/agents/test-coverage-analyzer/AGENT.md +0 -0
  109. /package/dist/{skills → tools}/code-review/agents/test-coverage-analyzer/AGENT.yaml +0 -0
  110. /package/dist/{skills → tools}/code-review/agents/type-reviewer/AGENT.md +0 -0
  111. /package/dist/{skills → tools}/code-review/agents/type-reviewer/AGENT.yaml +0 -0
  112. /package/dist/{skills → tools}/code-review/commands/code-review.md +0 -0
  113. /package/dist/{skills → tools/code-review/skills}/code-review/SKILL.md +0 -0
  114. /package/dist/{skills → tools/code-review/skills}/code-review/SKILL.yaml +0 -0
  115. /package/dist/{skills → tools}/comments/commands/README.md +0 -0
  116. /package/dist/{skills → tools}/comments/commands/comments.md +0 -0
  117. /package/dist/{skills → tools}/project/commands/README.md +0 -0
  118. /package/dist/{skills → tools}/project/commands/project.md +0 -0
  119. /package/dist/{skills → tools/project/skills}/project/SKILL.md +0 -0
  120. /package/dist/{skills → tools/project/skills}/project/SKILL.yaml +0 -0
  121. /package/dist/{skills → tools/project/skills}/project/references/changelog.md +0 -0
  122. /package/dist/{skills → tools/project/skills}/project/references/creating.md +0 -0
  123. /package/dist/{skills → tools/project/skills}/project/references/loading.md +0 -0
  124. /package/dist/{skills → tools/project/skills}/project/references/templates.md +0 -0
  125. /package/dist/{skills → tools/project/skills}/project/references/updating.md +0 -0
  126. /package/dist/{skills → tools/project/skills}/project/references/versioning.md +0 -0
  127. /package/src/{skills → tools}/README.md +0 -0
  128. /package/src/{skills → tools}/brain/commands/README.md +0 -0
  129. /package/src/{skills → tools}/brain/commands/brain.md +0 -0
  130. /package/src/{skills → tools}/brain/commands/scratchpad.md +0 -0
  131. /package/src/{skills → tools/brain/skills}/brain/SKILL.md +0 -0
  132. /package/src/{skills → tools/brain/skills}/brain/SKILL.yaml +0 -0
  133. /package/src/{skills → tools/brain/skills}/brain/references/metadata.md +0 -0
  134. /package/src/{skills → tools/brain/skills}/brain/references/naming.md +0 -0
  135. /package/src/{skills → tools/brain/skills}/brain/references/templates.md +0 -0
  136. /package/src/{skills → tools/brain/skills}/brain/references/workflows.md +0 -0
  137. /package/src/{skills → tools/brain/skills}/brain-obsidian/SKILL.md +0 -0
  138. /package/src/{skills → tools/brain/skills}/brain-obsidian/SKILL.yaml +0 -0
  139. /package/src/{skills → tools/brain/skills}/brain-obsidian/references/templates.md +0 -0
  140. /package/src/{skills → tools/brain/skills}/brain-obsidian/references/workflows.md +0 -0
  141. /package/src/{skills → tools}/coach/commands/README.md +0 -0
  142. /package/src/{skills → tools}/coach/commands/coach.md +0 -0
  143. /package/src/{skills → tools/coach/skills}/coach/SKILL.md +0 -0
  144. /package/src/{skills → tools/coach/skills}/coach/SKILL.yaml +0 -0
  145. /package/src/{skills → tools}/code-review/agents/edi-standards-reviewer/AGENT.md +0 -0
  146. /package/src/{skills → tools}/code-review/agents/edi-standards-reviewer/AGENT.yaml +0 -0
  147. /package/src/{skills → tools}/code-review/agents/error-handling-reviewer/AGENT.md +0 -0
  148. /package/src/{skills → tools}/code-review/agents/error-handling-reviewer/AGENT.yaml +0 -0
  149. /package/src/{skills → tools}/code-review/agents/test-coverage-analyzer/AGENT.md +0 -0
  150. /package/src/{skills → tools}/code-review/agents/test-coverage-analyzer/AGENT.yaml +0 -0
  151. /package/src/{skills → tools}/code-review/agents/type-reviewer/AGENT.md +0 -0
  152. /package/src/{skills → tools}/code-review/agents/type-reviewer/AGENT.yaml +0 -0
  153. /package/src/{skills → tools}/code-review/commands/code-review.md +0 -0
  154. /package/src/{skills → tools/code-review/skills}/code-review/SKILL.md +0 -0
  155. /package/src/{skills → tools/code-review/skills}/code-review/SKILL.yaml +0 -0
  156. /package/src/{skills → tools}/comments/commands/README.md +0 -0
  157. /package/src/{skills → tools}/comments/commands/comments.md +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,16 +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);
343
+
344
+ // Commands and agents are at the tool level, not skill level
345
+ const commandsSource = join(toolDir, 'commands');
346
+ const agentsSource = join(toolDir, 'agents');
314
347
 
315
348
  // Check for collisions BEFORE installing (only if not already installed by droid)
316
349
  // Note: If skill folder exists but skill isn't in config, we allow overwriting (stale state
317
- // from AI tool switch or manual cleanup). Command/agent collisions still checked.
318
- if (!config.skills[skillName]) {
350
+ // from platform switch or manual cleanup). Command/agent collisions still checked.
351
+ if (!tools[skillName]) {
319
352
  // Check command file collisions (these could conflict with other skills)
320
- const commandsSource = join(bundledSkillDir, 'commands');
321
353
  if (existsSync(commandsSource)) {
322
354
  const commandFiles = readdirSync(commandsSource).filter(f => f.endsWith('.md') && f.toLowerCase() !== 'readme.md');
323
355
  for (const file of commandFiles) {
@@ -333,7 +365,6 @@ export function installSkill(skillName: string): { success: boolean; message: st
333
365
  }
334
366
 
335
367
  // Check bundled agent collisions
336
- const agentsSource = join(bundledSkillDir, 'agents');
337
368
  if (existsSync(agentsSource)) {
338
369
  const agentDirs = readdirSync(agentsSource, { withFileTypes: true })
339
370
  .filter(dirent => dirent.isDirectory())
@@ -355,7 +386,7 @@ export function installSkill(skillName: string): { success: boolean; message: st
355
386
  }
356
387
 
357
388
  // Copy SKILL.md (the actual skill file for Claude Code / OpenCode)
358
- const skillMdSource = join(bundledSkillDir, 'SKILL.md');
389
+ const skillMdSource = join(skillDir, 'SKILL.md');
359
390
  if (existsSync(skillMdSource)) {
360
391
  if (!existsSync(targetSkillDir)) {
361
392
  mkdirSync(targetSkillDir, { recursive: true });
@@ -366,7 +397,7 @@ export function installSkill(skillName: string): { success: boolean; message: st
366
397
  }
367
398
 
368
399
  // Copy references if present (skill documentation files)
369
- const referencesSource = join(bundledSkillDir, 'references');
400
+ const referencesSource = join(skillDir, 'references');
370
401
  if (existsSync(referencesSource)) {
371
402
  const targetReferencesDir = join(targetSkillDir, 'references');
372
403
  if (!existsSync(targetReferencesDir)) {
@@ -381,8 +412,7 @@ export function installSkill(skillName: string): { success: boolean; message: st
381
412
  }
382
413
  }
383
414
 
384
- // Copy commands if present
385
- const commandsSource = join(bundledSkillDir, 'commands');
415
+ // Copy commands if present (from tool level)
386
416
  if (existsSync(commandsSource)) {
387
417
  if (!existsSync(commandsPath)) {
388
418
  mkdirSync(commandsPath, { recursive: true });
@@ -396,16 +426,14 @@ export function installSkill(skillName: string): { success: boolean; message: st
396
426
  }
397
427
  }
398
428
 
399
- // Install bundled agents if present
429
+ // Install bundled agents if present (from tool level)
400
430
  const installedAgents: string[] = [];
401
- const agentsSource = join(bundledSkillDir, 'agents');
402
431
  if (existsSync(agentsSource)) {
403
432
  const agentDirs = readdirSync(agentsSource, { withFileTypes: true })
404
433
  .filter(dirent => dirent.isDirectory())
405
434
  .map(dirent => dirent.name);
406
435
 
407
436
  for (const agentName of agentDirs) {
408
- // Use the skill's bundled agent path instead of global agents
409
437
  const agentDir = join(agentsSource, agentName);
410
438
  const result = installAgentFromPath(agentDir, agentName);
411
439
  if (result.success) {
@@ -415,16 +443,20 @@ export function installSkill(skillName: string): { success: boolean; message: st
415
443
  }
416
444
 
417
445
  // Update config
418
- config.skills[skillName] = {
419
- version: manifest.version,
420
- installed_at: new Date().toISOString(),
421
- ...(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
+ },
422
453
  };
454
+ setPlatformTools(config, updatedTools);
423
455
  saveConfig(config);
424
456
 
425
- // Update AI tool's config file with skill reference
426
- const installedSkillNames = Object.keys(config.skills);
427
- 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);
428
460
 
429
461
  return { success: true, message: `Installed ${skillName} v${manifest.version}` };
430
462
  }
@@ -434,23 +466,24 @@ export function installSkill(skillName: string): { success: boolean; message: st
434
466
  */
435
467
  export function uninstallSkill(skillName: string): { success: boolean; message: string } {
436
468
  const config = loadConfig();
469
+ const tools = getPlatformTools(config);
437
470
 
438
471
  if (!isSkillInstalled(skillName)) {
439
472
  return { success: false, message: `Skill '${skillName}' is not installed` };
440
473
  }
441
474
 
442
- // Remove skill files from AI tool location
443
- const skillsPath = getSkillsInstallPath(config.ai_tool);
475
+ // Remove skill files from platform location
476
+ const skillsPath = getSkillsInstallPath(config.platform);
444
477
  const skillDir = join(skillsPath, skillName);
445
478
  if (existsSync(skillDir)) {
446
479
  rmSync(skillDir, { recursive: true });
447
480
  }
448
481
 
449
- // Remove command files if they exist
450
- const bundledSkillDir = join(BUNDLED_SKILLS_DIR, skillName);
451
- const commandsSource = join(bundledSkillDir, 'commands');
452
- const commandsPath = getCommandsInstallPath(config.ai_tool);
453
- 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)) {
454
487
  const commandFiles = readdirSync(commandsSource).filter(f => f.endsWith('.md') && f.toLowerCase() !== 'readme.md');
455
488
  for (const file of commandFiles) {
456
489
  const commandPath = join(commandsPath, file);
@@ -461,20 +494,22 @@ export function uninstallSkill(skillName: string): { success: boolean; message:
461
494
  }
462
495
 
463
496
  // Remove bundled agents if they were installed with this skill
464
- const installedSkillInfo = config.skills[skillName];
497
+ const installedSkillInfo = tools[skillName];
465
498
  if (installedSkillInfo?.bundled_agents) {
466
499
  for (const agentName of installedSkillInfo.bundled_agents) {
467
500
  uninstallAgent(agentName);
468
501
  }
469
502
  }
470
503
 
471
- // Remove from config
472
- 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);
473
508
  saveConfig(config);
474
509
 
475
- // Update AI tool's config file to remove skill reference
476
- const installedSkillNames = Object.keys(config.skills);
477
- 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);
478
513
 
479
514
  return { success: true, message: `Uninstalled ${skillName}` };
480
515
  }
@@ -507,7 +542,7 @@ export function isCommandInstalled(commandName: string, skillName: string): bool
507
542
 
508
543
  // Otherwise check if command was installed standalone
509
544
  const config = loadConfig();
510
- const commandsPath = getCommandsInstallPath(config.ai_tool);
545
+ const commandsPath = getCommandsInstallPath(config.platform);
511
546
 
512
547
  // Command filename is derived from the command name after the skill prefix
513
548
  // e.g., "comments check" from skill "comments" → "check.md"
@@ -528,31 +563,23 @@ export function installCommand(
528
563
  ): { success: boolean; message: string } {
529
564
  const config = loadConfig();
530
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
+
531
577
  // Find the source command file
532
578
  const cmdPart = commandName.startsWith(skillName + ' ')
533
579
  ? commandName.slice(skillName.length + 1)
534
580
  : commandName;
535
- const filename = cmdPart.replace(/\s+/g, '-') + '.md';
536
- const sourcePath = join(BUNDLED_SKILLS_DIR, skillName, 'commands', filename);
537
-
538
- if (!existsSync(sourcePath)) {
539
- // Try without hyphen conversion (original filename)
540
- const altFilename = cmdPart + '.md';
541
- const altSourcePath = join(BUNDLED_SKILLS_DIR, skillName, 'commands', altFilename);
542
- if (!existsSync(altSourcePath)) {
543
- return { success: false, message: `Command file not found: ${filename}` };
544
- }
545
- }
546
581
 
547
- // Ensure commands directory exists
548
- const commandsPath = getCommandsInstallPath(config.ai_tool);
549
- if (!existsSync(commandsPath)) {
550
- mkdirSync(commandsPath, { recursive: true });
551
- }
552
-
553
- // Find the actual source file
554
- const commandsDir = join(BUNDLED_SKILLS_DIR, skillName, 'commands');
555
- const files = readdirSync(commandsDir);
582
+ const files = readdirSync(commandsDir).filter(f => f.endsWith('.md') && f.toLowerCase() !== 'readme.md');
556
583
  const sourceFile = files.find(f => {
557
584
  const base = f.replace('.md', '');
558
585
  return base === cmdPart || base === cmdPart.replace(/\s+/g, '-');
@@ -562,6 +589,12 @@ export function installCommand(
562
589
  return { success: false, message: `Command file not found for: ${commandName}` };
563
590
  }
564
591
 
592
+ // Ensure commands directory exists
593
+ const commandsPath = getCommandsInstallPath(config.platform);
594
+ if (!existsSync(commandsPath)) {
595
+ mkdirSync(commandsPath, { recursive: true });
596
+ }
597
+
565
598
  const actualSourcePath = join(commandsDir, sourceFile);
566
599
  const targetPath = join(commandsPath, sourceFile);
567
600
 
@@ -591,7 +624,7 @@ export function uninstallCommand(
591
624
  };
592
625
  }
593
626
 
594
- const commandsPath = getCommandsInstallPath(config.ai_tool);
627
+ const commandsPath = getCommandsInstallPath(config.platform);
595
628
 
596
629
  // Find the installed command file
597
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');