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:
my-extension/
├── manifest.json # Extension metadata
├── main.py # Entry point
└── icons/ # Optional custom icons
└── icon.svg
manifest.json
Every extension needs a manifest:
{
"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:
#!/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:
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:
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:
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:
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:
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")
### 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:
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:
# 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
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
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:
- Place your extension folder in
~/.summon/extensions/ - Open Summon and type your extension's keyword
- 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=Falsefor 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 |