Claude Code Plugins

Community-maintained marketplace

Feedback

create-plugin

@Stewart86/hamr
203
0

Create Hamr launcher plugins with proper JSON protocol, testing, and manifest structure

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name create-plugin
description Create Hamr launcher plugins with proper JSON protocol, testing, and manifest structure
license MIT
compatibility opencode
metadata [object Object]

Creating Hamr Plugins

This skill helps you create plugins for the Hamr launcher. Plugins communicate via JSON over stdin/stdout.

Reference Documentation

For complete protocol details, see:

  • plugins/README.md - Full JSON protocol reference, response types, and examples
  • plugins/AGENTS.md - Condensed reference for AI agents

Plugin Structure

Simple Action (Script)

Single executable file in ~/.config/hamr/plugins/:

#!/bin/bash
notify-send "Hello!"

Multi-Step Workflow (Folder)

my-plugin/
├── manifest.json   # Plugin metadata
├── handler.py      # Main handler (executable)
└── test.sh         # Test script

manifest.json

{
  "name": "My Plugin",
  "description": "What it does",
  "icon": "material_icon_name",
  "trigger": "/myplugin"
}

Optional fields:

  • "handler": "handler.js" - Custom handler filename
  • "poll": 2000 - Auto-refresh interval in ms
  • "index": {"enabled": true} - Enable launcher indexing

Handler Template (Python)

#!/usr/bin/env python3
import json
import os
import sys

TEST_MODE = os.environ.get("HAMR_TEST_MODE") == "1"

def main():
    input_data = json.load(sys.stdin)
    step = input_data.get("step", "initial")
    query = input_data.get("query", "").strip()
    selected = input_data.get("selected", {})
    action = input_data.get("action", "")
    context = input_data.get("context", "")

    if step == "initial":
        print(json.dumps({
            "type": "results",
            "results": [
                {"id": "item1", "name": "First Item", "icon": "star"},
            ],
            "placeholder": "Search..."
        }))
        return

    if step == "search":
        # Filter based on query
        print(json.dumps({
            "type": "results",
            "results": [...],
            "inputMode": "realtime"
        }))
        return

    if step == "action":
        item_id = selected.get("id", "")
        
        if item_id == "__back__":
            # Handle back navigation
            print(json.dumps({
                "type": "results",
                "results": get_initial_results(),
                "clearInput": True
            }))
            return

        # Execute action
        print(json.dumps({
            "type": "execute",
            "execute": {
                "command": ["notify-send", f"Selected: {item_id}"],
                "close": True
            }
        }))

if __name__ == "__main__":
    main()

JSON Protocol

Input (stdin)

{
    "step": "initial|search|action|index|poll",
    "query": "user text",
    "selected": {"id": "item-id"},
    "action": "action-button-id",
    "context": "your-state",
    "session": "session-id",
    "replay": true
}

Response Types

1. results - Show list:

{
    "type": "results",
    "results": [
        {
            "id": "unique-id",
            "name": "Display Name",
            "description": "Subtitle",
            "icon": "material_icon",
            "actions": [{"id": "copy", "name": "Copy", "icon": "content_copy"}]
        }
    ],
    "inputMode": "realtime",
    "placeholder": "Search...",
    "pluginActions": [{"id": "add", "name": "Add", "icon": "add_circle"}]
}

2. execute - Run command:

{
    "type": "execute",
    "execute": {
        "command": ["xdg-open", "/path"],
        "notify": "Done",
        "close": True
    }
}

3. card - Rich content:

{
    "type": "card",
    "card": {"content": "**Markdown**", "markdown": True}
}

4. imageBrowser - Image grid:

{
    "type": "imageBrowser",
    "imageBrowser": {
        "directory": "~/Pictures",
        "title": "Select Image",
        "actions": [{"id": "set", "name": "Set", "icon": "check"}]
    }
}

5. error - Show error:

{"type": "error", "message": "Something went wrong"}

Input Modes

Mode Behavior
realtime Every keystroke triggers search
submit Only Enter triggers search

Testing

Use the test-harness:

export HAMR_TEST_MODE=1
./plugins/test-harness ./plugins/my-plugin/handler.py initial
./plugins/test-harness ./plugins/my-plugin/handler.py search --query "test"
./plugins/test-harness ./plugins/my-plugin/handler.py action --id "item1"

Test Script Template

#!/bin/bash
export HAMR_TEST_MODE=1
source "$(dirname "$0")/../test-helpers.sh"

TEST_NAME="My Plugin Tests"
HANDLER="$(dirname "$0")/handler.py"

test_initial() {
    local result=$(hamr_test initial)
    assert_type "$result" "results"
}

run_tests test_initial

Mock Data Pattern

Always check HAMR_TEST_MODE for testing:

TEST_MODE = os.environ.get("HAMR_TEST_MODE") == "1"

def get_data():
    if TEST_MODE:
        return [{"id": "mock", "name": "Mock Item"}]
    return fetch_real_data()

Plugin Indexing

Enable searchable items in main launcher:

{
  "index": {
    "enabled": true,
    "watchFiles": ["~/.config/my-data.json"]
  }
}

Handle step == "index":

if step == "index":
    print(json.dumps({
        "type": "index",
        "items": [
            {
                "id": "item1",
                "name": "Searchable Item",
                "keywords": ["alias"],
                "execute": {"command": ["xdg-open", "url"]}
            }
        ]
    }))

Common Patterns

Plugin Actions (Toolbar)

"pluginActions": [
    {"id": "add", "name": "Add", "icon": "add_circle"},
    {"id": "wipe", "name": "Wipe", "icon": "delete_sweep", "confirm": "Are you sure?"}
]

Handle with selected.get("id") == "__plugin__".

Context for State

# Set context
{"type": "results", "context": "__edit__:item1", "inputMode": "submit"}

# Read context in search
if context.startswith("__edit__:"):
    item_id = context.split(":")[1]

History Tracking

{
    "type": "execute",
    "execute": {
        "command": ["xdg-open", url],
        "name": "Open Example",  # Required for history
        "icon": "link",
        "close": True
    }
}

Icon Types

  • Material (default): star, folder, content_copy
  • System: Set "iconType": "system" for desktop app icons

Development Workflow

  1. Create manifest.json with name, description, icon
  2. Create handler.py with shebang, make executable
  3. Test with HAMR_TEST_MODE=1 ./plugins/test-harness
  4. Add mock data for test mode
  5. Create test.sh for CI
  6. Plugin auto-loads on file save