Skip to content

Commit b4a3a41

Browse files
author
jzhu
committed
feat: Add support for skills for Droid assistant
- Add 'droid' to SKILL_INSTALL_DIRS with path ~/.factory/skills - Add 'droid' to VALID_APP_TYPES for skill commands - Update CLI help text to include Droid - Update tests to verify Droid support
1 parent 639dffe commit b4a3a41

File tree

3 files changed

+75
-46
lines changed

3 files changed

+75
-46
lines changed

code_assistant_manager/cli/skills_commands.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,12 @@
2222
logger = logging.getLogger(__name__)
2323

2424
skill_app = typer.Typer(
25-
help="Manage skills for AI assistants (Claude, Codex, Gemini)",
25+
help="Manage skills for AI assistants (Claude, Codex, Gemini, Droid)",
2626
no_args_is_help=True,
2727
)
2828

2929
# Valid app types
30-
VALID_APP_TYPES = ["claude", "codex", "gemini"]
30+
VALID_APP_TYPES = ["claude", "codex", "gemini", "droid"]
3131

3232

3333
def _get_skill_manager() -> SkillManager:

code_assistant_manager/skills.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ def _load_builtin_skill_repos() -> List[Dict]:
2929
# Look for skill_repos.json in the package directory
3030
package_dir = Path(__file__).parent
3131
repos_file = package_dir / "skill_repos.json"
32-
32+
3333
if repos_file.exists():
3434
try:
3535
with open(repos_file, "r", encoding="utf-8") as f:
@@ -47,7 +47,7 @@ def _load_builtin_skill_repos() -> List[Dict]:
4747
]
4848
except Exception as e:
4949
logger.warning(f"Failed to load builtin skill repos: {e}")
50-
50+
5151
# Fallback defaults if file not found
5252
return [
5353
{
@@ -75,6 +75,7 @@ def _load_builtin_skill_repos() -> List[Dict]:
7575
"claude": Path.home() / ".claude" / "skills",
7676
"codex": Path.home() / ".codex" / "skills",
7777
"gemini": Path.home() / ".gemini" / "skills",
78+
"droid": Path.home() / ".factory" / "skills",
7879
}
7980

8081

@@ -388,7 +389,9 @@ def _download_and_install_skill(self, skill: Skill, install_dir: Path) -> None:
388389
branch = skill.repo_branch or "main"
389390

390391
# Try downloading the repo
391-
temp_dir, actual_branch = self._download_repo(skill.repo_owner, skill.repo_name, branch)
392+
temp_dir, actual_branch = self._download_repo(
393+
skill.repo_owner, skill.repo_name, branch
394+
)
392395

393396
try:
394397
# Determine the source path within the downloaded repo
@@ -614,7 +617,9 @@ def _fetch_skills_from_repo(self, repo: SkillRepo) -> List[Skill]:
614617
Returns:
615618
List of skills found in the repository
616619
"""
617-
temp_dir, actual_branch = self._download_repo(repo.owner, repo.name, repo.branch)
620+
temp_dir, actual_branch = self._download_repo(
621+
repo.owner, repo.name, repo.branch
622+
)
618623
skills = []
619624

620625
try:

tests/unit/test_skills.py

Lines changed: 64 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,11 @@
99
import pytest
1010

1111
from code_assistant_manager.skills import (
12+
DEFAULT_SKILL_REPOS,
13+
SKILL_INSTALL_DIRS,
1214
Skill,
1315
SkillManager,
1416
SkillRepo,
15-
SKILL_INSTALL_DIRS,
16-
DEFAULT_SKILL_REPOS,
1717
)
1818

1919

@@ -210,18 +210,18 @@ def test_manager_delete(self, temp_config_dir):
210210
manager = SkillManager(temp_config_dir)
211211
skill = Skill(key="test", name="Test", description="Desc", directory="/path")
212212
manager.create(skill)
213-
213+
214214
manager.delete("test")
215215
assert manager.get("test") is None
216216

217217
def test_manager_add_remove_repo(self, temp_config_dir):
218218
"""Test adding and removing skill repos."""
219219
manager = SkillManager(temp_config_dir)
220-
220+
221221
# Get initial count (includes default repos)
222222
initial_repos = manager.get_repos()
223223
initial_count = len(initial_repos)
224-
224+
225225
repo = SkillRepo(owner="testowner", name="testrepo", branch="main")
226226
manager.add_repo(repo)
227227

@@ -292,11 +292,11 @@ def test_manager_nonexistent_operations_error(self, temp_config_dir):
292292
def test_manager_init_default_repos(self, temp_config_dir):
293293
"""Test initializing default repos."""
294294
manager = SkillManager(temp_config_dir)
295-
295+
296296
# Repos are auto-initialized now, so should already have defaults
297297
repos = manager.get_repos()
298298
assert len(repos) == len(DEFAULT_SKILL_REPOS)
299-
299+
300300
# Calling init_default_repos again should be idempotent
301301
manager.init_default_repos()
302302
repos = manager.get_repos()
@@ -305,38 +305,48 @@ def test_manager_init_default_repos(self, temp_config_dir):
305305
def test_manager_sync_installed_status(self, temp_config_dir, temp_install_dir):
306306
"""Test syncing installed status."""
307307
manager = SkillManager(temp_config_dir)
308-
308+
309309
# Create skills
310-
skill1 = Skill(key="test1", name="Test 1", description="Desc", directory="skill1")
311-
skill2 = Skill(key="test2", name="Test 2", description="Desc", directory="skill2")
310+
skill1 = Skill(
311+
key="test1", name="Test 1", description="Desc", directory="skill1"
312+
)
313+
skill2 = Skill(
314+
key="test2", name="Test 2", description="Desc", directory="skill2"
315+
)
312316
manager.create(skill1)
313317
manager.create(skill2)
314-
318+
315319
# Mock the install directory
316-
with patch.dict('code_assistant_manager.skills.SKILL_INSTALL_DIRS', {'test_app': temp_install_dir}):
320+
with patch.dict(
321+
"code_assistant_manager.skills.SKILL_INSTALL_DIRS",
322+
{"test_app": temp_install_dir},
323+
):
317324
# Create skill1 directory in install location
318325
(temp_install_dir / "skill1").mkdir(parents=True)
319-
326+
320327
manager.sync_installed_status("test_app")
321-
328+
322329
skills = manager.get_all()
323330
assert skills["test1"].installed is True
324331
assert skills["test2"].installed is False
325332

326333
def test_manager_get_installed_skills(self, temp_config_dir, temp_install_dir):
327334
"""Test getting installed skills."""
328335
manager = SkillManager(temp_config_dir)
329-
336+
330337
# Create a skill and a skill directory
331338
skill = Skill(key="test", name="Test", description="Desc", directory="my-skill")
332339
manager.create(skill)
333-
334-
with patch.dict('code_assistant_manager.skills.SKILL_INSTALL_DIRS', {'test_app': temp_install_dir}):
340+
341+
with patch.dict(
342+
"code_assistant_manager.skills.SKILL_INSTALL_DIRS",
343+
{"test_app": temp_install_dir},
344+
):
335345
# Create skill directory with SKILL.md
336346
skill_dir = temp_install_dir / "my-skill"
337347
skill_dir.mkdir(parents=True)
338348
(skill_dir / "SKILL.md").write_text("---\nname: My Skill\n---\n# My Skill")
339-
349+
340350
installed = manager.get_installed_skills("test_app")
341351
assert len(installed) == 1
342352
assert installed[0].name == "Test"
@@ -345,39 +355,41 @@ def test_manager_get_installed_skills(self, temp_config_dir, temp_install_dir):
345355
def test_parse_skill_metadata(self, temp_config_dir):
346356
"""Test parsing SKILL.md metadata."""
347357
manager = SkillManager(temp_config_dir)
348-
358+
349359
skill_md = temp_config_dir / "SKILL.md"
350-
skill_md.write_text("""---
360+
skill_md.write_text(
361+
"""---
351362
name: Test Skill
352363
description: A test skill description
353364
---
354365
355366
# Test Skill
356367
357368
This is the skill content.
358-
""")
359-
369+
"""
370+
)
371+
360372
meta = manager._parse_skill_metadata(skill_md)
361373
assert meta.get("name") == "Test Skill"
362374
assert meta.get("description") == "A test skill description"
363375

364376
def test_parse_skill_metadata_no_frontmatter(self, temp_config_dir):
365377
"""Test parsing SKILL.md without frontmatter."""
366378
manager = SkillManager(temp_config_dir)
367-
379+
368380
skill_md = temp_config_dir / "SKILL.md"
369381
skill_md.write_text("# Just Content\n\nNo frontmatter here.")
370-
382+
371383
meta = manager._parse_skill_metadata(skill_md)
372384
assert meta == {}
373385

374386
def test_parse_skill_metadata_invalid_yaml(self, temp_config_dir):
375387
"""Test parsing SKILL.md with invalid YAML."""
376388
manager = SkillManager(temp_config_dir)
377-
389+
378390
skill_md = temp_config_dir / "SKILL.md"
379391
skill_md.write_text("---\ninvalid: yaml: content:\n---\n# Content")
380-
392+
381393
meta = manager._parse_skill_metadata(skill_md)
382394
assert meta == {}
383395

@@ -401,28 +413,33 @@ def temp_install_dir(self):
401413
def mock_skill_repo(self, temp_config_dir):
402414
"""Create a mock skill repository structure."""
403415
repo_dir = temp_config_dir / "mock_repo"
404-
416+
405417
# Create skill structure
406418
skill_dir = repo_dir / "my-skill"
407419
skill_dir.mkdir(parents=True)
408-
(skill_dir / "SKILL.md").write_text("""---
420+
(skill_dir / "SKILL.md").write_text(
421+
"""---
409422
name: My Skill
410423
description: A test skill
411424
---
412425
# My Skill
413426
Content here.
414-
""")
427+
"""
428+
)
415429
(skill_dir / "config.json").write_text('{"key": "value"}')
416-
430+
417431
return repo_dir
418432

419433
def test_install_skill_no_repo_info(self, temp_config_dir, temp_install_dir):
420434
"""Test installing skill without repo info raises error."""
421435
manager = SkillManager(temp_config_dir)
422436
skill = Skill(key="test", name="Test", description="Desc", directory="my-skill")
423437
manager.create(skill)
424-
425-
with patch.dict('code_assistant_manager.skills.SKILL_INSTALL_DIRS', {'claude': temp_install_dir}):
438+
439+
with patch.dict(
440+
"code_assistant_manager.skills.SKILL_INSTALL_DIRS",
441+
{"claude": temp_install_dir},
442+
):
426443
with pytest.raises(ValueError, match="no repository information"):
427444
manager.install("test", "claude")
428445

@@ -439,18 +456,21 @@ def test_uninstall_skill(self, temp_config_dir, temp_install_dir):
439456
repo_name="repo",
440457
)
441458
manager.create(skill)
442-
459+
443460
# Create the skill directory
444461
skill_dir = temp_install_dir / "my-skill"
445462
skill_dir.mkdir(parents=True)
446463
(skill_dir / "SKILL.md").write_text("# Skill")
447-
448-
with patch.dict('code_assistant_manager.skills.SKILL_INSTALL_DIRS', {'claude': temp_install_dir}):
464+
465+
with patch.dict(
466+
"code_assistant_manager.skills.SKILL_INSTALL_DIRS",
467+
{"claude": temp_install_dir},
468+
):
449469
manager.uninstall("test", "claude")
450-
470+
451471
# Check skill directory was removed
452472
assert not skill_dir.exists()
453-
473+
454474
# Check skill is marked as uninstalled
455475
loaded = manager.get("test")
456476
assert loaded.installed is False
@@ -468,11 +488,14 @@ def test_uninstall_nonexistent_directory(self, temp_config_dir, temp_install_dir
468488
repo_name="repo",
469489
)
470490
manager.create(skill)
471-
472-
with patch.dict('code_assistant_manager.skills.SKILL_INSTALL_DIRS', {'claude': temp_install_dir}):
491+
492+
with patch.dict(
493+
"code_assistant_manager.skills.SKILL_INSTALL_DIRS",
494+
{"claude": temp_install_dir},
495+
):
473496
# Should not raise even if directory doesn't exist
474497
manager.uninstall("test", "claude")
475-
498+
476499
loaded = manager.get("test")
477500
assert loaded.installed is False
478501

@@ -485,6 +508,7 @@ def test_skill_install_dirs(self):
485508
assert "claude" in SKILL_INSTALL_DIRS
486509
assert "codex" in SKILL_INSTALL_DIRS
487510
assert "gemini" in SKILL_INSTALL_DIRS
511+
assert "droid" in SKILL_INSTALL_DIRS
488512

489513
def test_default_skill_repos(self):
490514
"""Test DEFAULT_SKILL_REPOS has expected structure."""

0 commit comments

Comments
 (0)