Skill: drgeld/echodecks (version 1.0.2)

# EchoDecks Skill (v1) Integrates with EchoDecks for flashcard management, study sessions, and AI generation. ## Configuration This skill requires the `ECHODECKS_API_KEY` environment variable to be set. ## Tools ### `echodecks_get_decks` List all available decks or details for a specific deck. - `id` (optional): The ID of a specific deck to retrieve. ### `echodecks_get_due_cards` Retrieve cards that are currently due for review. - `deck_id` (optional): Filter due cards by a specific deck ID. ### `echodecks_submit_review` Submit a spaced repetition review for a card. - `card_id` (required): The ID of the card being reviewed. - `quality` (required): Integer rating (0-3). - 0: Again (Failure/Forgot) - 1: Hard - 2: Good - 3: Easy ### `echodecks_generate_cards` Generate new flashcards from a topic or text content using AI. - `deck_id` (required): The target deck ID for the new cards. - `topic` (optional): A short topic string to generate from. - `text` (optional): Raw text content to generate from. **Note:** One of `topic` or `text` must be provided. Cost: 10 credits. ### `echodecks_generate_podcast` Generate an audio podcast summary or conversation from a deck. - `deck_id` (required): The source deck ID. - `voice` (optional): Voice preference (default: "neutral"). - `type` (optional): "summary" or "conversation" (default: "summary"). **Note:** Cost: 50 credits. ### `echodecks_get_podcasts` Retrieve existing podcasts for a deck. - `deck_id` (optional): Filter by deck ID. - `id` (optional): specific podcast ID. ### `echodecks_get_user_stats` Get current user profile and study statistics. ## Implementation Details All tools are wrappers around `skills/echodecks-v1/echodecks_client.py`. ```bash # Example ./skills/echodecks-v1/echodecks_client.py get-due --deck-id 123 ```

path: echodecks_client.py
content: #!/usr/bin/env python3
import os
import sys
import json
import argparse
import requests

# Base Configuration
BASE_URL = "https://echodecks.com/api/apps/68bc0769be6e58e1c8385b2b/functions/externalApi"

def get_api_key():
    [REDACTED_SECRET]"ECHODECKS_API_KEY")
    if not [REDACTED_SECRET]"error": "Missing ECHODECKS_API_KEY environment variable."}))
        sys.exit(1)
    return api_key

def make_request(method, resource, action=None, params=None, data=None):
    headers = {
        "X-API-KEY": get_api_key(),
        "Content-Type": "application/json"
    }
    
    url = f"{BASE_URL}?resource={resource}"
    if action:
        url += f"&action={action}"
    
    # Merge extra params into URL if provided (e.g. deck_id for GET requests)
    if params:
        for key, value in params.items():
            if value is not None:
                url += f"&{key}={value}"

    try:
        if method == "GET":
            response = requests.get(url, headers=headers)
        elif method == "POST":
            response = requests.post(url, headers=headers, json=data)
        elif method == "PUT":
            response = requests.put(url, headers=headers, json=data)
        elif method == "DELETE":
            response = requests.delete(url, headers=headers)
        else:
            return {"error": f"Unsupported method: {method}"}
            
        response.raise_for_status()
        
        # Try parsing JSON, fallback to text if empty or not JSON
        try:
            return response.json()
        except json.JSONDecodeError:
            return {"status": "success", "message": response.text}
            
    except requests.exceptions.RequestException as e:
        error_msg = str(e)
        if hasattr(e.response, 'text') and e.response.text:
             try:
                 api_err = json.loads(e.response.text)
                 error_msg = api_err.get('message', error_msg)
             except:
                 error_msg = e.response.text
        return {"error": error_msg}

# --- Command Handlers ---

def handle_list_decks(args):
    return make_request("GET", "decks", params={"id": args.id})

def handle_create_deck(args):
    return make_request("POST", "decks", data={"title": args.title, "description": args.description})

def handle_list_cards(args):
    return make_request("GET", "cards", params={"deck_id": args.deck_id, "id": args.card_id})

def handle_get_due_cards(args):
    return make_request("GET", "study", action="due", params={"deck_id": args.deck_id})

def handle_submit_review(args):
    return make_request("POST", "study", action="submit", data={
        "card_id": args.card_id,
        "quality": args.quality
    })

def handle_generate_cards(args):
    data = {"deck_id": args.deck_id}
    if args.topic:
        data["topic"] = args.topic
    if args.text:
        data["text"] = args.text
    return make_request("POST", "generate", action="cards", data=data)

def handle_generate_podcast(args):
    return make_request("POST", "podcasts", action="generate", data={
        "deck_id": args.deck_id,
        "style": args.style
    })

def handle_list_podcasts(args):
    return make_request("GET", "podcasts", params={"deck_id": args.deck_id, "id": args.id})

def handle_get_user(args):
    return make_request("GET", "user")

def handle_get_podcast_status(args):
    return make_request("GET", "podcasts", action="status", params={"id": args.id})

# --- Main CLI ---

def main():
    parser = argparse.ArgumentParser(description="EchoDecks API Client")
    subparsers = parser.add_subparsers(dest="command", required=True)
    # ... (existing subparsers) ...
    p_pod_status = subparsers.add_parser("podcast-status", help="Check podcast status")
    p_pod_status.add_argument("--id", required=True)

    # Decks
    p_decks = subparsers.add_parser("list-decks", help="List decks or get a specific deck")
    p_decks.add_argument("--id", help="Specific Deck ID")
    
    p_create_deck = subparsers.add_parser("create-deck", help="Create a new deck")
    p_create_deck.add_argument("--title", required=True)
    p_create_deck.add_argument("--description")

    # Cards
    p_cards = subparsers.add_parser("list-cards", help="List cards in a deck")
    p_cards.add_argument("--deck-id", required=True)
    p_cards.add_argument("--card-id", help="Specific Card ID")

    # Study
    p_due = subparsers.add_parser("get-due", help="Get due cards")
    p_due.add_argument("--deck-id", help="Filter by Deck ID")

    p_review = subparsers.add_parser("submit-review", help="Submit a card review")
    p_review.add_argument("--card-id", required=True)
    p_review.add_argument("--quality", type=int, required=True, choices=[0, 1, 2, 3], help="0=Again, 1=Hard, 2=Good, 3=Easy")

    # Generate
    p_gen_cards = subparsers.add_parser("generate-cards", help="AI Generate cards")
    p_gen_cards.add_argument("--topic")
    p_gen_cards.add_argument("--text")
    p_gen_cards.add_argument("--deck-id", required=True)

    p_gen_pod = subparsers.add_parser("generate-podcast", help="AI Generate podcast")
    p_gen_pod.add_argument("--deck-id", required=True)
    p_gen_pod.add_argument("--style", default="summary", help="Podcast style (e.g., summary, conversation)")

    # Podcasts
    p_pods = subparsers.add_parser("list-podcasts", help="List podcasts")
    p_pods.add_argument("--deck-id")
    p_pods.add_argument("--id")

    # User
    subparsers.add_parser("get-user", help="Get user info")

    args = parser.parse_args()

    commands = {
        "list-decks": handle_list_decks,
        "create-deck": handle_create_deck,
        "list-cards": handle_list_cards,
        "get-due": handle_get_due_cards,
        "submit-review": handle_submit_review,
        "generate-cards": handle_generate_cards,
        "generate-podcast": handle_generate_podcast,
        "list-podcasts": handle_list_podcasts,
        "podcast-status": handle_get_podcast_status,
        "get-user": handle_get_user
    }

    if args.command in commands:
        result = commands[args.command](args)
        print(json.dumps(result, indent=2))
    else:
        print(json.dumps({"error": "Unknown command"}))

if __name__ == "__main__":
    main()


path: test_echodecks_client.py
content: import unittest
from unittest.mock import patch, MagicMock
import json
import sys
import os

# Add current directory to path so we can import the client
sys.path.append(os.path.dirname(os.path.abspath(__file__)))

import echodecks_client

class TestEchoDecksClient(unittest.TestCase):

    def setUp(self):
        # Mock environment variable
        self.env_patcher = patch.dict(os.environ, {"ECHODECKS_API_KEY": "test_key"})
        self.env_patcher.start()

    def tearDown(self):
        self.env_patcher.stop()

    @patch('requests.get')
    def test_list_decks(self, mock_get):
        # Setup mock response
        mock_response = MagicMock()
        mock_response.json.return_value = {"decks": [{"id": "1", "title": "Test Deck"}]}
        mock_response.raise_for_status.return_value = None
        mock_get.return_value = mock_response

        # Call the handler directly
        args = MagicMock()
        args.id = None
        result = echodecks_client.handle_list_decks(args)

        # Assertions
        self.assertEqual(result, {"decks": [{"id": "1", "title": "Test Deck"}]})
        mock_get.assert_called_with(
            "https://echodecks.com/functions/externalApi?resource=decks",
            headers={"X-API-KEY": "test_key", "Content-Type": "application/json"}
        )

    @patch('requests.post')
    def test_submit_review(self, mock_post):
        mock_response = MagicMock()
        mock_response.json.return_value = {"success": True}
        mock_response.raise_for_status.return_value = None
        mock_post.return_value = mock_response

        args = MagicMock()
        args.card_id = "card_123"
        args.quality = 3
        
        result = echodecks_client.handle_submit_review(args)

        self.assertEqual(result, {"success": True})
        mock_post.assert_called_with(
            "https://echodecks.com/functions/externalApi?resource=study&action=submit",
            headers={"X-API-KEY": "test_key", "Content-Type": "application/json"},
            json={"cardId": "card_123", "quality": 3}
        )

    @patch('requests.post')
    def test_generate_podcast(self, mock_post):
        mock_response = MagicMock()
        mock_response.json.return_value = {"jobId": "job_999"}
        mock_post.return_value = mock_response

        args = MagicMock()
        args.deck_id = "deck_55"
        args.voice = "nova"
        args.type = "summary"

        result = echodecks_client.handle_generate_podcast(args)
        
        mock_post.assert_called_with(
            "https://echodecks.com/functions/externalApi?resource=podcasts&action=generate",
            headers={"X-API-KEY": "test_key", "Content-Type": "application/json"},
            json={"deckId": "deck_55", "voice": "nova", "type": "summary"}
        )
        self.assertEqual(result, {"jobId": "job_999"})

if __name__ == '__main__':
    unittest.main()


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: clean (LOW)