@orderful/droid 0.2.0 → 0.4.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 (50) hide show
  1. package/.claude/CLAUDE.md +41 -0
  2. package/.github/workflows/changeset-check.yml +43 -0
  3. package/.github/workflows/release.yml +6 -3
  4. package/CHANGELOG.md +43 -0
  5. package/bun.lock +357 -14
  6. package/dist/agents/README.md +137 -0
  7. package/dist/bin/droid.js +12 -1
  8. package/dist/bin/droid.js.map +1 -1
  9. package/dist/commands/setup.d.ts +8 -0
  10. package/dist/commands/setup.d.ts.map +1 -1
  11. package/dist/commands/setup.js +67 -0
  12. package/dist/commands/setup.js.map +1 -1
  13. package/dist/commands/tui.d.ts +2 -0
  14. package/dist/commands/tui.d.ts.map +1 -0
  15. package/dist/commands/tui.js +737 -0
  16. package/dist/commands/tui.js.map +1 -0
  17. package/dist/lib/agents.d.ts +53 -0
  18. package/dist/lib/agents.d.ts.map +1 -0
  19. package/dist/lib/agents.js +149 -0
  20. package/dist/lib/agents.js.map +1 -0
  21. package/dist/lib/skills.d.ts +20 -0
  22. package/dist/lib/skills.d.ts.map +1 -1
  23. package/dist/lib/skills.js +102 -0
  24. package/dist/lib/skills.js.map +1 -1
  25. package/dist/lib/types.d.ts +5 -0
  26. package/dist/lib/types.d.ts.map +1 -1
  27. package/dist/lib/version.d.ts +5 -0
  28. package/dist/lib/version.d.ts.map +1 -1
  29. package/dist/lib/version.js +19 -1
  30. package/dist/lib/version.js.map +1 -1
  31. package/dist/skills/README.md +85 -0
  32. package/dist/skills/comments/SKILL.md +8 -0
  33. package/dist/skills/comments/SKILL.yaml +32 -0
  34. package/dist/skills/comments/commands/README.md +58 -0
  35. package/package.json +15 -2
  36. package/src/agents/README.md +137 -0
  37. package/src/bin/droid.ts +12 -1
  38. package/src/commands/setup.ts +77 -0
  39. package/src/commands/tui.tsx +1535 -0
  40. package/src/lib/agents.ts +186 -0
  41. package/src/lib/skills.test.ts +75 -1
  42. package/src/lib/skills.ts +125 -0
  43. package/src/lib/types.ts +7 -0
  44. package/src/lib/version.test.ts +20 -1
  45. package/src/lib/version.ts +19 -1
  46. package/src/skills/README.md +85 -0
  47. package/src/skills/comments/SKILL.md +8 -0
  48. package/src/skills/comments/SKILL.yaml +32 -0
  49. package/src/skills/comments/commands/README.md +58 -0
  50. package/tsconfig.json +5 -3
@@ -0,0 +1,186 @@
1
+ import { existsSync, readdirSync, readFileSync, writeFileSync, unlinkSync, mkdirSync } from 'fs';
2
+ import { join, dirname } from 'path';
3
+ import { fileURLToPath } from 'url';
4
+ import { homedir } from 'os';
5
+ import YAML from 'yaml';
6
+
7
+ const __dirname = dirname(fileURLToPath(import.meta.url));
8
+ const BUNDLED_AGENTS_DIR = join(__dirname, '../agents');
9
+ const INSTALLED_AGENTS_DIR = join(homedir(), '.claude', 'agents');
10
+
11
+ /**
12
+ * Agent manifest structure
13
+ */
14
+ export interface AgentManifest {
15
+ name: string;
16
+ description: string;
17
+ version: string;
18
+ status?: 'alpha' | 'beta' | 'stable';
19
+ mode?: 'primary' | 'subagent' | 'all'; // How the agent is used
20
+ tools?: string[]; // Allowed tools for this agent
21
+ triggers?: string[];
22
+ persona?: string;
23
+ }
24
+
25
+ /**
26
+ * Get the path to bundled agents directory
27
+ */
28
+ export function getBundledAgentsDir(): string {
29
+ return BUNDLED_AGENTS_DIR;
30
+ }
31
+
32
+ /**
33
+ * Load an agent manifest from an agent directory
34
+ */
35
+ export function loadAgentManifest(agentDir: string): AgentManifest | null {
36
+ const manifestPath = join(agentDir, 'AGENT.yaml');
37
+
38
+ if (!existsSync(manifestPath)) {
39
+ return null;
40
+ }
41
+
42
+ try {
43
+ const content = readFileSync(manifestPath, 'utf-8');
44
+ return YAML.parse(content) as AgentManifest;
45
+ } catch {
46
+ return null;
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Get all bundled agents
52
+ */
53
+ export function getBundledAgents(): AgentManifest[] {
54
+ if (!existsSync(BUNDLED_AGENTS_DIR)) {
55
+ return [];
56
+ }
57
+
58
+ const agentDirs = readdirSync(BUNDLED_AGENTS_DIR, { withFileTypes: true })
59
+ .filter((dirent) => dirent.isDirectory())
60
+ .map((dirent) => dirent.name);
61
+
62
+ const agents: AgentManifest[] = [];
63
+
64
+ for (const agentDir of agentDirs) {
65
+ const manifest = loadAgentManifest(join(BUNDLED_AGENTS_DIR, agentDir));
66
+ if (manifest) {
67
+ agents.push(manifest);
68
+ }
69
+ }
70
+
71
+ return agents;
72
+ }
73
+
74
+ /**
75
+ * Get agent status display string
76
+ */
77
+ export function getAgentStatusDisplay(status?: string): string {
78
+ switch (status) {
79
+ case 'alpha':
80
+ return '[alpha]';
81
+ case 'beta':
82
+ return '[beta]';
83
+ case 'stable':
84
+ default:
85
+ return '';
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Get installed agents directory
91
+ */
92
+ export function getInstalledAgentsDir(): string {
93
+ return INSTALLED_AGENTS_DIR;
94
+ }
95
+
96
+ /**
97
+ * Check if an agent is installed
98
+ */
99
+ export function isAgentInstalled(agentName: string): boolean {
100
+ const agentPath = join(INSTALLED_AGENTS_DIR, `${agentName}.md`);
101
+ return existsSync(agentPath);
102
+ }
103
+
104
+ /**
105
+ * Install an agent to ~/.claude/agents/
106
+ * Combines AGENT.yaml metadata with AGENT.md content into a single .md file
107
+ */
108
+ export function installAgent(agentName: string): { success: boolean; message: string } {
109
+ const agentDir = join(BUNDLED_AGENTS_DIR, agentName);
110
+ const manifestPath = join(agentDir, 'AGENT.yaml');
111
+ const contentPath = join(agentDir, 'AGENT.md');
112
+
113
+ if (!existsSync(manifestPath)) {
114
+ return { success: false, message: `Agent manifest not found: ${agentName}` };
115
+ }
116
+
117
+ try {
118
+ // Load manifest
119
+ const manifest = YAML.parse(readFileSync(manifestPath, 'utf-8')) as AgentManifest;
120
+
121
+ // Load content (AGENT.md)
122
+ let agentContent = '';
123
+ if (existsSync(contentPath)) {
124
+ const rawContent = readFileSync(contentPath, 'utf-8');
125
+ // Strip frontmatter from AGENT.md if present (we'll use AGENT.yaml for metadata)
126
+ const frontmatterMatch = rawContent.match(/^---\n[\s\S]*?\n---\n?/);
127
+ agentContent = frontmatterMatch ? rawContent.slice(frontmatterMatch[0].length) : rawContent;
128
+ }
129
+
130
+ // If no AGENT.md content, use persona from AGENT.yaml
131
+ if (!agentContent.trim() && manifest.persona) {
132
+ agentContent = manifest.persona;
133
+ }
134
+
135
+ // Build frontmatter for Claude Code agent format
136
+ const frontmatter: Record<string, unknown> = {
137
+ name: manifest.name,
138
+ description: manifest.description,
139
+ };
140
+
141
+ // Add tools if specified
142
+ if (manifest.tools && manifest.tools.length > 0) {
143
+ frontmatter.tools = manifest.tools.join(', ');
144
+ }
145
+
146
+ // Generate the installed agent file
147
+ const installedContent = `---
148
+ name: ${frontmatter.name}
149
+ description: ${frontmatter.description}${frontmatter.tools ? `\ntools: ${frontmatter.tools}` : ''}
150
+ ---
151
+
152
+ ${agentContent.trim()}
153
+ `;
154
+
155
+ // Ensure agents directory exists
156
+ if (!existsSync(INSTALLED_AGENTS_DIR)) {
157
+ mkdirSync(INSTALLED_AGENTS_DIR, { recursive: true });
158
+ }
159
+
160
+ // Write the agent file
161
+ const outputPath = join(INSTALLED_AGENTS_DIR, `${agentName}.md`);
162
+ writeFileSync(outputPath, installedContent);
163
+
164
+ return { success: true, message: `Installed ${agentName} to ~/.claude/agents/` };
165
+ } catch (error) {
166
+ return { success: false, message: `Failed to install agent: ${error}` };
167
+ }
168
+ }
169
+
170
+ /**
171
+ * Uninstall an agent from ~/.claude/agents/
172
+ */
173
+ export function uninstallAgent(agentName: string): { success: boolean; message: string } {
174
+ const agentPath = join(INSTALLED_AGENTS_DIR, `${agentName}.md`);
175
+
176
+ if (!existsSync(agentPath)) {
177
+ return { success: false, message: `Agent not installed: ${agentName}` };
178
+ }
179
+
180
+ try {
181
+ unlinkSync(agentPath);
182
+ return { success: true, message: `Uninstalled ${agentName}` };
183
+ } catch (error) {
184
+ return { success: false, message: `Failed to uninstall agent: ${error}` };
185
+ }
186
+ }
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect, beforeEach, afterEach } from 'bun:test';
2
- import { existsSync, mkdirSync, rmSync, writeFileSync, readFileSync } from 'fs';
2
+ import { existsSync, mkdirSync, rmSync, writeFileSync, readFileSync, readdirSync } from 'fs';
3
3
  import { join } from 'path';
4
4
  import { tmpdir } from 'os';
5
5
  import { homedir } from 'os';
@@ -13,6 +13,19 @@ import {
13
13
  getBundledSkillsDir,
14
14
  } from './skills.js';
15
15
 
16
+ /**
17
+ * Parse YAML frontmatter from markdown content
18
+ */
19
+ function parseFrontmatter(content: string): Record<string, unknown> | null {
20
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
21
+ if (!match) return null;
22
+ try {
23
+ return YAML.parse(match[1]);
24
+ } catch {
25
+ return null;
26
+ }
27
+ }
28
+
16
29
  describe('getSkillsInstallPath', () => {
17
30
  it('should return Claude Code path', () => {
18
31
  const path = getSkillsInstallPath(AITool.ClaudeCode);
@@ -136,3 +149,64 @@ describe('skill manifest parsing', () => {
136
149
  expect(parsed.provides_output).toBeUndefined();
137
150
  });
138
151
  });
152
+
153
+ describe('bundled skills validation', () => {
154
+ it('all skills should have SKILL.md with valid frontmatter', () => {
155
+ const skillsDir = getBundledSkillsDir();
156
+ const skillDirs = readdirSync(skillsDir, { withFileTypes: true })
157
+ .filter((d) => d.isDirectory())
158
+ .map((d) => d.name);
159
+
160
+ for (const skillName of skillDirs) {
161
+ const skillMdPath = join(skillsDir, skillName, 'SKILL.md');
162
+ expect(existsSync(skillMdPath)).toBe(true);
163
+
164
+ const content = readFileSync(skillMdPath, 'utf-8');
165
+ const frontmatter = parseFrontmatter(content);
166
+
167
+ expect(frontmatter).not.toBeNull();
168
+ expect(frontmatter?.name).toBe(skillName);
169
+ expect(typeof frontmatter?.description).toBe('string');
170
+ }
171
+ });
172
+
173
+ it('all skills should have SKILL.yaml manifest', () => {
174
+ const skillsDir = getBundledSkillsDir();
175
+ const skillDirs = readdirSync(skillsDir, { withFileTypes: true })
176
+ .filter((d) => d.isDirectory())
177
+ .map((d) => d.name);
178
+
179
+ for (const skillName of skillDirs) {
180
+ const yamlPath = join(skillsDir, skillName, 'SKILL.yaml');
181
+ expect(existsSync(yamlPath)).toBe(true);
182
+
183
+ const content = readFileSync(yamlPath, 'utf-8');
184
+ const manifest = YAML.parse(content);
185
+
186
+ expect(manifest.name).toBe(skillName);
187
+ expect(typeof manifest.description).toBe('string');
188
+ expect(typeof manifest.version).toBe('string');
189
+ }
190
+ });
191
+
192
+ it('SKILL.md frontmatter should match SKILL.yaml', () => {
193
+ const skillsDir = getBundledSkillsDir();
194
+ const skillDirs = readdirSync(skillsDir, { withFileTypes: true })
195
+ .filter((d) => d.isDirectory())
196
+ .map((d) => d.name);
197
+
198
+ for (const skillName of skillDirs) {
199
+ const mdPath = join(skillsDir, skillName, 'SKILL.md');
200
+ const yamlPath = join(skillsDir, skillName, 'SKILL.yaml');
201
+
202
+ const mdContent = readFileSync(mdPath, 'utf-8');
203
+ const yamlContent = readFileSync(yamlPath, 'utf-8');
204
+
205
+ const frontmatter = parseFrontmatter(mdContent);
206
+ const manifest = YAML.parse(yamlContent);
207
+
208
+ expect(frontmatter?.name).toBe(manifest.name);
209
+ expect(frontmatter?.description).toBe(manifest.description);
210
+ }
211
+ });
212
+ });
package/src/lib/skills.ts CHANGED
@@ -283,3 +283,128 @@ export function getSkillStatusDisplay(status?: SkillStatus): string {
283
283
  return '';
284
284
  }
285
285
  }
286
+
287
+ /**
288
+ * Check if a command is installed
289
+ * @param commandName - The display name (e.g., "comments check")
290
+ * @param skillName - The skill the command belongs to
291
+ */
292
+ export function isCommandInstalled(commandName: string, skillName: string): boolean {
293
+ // If the parent skill is installed, the command is installed with it
294
+ if (isSkillInstalled(skillName)) {
295
+ return true;
296
+ }
297
+
298
+ // Otherwise check if command was installed standalone
299
+ const config = loadConfig();
300
+ const commandsPath = getCommandsInstallPath(config.ai_tool);
301
+
302
+ // Command filename is derived from the command name after the skill prefix
303
+ // e.g., "comments check" from skill "comments" → "check.md"
304
+ const cmdPart = commandName.startsWith(skillName + ' ')
305
+ ? commandName.slice(skillName.length + 1)
306
+ : commandName;
307
+ const filename = cmdPart.replace(/\s+/g, '-') + '.md';
308
+
309
+ return existsSync(join(commandsPath, filename));
310
+ }
311
+
312
+ /**
313
+ * Install a single command (without installing the full skill)
314
+ */
315
+ export function installCommand(
316
+ commandName: string,
317
+ skillName: string
318
+ ): { success: boolean; message: string } {
319
+ const config = loadConfig();
320
+
321
+ // Find the source command file
322
+ const cmdPart = commandName.startsWith(skillName + ' ')
323
+ ? commandName.slice(skillName.length + 1)
324
+ : commandName;
325
+ const filename = cmdPart.replace(/\s+/g, '-') + '.md';
326
+ const sourcePath = join(BUNDLED_SKILLS_DIR, skillName, 'commands', filename);
327
+
328
+ if (!existsSync(sourcePath)) {
329
+ // Try without hyphen conversion (original filename)
330
+ const altFilename = cmdPart + '.md';
331
+ const altSourcePath = join(BUNDLED_SKILLS_DIR, skillName, 'commands', altFilename);
332
+ if (!existsSync(altSourcePath)) {
333
+ return { success: false, message: `Command file not found: ${filename}` };
334
+ }
335
+ }
336
+
337
+ // Ensure commands directory exists
338
+ const commandsPath = getCommandsInstallPath(config.ai_tool);
339
+ if (!existsSync(commandsPath)) {
340
+ mkdirSync(commandsPath, { recursive: true });
341
+ }
342
+
343
+ // Find the actual source file
344
+ const commandsDir = join(BUNDLED_SKILLS_DIR, skillName, 'commands');
345
+ const files = readdirSync(commandsDir);
346
+ const sourceFile = files.find(f => {
347
+ const base = f.replace('.md', '');
348
+ return base === cmdPart || base === cmdPart.replace(/\s+/g, '-');
349
+ });
350
+
351
+ if (!sourceFile) {
352
+ return { success: false, message: `Command file not found for: ${commandName}` };
353
+ }
354
+
355
+ const actualSourcePath = join(commandsDir, sourceFile);
356
+ const targetPath = join(commandsPath, sourceFile);
357
+
358
+ try {
359
+ const content = readFileSync(actualSourcePath, 'utf-8');
360
+ writeFileSync(targetPath, content);
361
+ return { success: true, message: `Installed /${commandName}` };
362
+ } catch (error) {
363
+ return { success: false, message: `Failed to install command: ${error}` };
364
+ }
365
+ }
366
+
367
+ /**
368
+ * Uninstall a single command
369
+ */
370
+ export function uninstallCommand(
371
+ commandName: string,
372
+ skillName: string
373
+ ): { success: boolean; message: string } {
374
+ const config = loadConfig();
375
+
376
+ // If the parent skill is installed, block command uninstall
377
+ if (isSkillInstalled(skillName)) {
378
+ return {
379
+ success: false,
380
+ message: `Command belongs to installed skill '${skillName}'. Uninstall the skill to remove this command.`,
381
+ };
382
+ }
383
+
384
+ const commandsPath = getCommandsInstallPath(config.ai_tool);
385
+
386
+ // Find the installed command file
387
+ const cmdPart = commandName.startsWith(skillName + ' ')
388
+ ? commandName.slice(skillName.length + 1)
389
+ : commandName;
390
+
391
+ // Check possible filenames
392
+ const possibleFilenames = [
393
+ cmdPart + '.md',
394
+ cmdPart.replace(/\s+/g, '-') + '.md',
395
+ ];
396
+
397
+ for (const filename of possibleFilenames) {
398
+ const commandPath = join(commandsPath, filename);
399
+ if (existsSync(commandPath)) {
400
+ try {
401
+ rmSync(commandPath);
402
+ return { success: true, message: `Uninstalled /${commandName}` };
403
+ } catch (error) {
404
+ return { success: false, message: `Failed to uninstall command: ${error}` };
405
+ }
406
+ }
407
+ }
408
+
409
+ return { success: false, message: `Command not installed: ${commandName}` };
410
+ }
package/src/lib/types.ts CHANGED
@@ -45,6 +45,11 @@ export interface InstalledSkill {
45
45
  installed_at: string;
46
46
  }
47
47
 
48
+ export interface SkillExample {
49
+ title: string;
50
+ code: string;
51
+ }
52
+
48
53
  export interface SkillManifest {
49
54
  name: string;
50
55
  description: string;
@@ -54,6 +59,8 @@ export interface SkillManifest {
54
59
  config_schema?: Record<string, ConfigOption>;
55
60
  // Skills can declare they provide an output target
56
61
  provides_output?: boolean;
62
+ // Usage examples to show in the TUI
63
+ examples?: SkillExample[];
57
64
  }
58
65
 
59
66
  export interface ConfigOption {
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect } from 'bun:test';
2
- import { getVersion } from './version.js';
2
+ import { getVersion, compareSemver } from './version.js';
3
3
 
4
4
  describe('getVersion', () => {
5
5
  it('should return a version string', () => {
@@ -21,3 +21,22 @@ describe('getVersion', () => {
21
21
  expect(version).not.toBe('0.0.0');
22
22
  });
23
23
  });
24
+
25
+ describe('compareSemver', () => {
26
+ it('should return 1 when first version is greater', () => {
27
+ expect(compareSemver('1.0.0', '0.9.9')).toBe(1);
28
+ expect(compareSemver('0.2.1', '0.2.0')).toBe(1);
29
+ expect(compareSemver('2.0.0', '1.9.9')).toBe(1);
30
+ });
31
+
32
+ it('should return -1 when first version is lesser', () => {
33
+ expect(compareSemver('0.9.9', '1.0.0')).toBe(-1);
34
+ expect(compareSemver('0.2.0', '0.2.1')).toBe(-1);
35
+ expect(compareSemver('1.9.9', '2.0.0')).toBe(-1);
36
+ });
37
+
38
+ it('should return 0 when versions are equal', () => {
39
+ expect(compareSemver('1.0.0', '1.0.0')).toBe(0);
40
+ expect(compareSemver('0.2.1', '0.2.1')).toBe(0);
41
+ });
42
+ });
@@ -19,6 +19,23 @@ export function getVersion(): string {
19
19
  }
20
20
  }
21
21
 
22
+ /**
23
+ * Compare two semver versions
24
+ * Returns: 1 if a > b, -1 if a < b, 0 if equal
25
+ */
26
+ export function compareSemver(a: string, b: string): number {
27
+ const partsA = a.split('.').map(Number);
28
+ const partsB = b.split('.').map(Number);
29
+
30
+ for (let i = 0; i < 3; i++) {
31
+ const numA = partsA[i] || 0;
32
+ const numB = partsB[i] || 0;
33
+ if (numA > numB) return 1;
34
+ if (numA < numB) return -1;
35
+ }
36
+ return 0;
37
+ }
38
+
22
39
  /**
23
40
  * Check for updates (non-blocking)
24
41
  * Shows a message if a new version is available
@@ -33,7 +50,8 @@ export async function checkForUpdates(): Promise<void> {
33
50
  timeout: 3000,
34
51
  }).trim();
35
52
 
36
- if (latestVersion && latestVersion !== currentVersion) {
53
+ // Only show update message if latest is actually newer
54
+ if (latestVersion && compareSemver(latestVersion, currentVersion) > 0) {
37
55
  console.log(
38
56
  chalk.yellow(
39
57
  `\n⚠️ A new version of droid is available (${currentVersion} → ${latestVersion})`
@@ -0,0 +1,85 @@
1
+ # Contributing Skills
2
+
3
+ Skills are reusable AI capabilities that can be installed into Claude Code or OpenCode.
4
+
5
+ ## Directory Structure
6
+
7
+ Each skill is a directory containing:
8
+
9
+ ```
10
+ skills/
11
+ └── my-skill/
12
+ ├── SKILL.yaml # Required: Manifest with metadata
13
+ ├── SKILL.md # Required: Instructions for the AI
14
+ └── commands/ # Optional: Slash commands
15
+ └── my-command.md
16
+ ```
17
+
18
+ ## SKILL.yaml (Manifest)
19
+
20
+ ```yaml
21
+ name: my-skill # Must match directory name
22
+ description: Short description # Shown in TUI and listings
23
+ version: 1.0.0 # Semver
24
+ status: beta # alpha | beta | stable (optional)
25
+ dependencies: [] # Other skills required (optional)
26
+ provides_output: false # Can this skill be an output target?
27
+
28
+ # Configuration schema (optional)
29
+ config_schema:
30
+ option_name:
31
+ type: string # string | boolean
32
+ description: What this option does
33
+ default: "default value" # Optional default
34
+
35
+ # Examples shown in TUI (optional)
36
+ examples:
37
+ - title: "Example name"
38
+ code: |
39
+ // Example code block
40
+ ```
41
+
42
+ ## SKILL.md (AI Instructions)
43
+
44
+ The SKILL.md file contains instructions for the AI. It must have YAML frontmatter:
45
+
46
+ ```markdown
47
+ ---
48
+ name: my-skill
49
+ description: Short description (must match SKILL.yaml)
50
+ globs:
51
+ - "**/*" # File patterns this skill applies to
52
+ alwaysApply: false # Always include in context?
53
+ ---
54
+
55
+ # My Skill
56
+
57
+ Instructions for the AI on how to use this skill...
58
+ ```
59
+
60
+ ## Commands (Optional)
61
+
62
+ Commands are slash commands the user can invoke. Each is a markdown file:
63
+
64
+ ```
65
+ commands/
66
+ └── do-thing.md
67
+ ```
68
+
69
+ The command file is just markdown instructions. The filename becomes the command name (`/do-thing`).
70
+
71
+ ## Testing Your Skill
72
+
73
+ 1. Run `npm run build` to compile
74
+ 2. Run `droid` to open the TUI
75
+ 3. Navigate to Skills tab
76
+ 4. Install your skill
77
+ 5. Test in Claude Code with `/your-command` or by referencing the skill
78
+
79
+ ## Checklist
80
+
81
+ - [ ] `SKILL.yaml` has all required fields (name, description, version)
82
+ - [ ] `SKILL.md` has valid YAML frontmatter
83
+ - [ ] Name in frontmatter matches directory name
84
+ - [ ] Description matches between SKILL.yaml and SKILL.md
85
+ - [ ] Tests pass: `bun test src/lib/skills.test.ts`
@@ -1,3 +1,11 @@
1
+ ---
2
+ name: comments
3
+ description: Inline conversation in any file via @droid/@user markers
4
+ globs:
5
+ - "**/*"
6
+ alwaysApply: false
7
+ ---
8
+
1
9
  # Comments Skill
2
10
 
3
11
  Enable inline conversation in any file using `> @droid` and `> @{user}` markers.
@@ -16,3 +16,35 @@ config_schema:
16
16
  type: boolean
17
17
  description: Keep original comments after addressing (vs removing them)
18
18
  default: false
19
+ examples:
20
+ - title: "Action request"
21
+ code: |
22
+ // @droid use PascalCase for enum keys
23
+ enum Status {
24
+ PENDING = 'pending'
25
+ }
26
+ - title: "Ask a question"
27
+ code: |
28
+ > @droid Should we cache this?
29
+
30
+ > @fry Yes, add Redis caching here...
31
+ - title: "TODO with context"
32
+ code: |
33
+ // @droid TODO: refactor to async/await
34
+ // The callback pattern is unwieldy
35
+ function fetchData(callback) {
36
+ api.get('/data', callback);
37
+ }
38
+ - title: "Code review note"
39
+ code: |
40
+ // @droid potential memory leak here
41
+ // Clear interval on unmount?
42
+ useEffect(() => {
43
+ setInterval(poll, 1000);
44
+ }, []);
45
+ - title: "Multi-line discussion"
46
+ code: |
47
+ > @droid Best approach for pagination?
48
+ > We have ~10k records.
49
+
50
+ > @fry Use cursor-based, limit 50.
@@ -0,0 +1,58 @@
1
+ # Contributing Commands
2
+
3
+ Commands are slash commands that users invoke in Claude Code or OpenCode. They're bundled with skills.
4
+
5
+ ## Location
6
+
7
+ Commands live in a `commands/` subdirectory within a skill:
8
+
9
+ ```
10
+ skills/
11
+ └── my-skill/
12
+ ├── SKILL.yaml
13
+ ├── SKILL.md
14
+ └── commands/
15
+ ├── README.md # This file
16
+ ├── check.md # /check command
17
+ └── cleanup.md # /cleanup command
18
+ ```
19
+
20
+ ## Command File Format
21
+
22
+ Each command is a simple markdown file. The filename (without `.md`) becomes the command name.
23
+
24
+ **Example: `commands/check.md`**
25
+
26
+ ```markdown
27
+ Scan the codebase for @droid comments and address each one.
28
+
29
+ ## Behavior
30
+
31
+ 1. Search for `> @droid` markers in files
32
+ 2. For each comment found:
33
+ - If it's an action request → execute and remove
34
+ - If it's a question → respond with `> @{user_mention}`
35
+
36
+ ## Arguments
37
+
38
+ - `{path}` - Optional path to scope the search (default: current directory)
39
+
40
+ ## Examples
41
+
42
+ - `/comments check` - Check all files
43
+ - `/comments check src/` - Check only src directory
44
+ ```
45
+
46
+ ## Naming Convention
47
+
48
+ Commands are namespaced by skill. If your skill is `comments`, your commands become:
49
+
50
+ - `commands/check.md` → `/comments check`
51
+ - `commands/cleanup.md` → `/comments cleanup`
52
+
53
+ ## Best Practices
54
+
55
+ 1. **Be specific** - Describe exactly what the command does
56
+ 2. **Include examples** - Show common usage patterns
57
+ 3. **Document arguments** - List any parameters the command accepts
58
+ 4. **Explain behavior** - Break down the steps the AI should follow