Skip to main content

Creating Extensions

Build your own Summon extensions using Python to add custom functionality and integrate with your favorite tools.

Extension Structure

A basic extension consists of:

Code
my-extension/
├── manifest.json    # Extension metadata
├── main.py          # Entry point
└── icons/           # Optional custom icons
    └── icon.svg

manifest.json

Every extension needs a manifest:

JSON
{
  "id": "my-extension",
  "name": "My Extension",
  "version": "1.0.0",
  "description": "A brief description",
  "keyword": "myext",
  "runtime": "persistent",
  "entry": "main.py",
  "icon": {
    "type": "sf_symbol",
    "value": "star.fill"
  },
  "view_type": "list",
  "permissions": {
    "shell": false,
    "storage": true,
    "clipboard": true,
    "network": true
  },
  "config_schema": {
    "api_key": {
      "type": "string",
      "description": "Your API key",
      "default": ""
    }
  }
}

main.py

The main entry point:

Python
#!/usr/bin/env python3
from summon import Extension, Item, SearchContext, sf_symbol, copy, markdown

class MyExtension(Extension):
    def search(self, query: str, context: SearchContext | None = None) -> list[Item]:
        if not query:
            return []
        
        return [
            Item(
                id="result-1",
                title=f"Search for: {query}",
                subtitle="Press Enter to copy",
                icon=sf_symbol("magnifyingglass"),
                action=copy(query),
                preview=markdown(f"## Query\n\n`{query}`"),
            )
        ]

if __name__ == "__main__":
    MyExtension().run()

API Reference

Extension Class

Base class for all extensions:

Python
class Extension:
    # Called when extension receives a search query
    def search(self, query: str, context: SearchContext | None = None) -> list[Item]:
        pass
    
    # Called when a custom action is triggered
    def on_action(self, item_id: str, action_type: str, action_data: dict) -> None:
        pass
    
    # Called when a form is submitted
    def on_form_submit(self, form_id: str, values: dict) -> list[Item] | None:
        pass

Item

Search result items:

Python
Item(
    id="unique-id",           # Required: unique identifier
    title="Title",            # Required: display title
    subtitle="Subtitle",      # Optional: secondary text
    icon=sf_symbol("star"),   # Optional: icon
    action=copy("text"),      # Optional: default action
    preview=markdown("# Hi"), # Optional: preview panel
    shortcut="cmd+shift+s",   # Optional: keyboard shortcut
    valid=True,               # Optional: can be executed
)

Icons

Create icons using helper functions:

Python
from summon import sf_symbol, emoji_icon, file_icon, url_icon

# SF Symbols (macOS system icons)
icon = sf_symbol("star.fill")
icon = sf_symbol("folder", color="#FF5500")

# Emoji
icon = emoji_icon("🚀")

# File icon (uses system file type icon)
icon = file_icon("/path/to/file.pdf")

# URL favicon
icon = url_icon("https://github.com")

Actions

Available action types:

Python
from summon import open_url, copy, open_file, reveal, paste, run, custom_action

# Open URL in browser
action = open_url("https://github.com")

# Copy text to clipboard
action = copy("Hello, World!")

# Open file with default app
action = open_file("/path/to/file.txt")

# Reveal file in Finder
action = reveal("/path/to/folder")

# Paste/type text at cursor
action = paste("Typed text")

# Run shell command
action = run("echo", args=["Hello", "World"])

# Custom action (handled by on_action)
action = custom_action("my_action", {"key": "value"})

Previews

Rich preview panel content:

Python
from summon import markdown, text, metadata, meta_field, form, loading, error

# Markdown preview
preview = markdown("""
# Title

**Bold** and *italic* text.

```python
print("Hello!")

""")

Plain text with optional syntax highlighting

preview = text("console.log('hello')", language="javascript")

Key-value metadata

preview = metadata( meta_field("Name", "Value", copyable=True), meta_field("Link", "Click here", url="https://example.com"), )

Loading state

preview = loading("Fetching data...")

Error state

preview = error("Something went wrong")

Code

### Forms

Interactive forms in previews:

```python
from summon import form, text_field, select_field, number_field, checkbox_field, option

preview = form(
    "my-form",
    text_field("name", "Your Name", required=True, placeholder="Enter name"),
    text_field("bio", "Bio", multiline=True),
    select_field("priority", "Priority", [
        option("low", "Low"),
        option("medium", "Medium"),
        option("high", "High"),
    ], default="medium"),
    number_field("count", "Count", min=1, max=100, default=10),
    checkbox_field("notify", "Send notification", default=True),
    title="Settings Form",
    submit_label="Save",
)

Handle form submission:

Python
def on_form_submit(self, form_id: str, values: dict) -> list[Item] | None:
    if form_id == "my-form":
        name = values.get("name", "")
        # Process form data
        self.host.toast(f"Saved settings for {name}", kind="success")
        return None  # Or return new items to display

Host API

Access host functionality:

Python
# Show toast notification
self.host.toast("Message", kind="success")  # success, info, warning, error

# Access configuration values
api_key = self.host.config.get("api_key", "")

Permissions

Request permissions in your manifest:

| Permission | Description | |------------|-------------| | clipboard | Read/write clipboard | | network | Make HTTP requests | | storage | Persistent key-value storage | | shell | Execute shell commands |

Examples

API Integration

Python
import requests
from summon import Extension, Item, SearchContext, sf_symbol, open_url, markdown

class GitHubExtension(Extension):
    def search(self, query: str, context: SearchContext | None = None) -> list[Item]:
        if not query or len(query) < 2:
            return []
        
        api_key = self.host.config.get("api_key", "")
        headers = {"Authorization": f"token {api_key}"} if api_key else {}
        
        response = requests.get(
            f"https://api.github.com/search/repositories?q={query}",
            headers=headers,
        )
        data = response.json()
        
        return [
            Item(
                id=f"repo-{repo['id']}",
                title=repo["full_name"],
                subtitle=repo.get("description", "No description"),
                icon=sf_symbol("folder.fill"),
                action=open_url(repo["html_url"]),
                preview=markdown(f"""
## {repo['full_name']}

{repo.get('description', 'No description')}

- Stars: {repo['stargazers_count']}
- Forks: {repo['forks_count']}
- Language: {repo.get('language', 'Unknown')}
"""),
            )
            for repo in data.get("items", [])[:10]
        ]

Shell Command Runner

Python
from summon import Extension, Item, SearchContext, sf_symbol, run, markdown

class ShellExtension(Extension):
    COMMANDS = {
        "ip": ("Get IP Address", "curl -s ifconfig.me"),
        "disk": ("Disk Usage", "df -h"),
        "mem": ("Memory Usage", "top -l 1 | head -n 10"),
    }
    
    def search(self, query: str, context: SearchContext | None = None) -> list[Item]:
        items = []
        for key, (title, cmd) in self.COMMANDS.items():
            if not query or query.lower() in key or query.lower() in title.lower():
                items.append(
                    Item(
                        id=f"cmd-{key}",
                        title=title,
                        subtitle=cmd,
                        icon=sf_symbol("terminal"),
                        action=run("bash", args=["-c", cmd]),
                        preview=markdown(f"```bash\n{cmd}\n```"),
                    )
                )
        return items

Testing

Test your extension locally:

  1. Place your extension folder in ~/.summon/extensions/
  2. Open Summon and type your extension's keyword
  3. Check Console.app for debug output and errors

Best Practices

  • Keep search results focused and relevant
  • Use meaningful SF Symbol icons
  • Provide keyboard shortcuts for common actions
  • Handle errors gracefully with try/except
  • Cache API responses when possible
  • Use valid=False for items that show previews but shouldn't execute

Manifest Reference

| Field | Type | Required | Description | |-------|------|----------|-------------| | id | string | Yes | Unique identifier | | name | string | Yes | Display name | | version | string | Yes | Semantic version | | description | string | Yes | Brief description | | keyword | string | Yes | Trigger keyword | | entry | string | Yes | Entry point file | | runtime | string | No | persistent or on_demand | | icon | object | No | Extension icon | | view_type | string | No | list (default) | | permissions | object | No | Required permissions | | config_schema | object | No | User configuration options |