@orderful/droid 0.2.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 (86) hide show
  1. package/.changeset/README.md +30 -0
  2. package/.changeset/config.json +14 -0
  3. package/.eslintrc.json +20 -0
  4. package/.github/PULL_REQUEST_TEMPLATE.md +9 -0
  5. package/.github/workflows/ci.yml +47 -0
  6. package/.github/workflows/release.yml +45 -0
  7. package/CHANGELOG.md +11 -0
  8. package/README.md +153 -0
  9. package/bun.lock +571 -0
  10. package/dist/bin/droid.d.ts +3 -0
  11. package/dist/bin/droid.d.ts.map +1 -0
  12. package/dist/bin/droid.js +48 -0
  13. package/dist/bin/droid.js.map +1 -0
  14. package/dist/commands/config.d.ts +8 -0
  15. package/dist/commands/config.d.ts.map +1 -0
  16. package/dist/commands/config.js +67 -0
  17. package/dist/commands/config.js.map +1 -0
  18. package/dist/commands/install.d.ts +2 -0
  19. package/dist/commands/install.d.ts.map +1 -0
  20. package/dist/commands/install.js +42 -0
  21. package/dist/commands/install.js.map +1 -0
  22. package/dist/commands/setup.d.ts +2 -0
  23. package/dist/commands/setup.d.ts.map +1 -0
  24. package/dist/commands/setup.js +132 -0
  25. package/dist/commands/setup.js.map +1 -0
  26. package/dist/commands/skills.d.ts +2 -0
  27. package/dist/commands/skills.d.ts.map +1 -0
  28. package/dist/commands/skills.js +135 -0
  29. package/dist/commands/skills.js.map +1 -0
  30. package/dist/commands/uninstall.d.ts +2 -0
  31. package/dist/commands/uninstall.d.ts.map +1 -0
  32. package/dist/commands/uninstall.js +17 -0
  33. package/dist/commands/uninstall.js.map +1 -0
  34. package/dist/commands/update.d.ts +7 -0
  35. package/dist/commands/update.d.ts.map +1 -0
  36. package/dist/commands/update.js +45 -0
  37. package/dist/commands/update.js.map +1 -0
  38. package/dist/index.d.ts +5 -0
  39. package/dist/index.d.ts.map +1 -0
  40. package/dist/index.js +6 -0
  41. package/dist/index.js.map +1 -0
  42. package/dist/lib/config.d.ts +46 -0
  43. package/dist/lib/config.d.ts.map +1 -0
  44. package/dist/lib/config.js +133 -0
  45. package/dist/lib/config.js.map +1 -0
  46. package/dist/lib/skill-config.d.ts +6 -0
  47. package/dist/lib/skill-config.d.ts.map +1 -0
  48. package/dist/lib/skill-config.js +80 -0
  49. package/dist/lib/skill-config.js.map +1 -0
  50. package/dist/lib/skills.d.ts +56 -0
  51. package/dist/lib/skills.d.ts.map +1 -0
  52. package/dist/lib/skills.js +245 -0
  53. package/dist/lib/skills.js.map +1 -0
  54. package/dist/lib/types.d.ts +54 -0
  55. package/dist/lib/types.d.ts.map +1 -0
  56. package/dist/lib/types.js +31 -0
  57. package/dist/lib/types.js.map +1 -0
  58. package/dist/lib/version.d.ts +10 -0
  59. package/dist/lib/version.d.ts.map +1 -0
  60. package/dist/lib/version.js +41 -0
  61. package/dist/lib/version.js.map +1 -0
  62. package/dist/skills/comments/SKILL.md +65 -0
  63. package/dist/skills/comments/SKILL.yaml +18 -0
  64. package/dist/skills/comments/commands/comments.md +48 -0
  65. package/package.json +58 -0
  66. package/src/bin/droid.ts +58 -0
  67. package/src/commands/config.ts +86 -0
  68. package/src/commands/install.ts +48 -0
  69. package/src/commands/setup.ts +149 -0
  70. package/src/commands/skills.ts +159 -0
  71. package/src/commands/uninstall.ts +18 -0
  72. package/src/commands/update.ts +58 -0
  73. package/src/index.ts +5 -0
  74. package/src/lib/config.test.ts +99 -0
  75. package/src/lib/config.ts +154 -0
  76. package/src/lib/skill-config.ts +93 -0
  77. package/src/lib/skills.test.ts +138 -0
  78. package/src/lib/skills.ts +285 -0
  79. package/src/lib/types.test.ts +65 -0
  80. package/src/lib/types.ts +68 -0
  81. package/src/lib/version.test.ts +23 -0
  82. package/src/lib/version.ts +47 -0
  83. package/src/skills/comments/SKILL.md +65 -0
  84. package/src/skills/comments/SKILL.yaml +18 -0
  85. package/src/skills/comments/commands/comments.md +48 -0
  86. package/tsconfig.json +19 -0
@@ -0,0 +1,138 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'bun:test';
2
+ import { existsSync, mkdirSync, rmSync, writeFileSync, readFileSync } from 'fs';
3
+ import { join } from 'path';
4
+ import { tmpdir } from 'os';
5
+ import { homedir } from 'os';
6
+ import YAML from 'yaml';
7
+ import { AITool, SkillStatus } from './types.js';
8
+ import {
9
+ getSkillsInstallPath,
10
+ getCommandsInstallPath,
11
+ getAIConfigPath,
12
+ getSkillStatusDisplay,
13
+ getBundledSkillsDir,
14
+ } from './skills.js';
15
+
16
+ describe('getSkillsInstallPath', () => {
17
+ it('should return Claude Code path', () => {
18
+ const path = getSkillsInstallPath(AITool.ClaudeCode);
19
+ expect(path).toBe(join(homedir(), '.claude', 'skills'));
20
+ });
21
+
22
+ it('should return OpenCode path', () => {
23
+ const path = getSkillsInstallPath(AITool.OpenCode);
24
+ expect(path).toBe(join(homedir(), '.config', 'opencode', 'skills'));
25
+ });
26
+ });
27
+
28
+ describe('getCommandsInstallPath', () => {
29
+ it('should return Claude Code commands path', () => {
30
+ const path = getCommandsInstallPath(AITool.ClaudeCode);
31
+ expect(path).toBe(join(homedir(), '.claude', 'commands'));
32
+ });
33
+
34
+ it('should return OpenCode commands path', () => {
35
+ const path = getCommandsInstallPath(AITool.OpenCode);
36
+ expect(path).toBe(join(homedir(), '.config', 'opencode', 'command'));
37
+ });
38
+ });
39
+
40
+ describe('getAIConfigPath', () => {
41
+ it('should return Claude Code CLAUDE.md path', () => {
42
+ const path = getAIConfigPath(AITool.ClaudeCode);
43
+ expect(path).toBe(join(homedir(), '.claude', 'CLAUDE.md'));
44
+ });
45
+
46
+ it('should return OpenCode AGENTS.md path', () => {
47
+ const path = getAIConfigPath(AITool.OpenCode);
48
+ expect(path).toBe(join(homedir(), '.config', 'opencode', 'AGENTS.md'));
49
+ });
50
+ });
51
+
52
+ describe('getBundledSkillsDir', () => {
53
+ it('should return a path ending in skills', () => {
54
+ const dir = getBundledSkillsDir();
55
+ expect(dir.endsWith('skills')).toBe(true);
56
+ });
57
+
58
+ it('should return an existing directory', () => {
59
+ const dir = getBundledSkillsDir();
60
+ expect(existsSync(dir)).toBe(true);
61
+ });
62
+ });
63
+
64
+ describe('getSkillStatusDisplay', () => {
65
+ it('should return [alpha] for alpha status', () => {
66
+ expect(getSkillStatusDisplay(SkillStatus.Alpha)).toBe('[alpha]');
67
+ });
68
+
69
+ it('should return [beta] for beta status', () => {
70
+ expect(getSkillStatusDisplay(SkillStatus.Beta)).toBe('[beta]');
71
+ });
72
+
73
+ it('should return empty string for stable status', () => {
74
+ expect(getSkillStatusDisplay(SkillStatus.Stable)).toBe('');
75
+ });
76
+
77
+ it('should return empty string for undefined status', () => {
78
+ expect(getSkillStatusDisplay(undefined)).toBe('');
79
+ });
80
+ });
81
+
82
+ describe('skill manifest parsing', () => {
83
+ let testDir: string;
84
+
85
+ beforeEach(() => {
86
+ testDir = join(tmpdir(), `droid-skill-test-${Date.now()}`);
87
+ mkdirSync(testDir, { recursive: true });
88
+ });
89
+
90
+ afterEach(() => {
91
+ if (existsSync(testDir)) {
92
+ rmSync(testDir, { recursive: true });
93
+ }
94
+ });
95
+
96
+ it('should parse valid skill manifest', () => {
97
+ const manifest = {
98
+ name: 'test-skill',
99
+ description: 'A test skill',
100
+ version: '1.0.0',
101
+ status: 'beta',
102
+ dependencies: ['comments'],
103
+ provides_output: true,
104
+ };
105
+
106
+ const manifestPath = join(testDir, 'SKILL.yaml');
107
+ writeFileSync(manifestPath, YAML.stringify(manifest), 'utf-8');
108
+
109
+ const content = require('fs').readFileSync(manifestPath, 'utf-8');
110
+ const parsed = YAML.parse(content);
111
+
112
+ expect(parsed.name).toBe('test-skill');
113
+ expect(parsed.description).toBe('A test skill');
114
+ expect(parsed.version).toBe('1.0.0');
115
+ expect(parsed.status).toBe('beta');
116
+ expect(parsed.dependencies).toEqual(['comments']);
117
+ expect(parsed.provides_output).toBe(true);
118
+ });
119
+
120
+ it('should handle manifest without optional fields', () => {
121
+ const manifest = {
122
+ name: 'minimal-skill',
123
+ description: 'Minimal',
124
+ version: '0.1.0',
125
+ };
126
+
127
+ const manifestPath = join(testDir, 'SKILL.yaml');
128
+ writeFileSync(manifestPath, YAML.stringify(manifest), 'utf-8');
129
+
130
+ const content = require('fs').readFileSync(manifestPath, 'utf-8');
131
+ const parsed = YAML.parse(content);
132
+
133
+ expect(parsed.name).toBe('minimal-skill');
134
+ expect(parsed.status).toBeUndefined();
135
+ expect(parsed.dependencies).toBeUndefined();
136
+ expect(parsed.provides_output).toBeUndefined();
137
+ });
138
+ });
@@ -0,0 +1,285 @@
1
+ import { existsSync, readdirSync, readFileSync, mkdirSync, writeFileSync, cpSync, rmSync } from 'fs';
2
+ import { join, dirname } from 'path';
3
+ import { homedir } from 'os';
4
+ import { fileURLToPath } from 'url';
5
+ import YAML from 'yaml';
6
+ import { loadConfig, saveConfig } from './config.js';
7
+ import { AITool, SkillStatus, type SkillManifest, type InstalledSkill } from './types.js';
8
+
9
+ // Marker comments for CLAUDE.md skill registration
10
+ const DROID_SKILLS_START = '<!-- droid-skills-start -->';
11
+ const DROID_SKILLS_END = '<!-- droid-skills-end -->';
12
+
13
+ const __dirname = dirname(fileURLToPath(import.meta.url));
14
+ const BUNDLED_SKILLS_DIR = join(__dirname, '../skills');
15
+
16
+ /**
17
+ * Get the path to bundled skills directory
18
+ */
19
+ export function getBundledSkillsDir(): string {
20
+ return BUNDLED_SKILLS_DIR;
21
+ }
22
+
23
+ /**
24
+ * Get the installation path for skills based on AI tool
25
+ */
26
+ export function getSkillsInstallPath(aiTool: AITool): string {
27
+ switch (aiTool) {
28
+ case AITool.ClaudeCode:
29
+ return join(homedir(), '.claude', 'skills');
30
+ case AITool.OpenCode:
31
+ return join(homedir(), '.config', 'opencode', 'skills');
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Get the commands installation path based on AI tool
37
+ */
38
+ export function getCommandsInstallPath(aiTool: AITool): string {
39
+ switch (aiTool) {
40
+ case AITool.ClaudeCode:
41
+ return join(homedir(), '.claude', 'commands');
42
+ case AITool.OpenCode:
43
+ return join(homedir(), '.config', 'opencode', 'command');
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Get the path to the AI tool's main config markdown file
49
+ */
50
+ export function getAIConfigPath(aiTool: AITool): string {
51
+ switch (aiTool) {
52
+ case AITool.ClaudeCode:
53
+ return join(homedir(), '.claude', 'CLAUDE.md');
54
+ case AITool.OpenCode:
55
+ return join(homedir(), '.config', 'opencode', 'AGENTS.md');
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Update the AI tool's config file with skill registrations
61
+ */
62
+ export function updateAIConfigSkills(aiTool: AITool, installedSkills: string[]): void {
63
+ const configPath = getAIConfigPath(aiTool);
64
+
65
+ let content = '';
66
+ if (existsSync(configPath)) {
67
+ content = readFileSync(configPath, 'utf-8');
68
+ }
69
+
70
+ // Generate skills section
71
+ const skillLines = installedSkills.map(name => {
72
+ const relativePath = `skills/${name}/SKILL.md`;
73
+ return `- [${name}](${relativePath})`;
74
+ });
75
+
76
+ const skillsSection = installedSkills.length > 0
77
+ ? `${DROID_SKILLS_START}\n## Droid Skills\n\n${skillLines.join('\n')}\n${DROID_SKILLS_END}`
78
+ : '';
79
+
80
+ // Check if section already exists
81
+ const startIdx = content.indexOf(DROID_SKILLS_START);
82
+ const endIdx = content.indexOf(DROID_SKILLS_END);
83
+
84
+ if (startIdx !== -1 && endIdx !== -1) {
85
+ // Replace existing section
86
+ content = content.slice(0, startIdx) + skillsSection + content.slice(endIdx + DROID_SKILLS_END.length);
87
+ } else if (skillsSection) {
88
+ // Append new section
89
+ content = content.trim() + '\n\n' + skillsSection + '\n';
90
+ }
91
+
92
+ // Ensure parent directory exists
93
+ const configDir = dirname(configPath);
94
+ if (!existsSync(configDir)) {
95
+ mkdirSync(configDir, { recursive: true });
96
+ }
97
+
98
+ writeFileSync(configPath, content, 'utf-8');
99
+ }
100
+
101
+ /**
102
+ * Load a skill manifest from a skill directory
103
+ */
104
+ export function loadSkillManifest(skillDir: string): SkillManifest | null {
105
+ const manifestPath = join(skillDir, 'SKILL.yaml');
106
+
107
+ if (!existsSync(manifestPath)) {
108
+ return null;
109
+ }
110
+
111
+ try {
112
+ const content = readFileSync(manifestPath, 'utf-8');
113
+ return YAML.parse(content) as SkillManifest;
114
+ } catch {
115
+ return null;
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Get all bundled skills
121
+ */
122
+ export function getBundledSkills(): SkillManifest[] {
123
+ if (!existsSync(BUNDLED_SKILLS_DIR)) {
124
+ return [];
125
+ }
126
+
127
+ const skillDirs = readdirSync(BUNDLED_SKILLS_DIR, { withFileTypes: true })
128
+ .filter((dirent) => dirent.isDirectory())
129
+ .map((dirent) => dirent.name);
130
+
131
+ const skills: SkillManifest[] = [];
132
+
133
+ for (const skillDir of skillDirs) {
134
+ const manifest = loadSkillManifest(join(BUNDLED_SKILLS_DIR, skillDir));
135
+ if (manifest) {
136
+ skills.push(manifest);
137
+ }
138
+ }
139
+
140
+ return skills;
141
+ }
142
+
143
+ /**
144
+ * Check if a skill is installed
145
+ */
146
+ export function isSkillInstalled(skillName: string): boolean {
147
+ const config = loadConfig();
148
+ return skillName in config.skills;
149
+ }
150
+
151
+ /**
152
+ * Get installed skill info
153
+ */
154
+ export function getInstalledSkill(skillName: string): InstalledSkill | null {
155
+ const config = loadConfig();
156
+ return config.skills[skillName] || null;
157
+ }
158
+
159
+ /**
160
+ * Install a skill
161
+ */
162
+ export function installSkill(skillName: string): { success: boolean; message: string } {
163
+ const config = loadConfig();
164
+ const bundledSkillDir = join(BUNDLED_SKILLS_DIR, skillName);
165
+
166
+ if (!existsSync(bundledSkillDir)) {
167
+ return { success: false, message: `Skill '${skillName}' not found` };
168
+ }
169
+
170
+ const manifest = loadSkillManifest(bundledSkillDir);
171
+ if (!manifest) {
172
+ return { success: false, message: `Invalid skill manifest for '${skillName}'` };
173
+ }
174
+
175
+ // Check dependencies
176
+ if (manifest.dependencies) {
177
+ for (const dep of manifest.dependencies) {
178
+ if (!isSkillInstalled(dep)) {
179
+ return {
180
+ success: false,
181
+ message: `Missing dependency: '${dep}'. Install it first with \`droid install ${dep}\``,
182
+ };
183
+ }
184
+ }
185
+ }
186
+
187
+ const skillsPath = getSkillsInstallPath(config.ai_tool);
188
+ const targetSkillDir = join(skillsPath, skillName);
189
+
190
+ // Ensure skills directory exists
191
+ if (!existsSync(skillsPath)) {
192
+ mkdirSync(skillsPath, { recursive: true });
193
+ }
194
+
195
+ // Copy SKILL.md (the actual skill file for Claude Code / OpenCode)
196
+ const skillMdSource = join(bundledSkillDir, 'SKILL.md');
197
+ if (existsSync(skillMdSource)) {
198
+ if (!existsSync(targetSkillDir)) {
199
+ mkdirSync(targetSkillDir, { recursive: true });
200
+ }
201
+ const skillMdTarget = join(targetSkillDir, 'SKILL.md');
202
+ const content = readFileSync(skillMdSource, 'utf-8');
203
+ writeFileSync(skillMdTarget, content);
204
+ }
205
+
206
+ // Copy commands if present
207
+ const commandsSource = join(bundledSkillDir, 'commands');
208
+ if (existsSync(commandsSource)) {
209
+ const commandsPath = getCommandsInstallPath(config.ai_tool);
210
+ if (!existsSync(commandsPath)) {
211
+ mkdirSync(commandsPath, { recursive: true });
212
+ }
213
+ cpSync(commandsSource, commandsPath, { recursive: true });
214
+ }
215
+
216
+ // Update config
217
+ config.skills[skillName] = {
218
+ version: manifest.version,
219
+ installed_at: new Date().toISOString(),
220
+ };
221
+ saveConfig(config);
222
+
223
+ // Update AI tool's config file with skill reference
224
+ const installedSkillNames = Object.keys(config.skills);
225
+ updateAIConfigSkills(config.ai_tool, installedSkillNames);
226
+
227
+ return { success: true, message: `Installed ${skillName} v${manifest.version}` };
228
+ }
229
+
230
+ /**
231
+ * Uninstall a skill
232
+ */
233
+ export function uninstallSkill(skillName: string): { success: boolean; message: string } {
234
+ const config = loadConfig();
235
+
236
+ if (!isSkillInstalled(skillName)) {
237
+ return { success: false, message: `Skill '${skillName}' is not installed` };
238
+ }
239
+
240
+ // Remove skill files from AI tool location
241
+ const skillsPath = getSkillsInstallPath(config.ai_tool);
242
+ const skillDir = join(skillsPath, skillName);
243
+ if (existsSync(skillDir)) {
244
+ rmSync(skillDir, { recursive: true });
245
+ }
246
+
247
+ // Remove command files if they exist
248
+ const bundledSkillDir = join(BUNDLED_SKILLS_DIR, skillName);
249
+ const commandsSource = join(bundledSkillDir, 'commands');
250
+ if (existsSync(commandsSource)) {
251
+ const commandsPath = getCommandsInstallPath(config.ai_tool);
252
+ const commandFiles = readdirSync(commandsSource);
253
+ for (const file of commandFiles) {
254
+ const commandPath = join(commandsPath, file);
255
+ if (existsSync(commandPath)) {
256
+ rmSync(commandPath);
257
+ }
258
+ }
259
+ }
260
+
261
+ // Remove from config
262
+ delete config.skills[skillName];
263
+ saveConfig(config);
264
+
265
+ // Update AI tool's config file to remove skill reference
266
+ const installedSkillNames = Object.keys(config.skills);
267
+ updateAIConfigSkills(config.ai_tool, installedSkillNames);
268
+
269
+ return { success: true, message: `Uninstalled ${skillName}` };
270
+ }
271
+
272
+ /**
273
+ * Get skill status display string
274
+ */
275
+ export function getSkillStatusDisplay(status?: SkillStatus): string {
276
+ switch (status) {
277
+ case SkillStatus.Alpha:
278
+ return '[alpha]';
279
+ case SkillStatus.Beta:
280
+ return '[beta]';
281
+ case SkillStatus.Stable:
282
+ default:
283
+ return '';
284
+ }
285
+ }
@@ -0,0 +1,65 @@
1
+ import { describe, it, expect } from 'bun:test';
2
+ import {
3
+ AITool,
4
+ BuiltInOutput,
5
+ SkillStatus,
6
+ ConfigOptionType,
7
+ getAITag,
8
+ } from './types.js';
9
+
10
+ describe('AITool enum', () => {
11
+ it('should have correct values', () => {
12
+ expect(AITool.ClaudeCode).toBe('claude-code');
13
+ expect(AITool.OpenCode).toBe('opencode');
14
+ });
15
+
16
+ it('should be usable in switch statements', () => {
17
+ const tool = AITool.ClaudeCode;
18
+ let result = '';
19
+
20
+ switch (tool) {
21
+ case AITool.ClaudeCode:
22
+ result = 'claude';
23
+ break;
24
+ case AITool.OpenCode:
25
+ result = 'opencode';
26
+ break;
27
+ }
28
+
29
+ expect(result).toBe('claude');
30
+ });
31
+ });
32
+
33
+ describe('BuiltInOutput enum', () => {
34
+ it('should have correct values', () => {
35
+ expect(BuiltInOutput.Terminal).toBe('terminal');
36
+ expect(BuiltInOutput.Editor).toBe('editor');
37
+ });
38
+
39
+ it('should only have Terminal and Editor (Brain is skill-provided)', () => {
40
+ const values = Object.values(BuiltInOutput);
41
+ expect(values).toEqual(['terminal', 'editor']);
42
+ });
43
+ });
44
+
45
+ describe('SkillStatus enum', () => {
46
+ it('should have correct values', () => {
47
+ expect(SkillStatus.Stable).toBe('stable');
48
+ expect(SkillStatus.Beta).toBe('beta');
49
+ expect(SkillStatus.Alpha).toBe('alpha');
50
+ });
51
+ });
52
+
53
+ describe('ConfigOptionType enum', () => {
54
+ it('should have correct values', () => {
55
+ expect(ConfigOptionType.String).toBe('string');
56
+ expect(ConfigOptionType.Boolean).toBe('boolean');
57
+ expect(ConfigOptionType.Select).toBe('select');
58
+ });
59
+ });
60
+
61
+ describe('getAITag', () => {
62
+ it('should always return @droid', () => {
63
+ expect(getAITag()).toBe('@droid');
64
+ });
65
+ });
@@ -0,0 +1,68 @@
1
+ export enum AITool {
2
+ ClaudeCode = 'claude-code',
3
+ OpenCode = 'opencode',
4
+ }
5
+
6
+ /**
7
+ * Get the AI's mention tag
8
+ * Always @droid - consistent branding regardless of underlying AI tool
9
+ */
10
+ export function getAITag(): string {
11
+ return '@droid';
12
+ }
13
+
14
+ // Built-in output preferences (always available)
15
+ export enum BuiltInOutput {
16
+ Terminal = 'terminal',
17
+ Editor = 'editor',
18
+ }
19
+
20
+ // Output preference in config can be a built-in OR a skill name
21
+ export type OutputPreference = BuiltInOutput | string;
22
+
23
+ export enum SkillStatus {
24
+ Stable = 'stable',
25
+ Beta = 'beta',
26
+ Alpha = 'alpha',
27
+ }
28
+
29
+ export enum ConfigOptionType {
30
+ String = 'string',
31
+ Boolean = 'boolean',
32
+ Select = 'select',
33
+ }
34
+
35
+ export interface DroidConfig {
36
+ ai_tool: AITool;
37
+ user_mention: string;
38
+ output_preference: OutputPreference;
39
+ git_username: string;
40
+ skills: Record<string, InstalledSkill>;
41
+ }
42
+
43
+ export interface InstalledSkill {
44
+ version: string;
45
+ installed_at: string;
46
+ }
47
+
48
+ export interface SkillManifest {
49
+ name: string;
50
+ description: string;
51
+ version: string;
52
+ status?: SkillStatus;
53
+ dependencies?: string[];
54
+ config_schema?: Record<string, ConfigOption>;
55
+ // Skills can declare they provide an output target
56
+ provides_output?: boolean;
57
+ }
58
+
59
+ export interface ConfigOption {
60
+ type: ConfigOptionType;
61
+ description: string;
62
+ default?: string | boolean;
63
+ options?: string[]; // for select type
64
+ }
65
+
66
+ export interface SkillOverrides {
67
+ [key: string]: string | boolean | number;
68
+ }
@@ -0,0 +1,23 @@
1
+ import { describe, it, expect } from 'bun:test';
2
+ import { getVersion } from './version.js';
3
+
4
+ describe('getVersion', () => {
5
+ it('should return a version string', () => {
6
+ const version = getVersion();
7
+ expect(typeof version).toBe('string');
8
+ });
9
+
10
+ it('should return a valid semver format', () => {
11
+ const version = getVersion();
12
+ // Basic semver check: major.minor.patch
13
+ const semverRegex = /^\d+\.\d+\.\d+/;
14
+ expect(version).toMatch(semverRegex);
15
+ });
16
+
17
+ it('should return version from package.json', () => {
18
+ const version = getVersion();
19
+ // Version should be a non-empty semver string (actual value changes with releases)
20
+ expect(version.length).toBeGreaterThan(0);
21
+ expect(version).not.toBe('0.0.0');
22
+ });
23
+ });
@@ -0,0 +1,47 @@
1
+ import { readFileSync } from 'fs';
2
+ import { fileURLToPath } from 'url';
3
+ import { dirname, join } from 'path';
4
+ import { execSync } from 'child_process';
5
+ import chalk from 'chalk';
6
+
7
+ const __dirname = dirname(fileURLToPath(import.meta.url));
8
+ const packageJsonPath = join(__dirname, '../../package.json');
9
+
10
+ /**
11
+ * Get the current version from package.json
12
+ */
13
+ export function getVersion(): string {
14
+ try {
15
+ const pkg = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
16
+ return pkg.version as string;
17
+ } catch {
18
+ return '0.0.0';
19
+ }
20
+ }
21
+
22
+ /**
23
+ * Check for updates (non-blocking)
24
+ * Shows a message if a new version is available
25
+ */
26
+ export async function checkForUpdates(): Promise<void> {
27
+ try {
28
+ const currentVersion = getVersion();
29
+
30
+ // Use npm view to check latest version
31
+ const latestVersion = execSync('npm view @orderful/droid version 2>/dev/null', {
32
+ encoding: 'utf-8',
33
+ timeout: 3000,
34
+ }).trim();
35
+
36
+ if (latestVersion && latestVersion !== currentVersion) {
37
+ console.log(
38
+ chalk.yellow(
39
+ `\n⚠️ A new version of droid is available (${currentVersion} → ${latestVersion})`
40
+ )
41
+ );
42
+ console.log(chalk.yellow(` Run ${chalk.bold('droid update')} to upgrade\n`));
43
+ }
44
+ } catch {
45
+ // Silently fail - update check is non-critical
46
+ }
47
+ }
@@ -0,0 +1,65 @@
1
+ # Comments Skill
2
+
3
+ Enable inline conversation in any file using `> @droid` and `> @{user}` markers.
4
+
5
+ ## How It Works
6
+
7
+ Leave comments for the AI using `> @droid`:
8
+
9
+ ```markdown
10
+ > @droid What do you think of this approach?
11
+
12
+ > @droid Can you add error handling here?
13
+ ```
14
+
15
+ The AI will respond with `> @{user_mention}` (configured in droid setup, e.g., `@fry`):
16
+
17
+ ```markdown
18
+ > @fry I think this approach is solid. One consideration...
19
+ ```
20
+
21
+ ## Tag Convention
22
+
23
+ | Who | Tag |
24
+ |-----|-----|
25
+ | AI | `@droid` (+ any configured aliases) |
26
+ | User | Configured (e.g., `@fry`) |
27
+
28
+ ## Commands
29
+
30
+ - `/comments check` - Scan for AI markers and address each one
31
+ - `/comments check {path}` - Scope to specific file or directory
32
+ - `/comments cleanup` - Find resolved comment threads and remove them
33
+
34
+ ## Behavior
35
+
36
+ ### On `/comments check`:
37
+ 1. Search for `> @droid` (and any configured `ai_mentions`) in the specified scope
38
+ 2. If there's a `git diff`, check those files first for relevant context
39
+ 3. For each comment, determine intent:
40
+ - **Action request** ("do X", "add Y", "fix Z") → Execute the action, remove the comment
41
+ - **Question** ("what do you think", "should we") → Respond with `> @{user_mention}`, keep original comment
42
+
43
+ ### On `/comments cleanup`:
44
+ 1. Find AI tag and `> @{user_mention}` pairs where conversation appears resolved
45
+ 2. Remove both markers
46
+ 3. Output a summary of what was discussed/decided
47
+
48
+ ## Configuration
49
+
50
+ Configured via `~/.droid/skills/comments/overrides.yaml` or inherits from global `~/.droid/config.yaml`:
51
+
52
+ ```yaml
53
+ # Override the user mention (default: from global config)
54
+ user_mention: "@fry"
55
+
56
+ # Additional AI mentions to recognize (e.g., if you're used to @claude)
57
+ ai_mentions: "@claude"
58
+
59
+ # Keep comments after addressing them (default: false)
60
+ preserve_comments: false
61
+ ```
62
+
63
+ ## File Type Support
64
+
65
+ Works in any text file. Respects `.gitignore` and skips common build directories (node_modules, dist, .git, etc.).