diff --git a/examples/plugin_example.py b/examples/plugin_example.py new file mode 100644 index 00000000..ac179f89 --- /dev/null +++ b/examples/plugin_example.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python3 +"""Example demonstrating how to use plugins with Claude Code SDK. + +Plugins allow you to extend Claude Code with custom commands, agents, skills, +and hooks. This example shows how to load a local plugin and verify it's +loaded by checking the system message. + +The demo plugin is located in examples/plugins/demo-plugin/ and provides +a custom /greet command. +""" + +from pathlib import Path + +import anyio + +from claude_agent_sdk import ( + ClaudeAgentOptions, + SystemMessage, + query, +) + + +async def plugin_example(): + """Example showing plugins being loaded in the system message.""" + print("=== Plugin Example ===\n") + + # Get the path to the demo plugin + # In production, you can use any path to your plugin directory + plugin_path = Path(__file__).parent / "plugins" / "demo-plugin" + + options = ClaudeAgentOptions( + plugins=[ + { + "type": "local", + "path": str(plugin_path), + } + ], + max_turns=1, # Limit to one turn for quick demo + ) + + print(f"Loading plugin from: {plugin_path}\n") + + found_plugins = False + async for message in query(prompt="Hello!", options=options): + if isinstance(message, SystemMessage) and message.subtype == "init": + print("System initialized!") + print(f"System message data keys: {list(message.data.keys())}\n") + + # Check for plugins in the system message + plugins_data = message.data.get("plugins", []) + if plugins_data: + print("Plugins loaded:") + for plugin in plugins_data: + print(f" - {plugin.get('name')} (path: {plugin.get('path')})") + found_plugins = True + else: + print("Note: Plugin was passed via CLI but may not appear in system message.") + print(f"Plugin path configured: {plugin_path}") + found_plugins = True + + if found_plugins: + print("\nPlugin successfully configured!\n") + + +async def main(): + """Run all plugin examples.""" + await plugin_example() + + +if __name__ == "__main__": + anyio.run(main) diff --git a/examples/plugins/demo-plugin/.claude-plugin/plugin.json b/examples/plugins/demo-plugin/.claude-plugin/plugin.json new file mode 100644 index 00000000..a33038ef --- /dev/null +++ b/examples/plugins/demo-plugin/.claude-plugin/plugin.json @@ -0,0 +1,8 @@ +{ + "name": "demo-plugin", + "description": "A demo plugin showing how to extend Claude Code with custom commands", + "version": "1.0.0", + "author": { + "name": "Claude Code Team" + } +} diff --git a/examples/plugins/demo-plugin/commands/greet.md b/examples/plugins/demo-plugin/commands/greet.md new file mode 100644 index 00000000..5274b20e --- /dev/null +++ b/examples/plugins/demo-plugin/commands/greet.md @@ -0,0 +1,5 @@ +# Greet Command + +This is a custom greeting command from the demo plugin. + +When the user runs this command, greet them warmly and explain that this message came from a custom plugin loaded via the Python SDK. Tell them that plugins can be used to extend Claude Code with custom commands, agents, skills, and hooks. diff --git a/src/claude_agent_sdk/__init__.py b/src/claude_agent_sdk/__init__.py index 1a5ebf5f..cde28be0 100644 --- a/src/claude_agent_sdk/__init__.py +++ b/src/claude_agent_sdk/__init__.py @@ -39,6 +39,7 @@ PreCompactHookInput, PreToolUseHookInput, ResultMessage, + SdkPluginConfig, SettingSource, StopHookInput, SubagentStopHookInput, @@ -339,6 +340,8 @@ async def call_tool(name: str, arguments: dict[str, Any]) -> Any: # Agent support "AgentDefinition", "SettingSource", + # Plugin support + "SdkPluginConfig", # MCP Server Support "create_sdk_mcp_server", "tool", diff --git a/src/claude_agent_sdk/_internal/transport/subprocess_cli.py b/src/claude_agent_sdk/_internal/transport/subprocess_cli.py index f19403c7..1ec352f9 100644 --- a/src/claude_agent_sdk/_internal/transport/subprocess_cli.py +++ b/src/claude_agent_sdk/_internal/transport/subprocess_cli.py @@ -191,6 +191,14 @@ def _build_command(self) -> list[str]: ) cmd.extend(["--setting-sources", sources_value]) + # Add plugin directories + if self._options.plugins: + for plugin in self._options.plugins: + if plugin["type"] == "local": + cmd.extend(["--plugin-dir", plugin["path"]]) + else: + raise ValueError(f"Unsupported plugin type: {plugin['type']}") + # Add extra args for future CLI flags for flag, value in self._options.extra_args.items(): if value is None: diff --git a/src/claude_agent_sdk/types.py b/src/claude_agent_sdk/types.py index be1cb996..efeaf70b 100644 --- a/src/claude_agent_sdk/types.py +++ b/src/claude_agent_sdk/types.py @@ -406,6 +406,16 @@ class McpSdkServerConfig(TypedDict): ) +class SdkPluginConfig(TypedDict): + """SDK plugin configuration. + + Currently only local plugins are supported via the 'local' type. + """ + + type: Literal["local"] + path: str + + # Content block types @dataclass class TextBlock: @@ -542,6 +552,8 @@ class ClaudeAgentOptions: agents: dict[str, AgentDefinition] | None = None # Setting sources to load (user, project, local) setting_sources: list[SettingSource] | None = None + # Plugin configurations for custom plugins + plugins: list[SdkPluginConfig] = field(default_factory=list) # SDK Control Protocol