@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.
- package/.claude/CLAUDE.md +41 -0
- package/.github/workflows/changeset-check.yml +43 -0
- package/.github/workflows/release.yml +6 -3
- package/CHANGELOG.md +43 -0
- package/bun.lock +357 -14
- package/dist/agents/README.md +137 -0
- package/dist/bin/droid.js +12 -1
- package/dist/bin/droid.js.map +1 -1
- package/dist/commands/setup.d.ts +8 -0
- package/dist/commands/setup.d.ts.map +1 -1
- package/dist/commands/setup.js +67 -0
- package/dist/commands/setup.js.map +1 -1
- package/dist/commands/tui.d.ts +2 -0
- package/dist/commands/tui.d.ts.map +1 -0
- package/dist/commands/tui.js +737 -0
- package/dist/commands/tui.js.map +1 -0
- package/dist/lib/agents.d.ts +53 -0
- package/dist/lib/agents.d.ts.map +1 -0
- package/dist/lib/agents.js +149 -0
- package/dist/lib/agents.js.map +1 -0
- package/dist/lib/skills.d.ts +20 -0
- package/dist/lib/skills.d.ts.map +1 -1
- package/dist/lib/skills.js +102 -0
- package/dist/lib/skills.js.map +1 -1
- package/dist/lib/types.d.ts +5 -0
- package/dist/lib/types.d.ts.map +1 -1
- package/dist/lib/version.d.ts +5 -0
- package/dist/lib/version.d.ts.map +1 -1
- package/dist/lib/version.js +19 -1
- package/dist/lib/version.js.map +1 -1
- package/dist/skills/README.md +85 -0
- package/dist/skills/comments/SKILL.md +8 -0
- package/dist/skills/comments/SKILL.yaml +32 -0
- package/dist/skills/comments/commands/README.md +58 -0
- package/package.json +15 -2
- package/src/agents/README.md +137 -0
- package/src/bin/droid.ts +12 -1
- package/src/commands/setup.ts +77 -0
- package/src/commands/tui.tsx +1535 -0
- package/src/lib/agents.ts +186 -0
- package/src/lib/skills.test.ts +75 -1
- package/src/lib/skills.ts +125 -0
- package/src/lib/types.ts +7 -0
- package/src/lib/version.test.ts +20 -1
- package/src/lib/version.ts +19 -1
- package/src/skills/README.md +85 -0
- package/src/skills/comments/SKILL.md +8 -0
- package/src/skills/comments/SKILL.yaml +32 -0
- package/src/skills/comments/commands/README.md +58 -0
- 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
|
+
}
|
package/src/lib/skills.test.ts
CHANGED
|
@@ -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 {
|
package/src/lib/version.test.ts
CHANGED
|
@@ -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
|
+
});
|
package/src/lib/version.ts
CHANGED
|
@@ -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
|
-
|
|
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`
|
|
@@ -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
|