Skip to content

Commit fc1eab8

Browse files
committed
Implement export/install and update examples/tests
Implement bundle and skill export/install functionality and update examples and tests accordingly. Added export_claude_plugin and install_claude_skills implementations on Bundle (writes plugin manifest, skill files, and .musher-managed markers; supports cleaning). Implemented SkillHandle export methods (export_openai_local_skill, export_openai_inline_skill, export_path, export_zip) and added plugin_name to ClaudePluginExport. Updated example scripts to demonstrate Claude/OpenAI usage and adjusted sample bundle refs. Added optional examples dependency (openai-agents) in pyproject.toml. Updated tests to validate the new export/install behaviors and file outputs.
1 parent 47643f6 commit fc1eab8

11 files changed

Lines changed: 433 additions & 78 deletions

File tree

examples/claude/export_plugin.py

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,21 +8,39 @@
88

99
import musher
1010

11-
# NOTE: Bundle references below (e.g. "acme/agent-toolkit:2.0.0") are
11+
# NOTE: Bundle references below (e.g. "acme/engineering-workflows:2.0.0") are
1212
# placeholders. Replace with a real bundle ref from your Musher registry.
1313

1414
# Credentials auto-discovered from MUSHER_API_KEY env var, keyring,
1515
# or credential file. To override: musher.configure(token="your-token")
1616

17-
bundle = musher.pull("acme/agent-toolkit:2.0.0")
17+
bundle = musher.pull("acme/engineering-workflows:2.0.0")
1818

1919
# Select only the skills needed for this session
20-
selection = bundle.select(skills=["csv-insights", "incident-summary"])
20+
selection = bundle.select(skills=["researching-repos", "drafting-release-notes"])
2121

22-
# PREVIEW: export_claude_plugin() is not yet implemented — will raise NotImplementedError.
23-
plugin = selection.export_claude_plugin("safe-tools", dest=Path("./plugins"))
22+
# Export as a local Claude plugin with a namespaced plugin name.
23+
# Skills will be accessible as "team-workflows:researching-repos", etc.
24+
plugin = selection.export_claude_plugin("team-workflows", dest=Path("./plugins"))
2425
print(f"Plugin exported to: {plugin.path}")
2526

2627
# Verify only the selected skills are present
2728
for skill in selection.skills():
2829
print(f" Skill: {skill.name}{skill.description}")
30+
31+
# To load this plugin with the Claude Agent SDK:
32+
#
33+
# from claude_agent_sdk import ClaudeAgentOptions, query
34+
#
35+
# options = ClaudeAgentOptions(
36+
# cwd=str(Path(__file__).resolve().parents[1]),
37+
# plugins=[{"type": "local", "path": str(plugin.path)}],
38+
# allowed_tools=["Skill", "Read", "Grep", "Glob", "Bash"],
39+
# max_turns=3,
40+
# )
41+
#
42+
# async for message in query(
43+
# prompt="What custom commands do you have available?",
44+
# options=options,
45+
# ):
46+
# ...

examples/claude/install_project_skills.py

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,23 +8,39 @@
88

99
import musher
1010

11-
# NOTE: Bundle references below (e.g. "acme/agent-toolkit:2.0.0") are
11+
# NOTE: Bundle references below (e.g. "acme/engineering-workflows:2.0.0") are
1212
# placeholders. Replace with a real bundle ref from your Musher registry.
1313

1414
# Credentials auto-discovered from MUSHER_API_KEY env var, keyring,
1515
# or credential file. To override: musher.configure(token="your-token")
1616

17-
bundle = musher.pull("acme/agent-toolkit:2.0.0")
17+
bundle = musher.pull("acme/engineering-workflows:2.0.0")
1818

19-
# Install all skills to the project-level Claude skills directory
20-
skills_dir = Path(".claude/skills")
21-
22-
# PREVIEW: install_claude_skills() is not yet implemented — will raise NotImplementedError.
19+
# Install specific skills to the project-level Claude skills directory.
2320
# clean=True removes stale Musher-managed skill installs for this bundle
2421
# from the target directory. It does NOT affect unrelated user-managed skills.
25-
bundle.install_claude_skills(skills_dir, clean=True)
22+
skills_dir = Path(".claude/skills")
23+
bundle.install_claude_skills(
24+
skills_dir,
25+
skills=["researching-repos", "drafting-release-notes"],
26+
clean=True,
27+
)
2628

27-
print(f"Installed {len(bundle.skills())} skills to {skills_dir}")
29+
print(f"Installed skills to {skills_dir}")
2830
for skill in bundle.skills():
2931
print(f" {skill.name}: {skill.description}")
3032
print(f" Files: {len(skill.files())}")
33+
34+
# To use these skills with the Claude Agent SDK:
35+
#
36+
# from claude_agent_sdk import ClaudeAgentOptions, query
37+
#
38+
# options = ClaudeAgentOptions(
39+
# cwd=str(Path(__file__).resolve().parents[1]),
40+
# setting_sources=["project"],
41+
# allowed_tools=["Skill", "Read", "Grep", "Glob", "Bash"],
42+
# max_turns=4,
43+
# )
44+
#
45+
# async for message in query(prompt="What skills are available?", options=options):
46+
# ...
Lines changed: 39 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,51 @@
1-
"""Example: Export a skill as an inline zip for OpenAI Agents.
1+
"""Load a Musher skill as an inline hosted OpenAI shell skill.
22
3-
Exports a single skill as a base64-encoded zip suitable for hosted inline
4-
consumption by the OpenAI Agents SDK.
3+
Requires: pip install openai-agents
54
"""
65

6+
import asyncio
7+
8+
from agents import Agent, Runner, ShellTool
9+
710
import musher
811

9-
# NOTE: Bundle references below (e.g. "acme/agent-toolkit:2.0.0") are
12+
# NOTE: Bundle references below (e.g. "acme/data-workflows:2.0.0") are
1013
# placeholders. Replace with a real bundle ref from your Musher registry.
1114

1215
# Credentials auto-discovered from MUSHER_API_KEY env var, keyring,
1316
# or credential file. To override: musher.configure(token="your-token")
1417

15-
bundle = musher.pull("acme/agent-toolkit:2.0.0")
16-
17-
# Get a single skill
18+
bundle = musher.pull("acme/data-workflows:2.0.0")
1819
skill = bundle.skill("csv-insights")
19-
20-
# PREVIEW: export_openai_inline_skill() is not yet implemented — will raise NotImplementedError.
2120
inline = skill.export_openai_inline_skill()
22-
print(f"Inline skill: {inline.name}")
23-
print(f" Base64 size: {len(inline.content_base64)} chars")
24-
print(f" Registration dict: {inline.to_dict()}")
21+
22+
agent = Agent(
23+
name="Hosted CSV Analyst",
24+
model="gpt-4.1",
25+
instructions="Use the inline skill when it helps.",
26+
tools=[
27+
ShellTool(
28+
environment={
29+
"type": "container_auto",
30+
"network_policy": {"type": "disabled"},
31+
"skills": [inline.to_dict()],
32+
}
33+
)
34+
],
35+
)
36+
37+
38+
async def main() -> None:
39+
result = await Runner.run(
40+
agent,
41+
(
42+
"Use the csv-insights skill. Create /mnt/data/orders.csv with columns "
43+
"id,region,amount,status and at least 8 rows. Then report total amount "
44+
"by region and count failed orders."
45+
),
46+
)
47+
print(result.final_output)
48+
49+
50+
if __name__ == "__main__":
51+
asyncio.run(main())
Lines changed: 78 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,90 @@
1-
"""Example: Export a skill as a local directory for OpenAI Agents.
1+
"""Load a Musher skill as a local OpenAI shell skill and use it on this repo.
22
3-
Exports a single skill to disk so it can be loaded as a local shell skill
4-
by the OpenAI Agents SDK.
3+
Requires: pip install openai-agents
54
"""
65

6+
import asyncio
7+
from pathlib import Path
8+
9+
from agents import (
10+
Agent,
11+
Runner,
12+
ShellCallOutcome,
13+
ShellCommandOutput,
14+
ShellCommandRequest,
15+
ShellResult,
16+
ShellTool,
17+
)
18+
719
import musher
820

9-
# NOTE: Bundle references below (e.g. "acme/agent-toolkit:2.0.0") are
21+
# NOTE: Bundle references below (e.g. "acme/engineering-workflows:2.0.0") are
1022
# placeholders. Replace with a real bundle ref from your Musher registry.
1123

1224
# Credentials auto-discovered from MUSHER_API_KEY env var, keyring,
1325
# or credential file. To override: musher.configure(token="your-token")
1426

15-
bundle = musher.pull("acme/agent-toolkit:2.0.0")
27+
PROJECT_DIR = Path(__file__).resolve().parents[2]
28+
29+
30+
class RepoShell:
31+
"""Minimal local shell executor for the OpenAI Agents SDK."""
32+
33+
def __init__(self, cwd: Path) -> None:
34+
self.cwd = cwd
35+
36+
async def __call__(self, request: ShellCommandRequest) -> ShellResult:
37+
outputs: list[ShellCommandOutput] = []
38+
39+
for command in request.data.action.commands:
40+
proc = await asyncio.create_subprocess_shell(
41+
command,
42+
cwd=self.cwd,
43+
stdout=asyncio.subprocess.PIPE,
44+
stderr=asyncio.subprocess.PIPE,
45+
)
46+
stdout, stderr = await proc.communicate()
47+
48+
outputs.append(
49+
ShellCommandOutput(
50+
command=command,
51+
stdout=stdout.decode("utf-8", errors="ignore"),
52+
stderr=stderr.decode("utf-8", errors="ignore"),
53+
outcome=ShellCallOutcome(type="exit", exit_code=proc.returncode),
54+
)
55+
)
56+
57+
return ShellResult(
58+
output=outputs,
59+
max_output_length=request.data.action.max_output_length,
60+
provider_data={"working_directory": str(self.cwd)},
61+
)
62+
63+
64+
bundle = musher.pull("acme/engineering-workflows:2.0.0")
65+
skill = bundle.skill("repo-maintainer")
66+
local = skill.export_openai_local_skill(dest=PROJECT_DIR / ".musher" / "openai" / "skills")
67+
68+
agent = Agent(
69+
name="Repo Triage Assistant",
70+
model="gpt-4.1",
71+
instructions="Use the local skill when it helps. Keep the answer concise and actionable.",
72+
tools=[
73+
ShellTool(
74+
executor=RepoShell(PROJECT_DIR),
75+
environment={"type": "local", "skills": [local.to_dict()]},
76+
)
77+
],
78+
)
79+
80+
81+
async def main() -> None:
82+
result = await Runner.run(
83+
agent,
84+
"Use the repo-maintainer skill to inspect this repository and tell me the first onboarding issue I should fix.",
85+
)
86+
print(result.final_output)
1687

17-
# Get a single skill
18-
skill = bundle.skill("csv-insights")
1988

20-
# PREVIEW: export_openai_local_skill() is not yet implemented — will raise NotImplementedError.
21-
local = skill.export_openai_local_skill()
22-
print(f"Local skill: {local.name} at {local.path}")
23-
print(f" Registration dict: {local.to_dict()}")
89+
if __name__ == "__main__":
90+
asyncio.run(main())

pyproject.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@ dependencies = [
1212
"pydantic>=2.12",
1313
]
1414

15+
[project.optional-dependencies]
16+
examples = [
17+
"openai-agents>=0.1",
18+
]
19+
1520
[dependency-groups]
1621
dev = [
1722
"ruff>=0.15.2",

src/musher/_bundle.py

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,19 @@
22

33
from __future__ import annotations
44

5+
import json as _json
6+
import shutil
57
from dataclasses import dataclass, field
6-
from pathlib import PurePosixPath
8+
from datetime import UTC, datetime
9+
from pathlib import Path, PurePosixPath
710
from typing import TYPE_CHECKING
811

912
from pydantic import BaseModel, ConfigDict
1013
from pydantic.alias_generators import to_camel
1114

15+
if TYPE_CHECKING:
16+
from musher._export import ClaudePluginExport
17+
1218
from musher._handles import (
1319
AgentSpecHandle,
1420
BundleSelection,
@@ -19,11 +25,6 @@
1925
)
2026
from musher._types import AssetType, BundleSourceType, BundleVersionState
2127

22-
if TYPE_CHECKING:
23-
from pathlib import Path
24-
25-
from musher._export import ClaudePluginExport
26-
2728

2829
class _SDKSchema(BaseModel):
2930
"""Base schema with camelCase aliasing, matching the platform API wire format."""
@@ -295,8 +296,8 @@ def export_claude_plugin(
295296
skills: list[str] | None = None,
296297
dest: Path | None = None,
297298
) -> ClaudePluginExport:
298-
"""Export bundle as a Claude plugin. Raises NotImplementedError (stub)."""
299-
raise NotImplementedError
299+
"""Export bundle as a Claude plugin."""
300+
return self.select(skills=skills).export_claude_plugin(plugin_name, dest=dest)
300301

301302
def install_vscode_skills(
302303
self,
@@ -315,8 +316,32 @@ def install_claude_skills(
315316
*,
316317
clean: bool = False,
317318
) -> None:
318-
"""Install skills to a Claude skills directory. Raises NotImplementedError (stub)."""
319-
raise NotImplementedError
319+
"""Install skills to a Claude skills directory."""
320+
selection = self.select(skills=skills)
321+
322+
if clean and dest.is_dir():
323+
for child in dest.iterdir():
324+
marker = child / ".musher-managed"
325+
if child.is_dir() and marker.is_file():
326+
try:
327+
info = _json.loads(marker.read_text(encoding="utf-8"))
328+
except (OSError, _json.JSONDecodeError):
329+
continue
330+
if info.get("bundle_ref") == self.ref:
331+
shutil.rmtree(child)
332+
333+
dest.mkdir(parents=True, exist_ok=True)
334+
335+
for skill in selection.skills():
336+
skill.export_path(dest=dest)
337+
marker_data = {
338+
"bundle_ref": self.ref,
339+
"bundle_version": self.version,
340+
"installed_at": datetime.now(UTC).isoformat(),
341+
}
342+
(dest / skill.name / ".musher-managed").write_text(
343+
_json.dumps(marker_data, indent=2) + "\n", encoding="utf-8"
344+
)
320345

321346
def write_lockfile(self, dest: Path | None = None) -> Path:
322347
"""Write a lockfile for the current bundle. Raises NotImplementedError (stub)."""

src/musher/_export.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
class ClaudePluginExport:
1414
"""Result of exporting a bundle selection as a Claude plugin."""
1515

16+
plugin_name: str
1617
path: Path
1718

1819

0 commit comments

Comments
 (0)