feat: implement Trello authentication & connection plugin
Add TrelloClient for Trello REST API interaction with env-var-based credential management (TRELLO_API_KEY, TRELLO_TOKEN). Implements: - trello_verify_credentials — verify API key/token against Trello API - trello_list_boards — list accessible Trello boards - trello_disconnect — clear in-memory credentials Includes full test suite (18 tests), spec document, and plugin metadata. Issue: #1
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
"""
|
||||
trello-plugin — Hermes Agent plugin for Trello board integration.
|
||||
"""
|
||||
|
||||
from trello_plugin.tools import (
|
||||
PLUGIN_DESCRIPTION,
|
||||
PLUGIN_NAME,
|
||||
PLUGIN_TOOLS,
|
||||
PLUGIN_VERSION,
|
||||
check_requirements,
|
||||
trello_disconnect,
|
||||
trello_list_boards,
|
||||
trello_verify_credentials,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"PLUGIN_NAME",
|
||||
"PLUGIN_DESCRIPTION",
|
||||
"PLUGIN_VERSION",
|
||||
"PLUGIN_TOOLS",
|
||||
"check_requirements",
|
||||
"trello_verify_credentials",
|
||||
"trello_list_boards",
|
||||
"trello_disconnect",
|
||||
]
|
||||
@@ -0,0 +1,175 @@
|
||||
"""
|
||||
Trello Plugin — Hermes Agent plugin for Trello board integration.
|
||||
|
||||
Authentication & Connection module.
|
||||
Provides the TrelloClient for interacting with the Trello REST API.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from typing import Any
|
||||
|
||||
import requests
|
||||
|
||||
TRELLO_API_BASE = "https://api.trello.com/1"
|
||||
|
||||
|
||||
class TrelloClient:
|
||||
"""Client for the Trello REST API.
|
||||
|
||||
Reads credentials from environment variables TRELLO_API_KEY and TRELLO_TOKEN
|
||||
unless explicitly passed. All public methods return plain dicts suitable for
|
||||
JSON serialization by Hermes tool handlers.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
api_key: str | None = None,
|
||||
token: str | None = None,
|
||||
session: requests.Session | None = None,
|
||||
) -> None:
|
||||
self.api_key = api_key or os.environ.get("TRELLO_API_KEY", "")
|
||||
self.token = token or os.environ.get("TRELLO_TOKEN", "")
|
||||
self._session = session or requests.Session()
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Internal helpers
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _auth_params(self) -> dict[str, str]:
|
||||
"""Return query parameters common to every Trello API call."""
|
||||
return {"key": self.api_key, "token": self.token}
|
||||
|
||||
def _get(self, path: str, params: dict[str, Any] | None = None) -> dict[str, Any]:
|
||||
"""Make an authenticated GET request and return the JSON response."""
|
||||
if not self.api_key or not self.token:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "TRELLO_API_KEY and TRELLO_TOKEN must both be set as environment variables.",
|
||||
}
|
||||
|
||||
url = f"{TRELLO_API_BASE}{path}"
|
||||
merged_params = self._auth_params()
|
||||
if params:
|
||||
merged_params.update(params)
|
||||
|
||||
try:
|
||||
resp = self._session.get(url, params=merged_params, timeout=15)
|
||||
resp.raise_for_status()
|
||||
data: dict[str, Any] = resp.json()
|
||||
return {"success": True, "data": data}
|
||||
except requests.exceptions.HTTPError as exc:
|
||||
status = exc.response.status_code if exc.response is not None else 0
|
||||
if status == 401:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Trello authentication failed (401). Check your TRELLO_API_KEY and TRELLO_TOKEN.",
|
||||
}
|
||||
if status == 403:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Trello access denied (403). Your token may not have the required scopes.",
|
||||
}
|
||||
if status == 429:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Trello rate limit exceeded. Try again later.",
|
||||
}
|
||||
return {
|
||||
"success": False,
|
||||
"message": f"Trello API error ({status}): {exc}",
|
||||
}
|
||||
except requests.exceptions.ConnectionError:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Could not connect to Trello API. Check your network connection.",
|
||||
}
|
||||
except requests.exceptions.Timeout:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Trello API request timed out. Try again later.",
|
||||
}
|
||||
except requests.exceptions.RequestException as exc:
|
||||
return {
|
||||
"success": False,
|
||||
"message": f"Trello API request failed: {exc}",
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Public API — Auth & Connection
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def verify_credentials(self) -> dict[str, Any]:
|
||||
"""Verify that the stored credentials are valid by fetching member info.
|
||||
|
||||
Returns:
|
||||
dict with ``success`` bool and ``message`` string. On success the
|
||||
``member`` key contains the Trello member id and username.
|
||||
"""
|
||||
result = self._get("/members/me")
|
||||
if result["success"]:
|
||||
member = result["data"]
|
||||
return {
|
||||
"success": True,
|
||||
"message": (
|
||||
f"✓ Trello credentials verified successfully "
|
||||
f"(authenticated as {member.get('username', 'unknown')})"
|
||||
),
|
||||
"member": {
|
||||
"id": member.get("id"),
|
||||
"username": member.get("username"),
|
||||
"full_name": member.get("fullName"),
|
||||
},
|
||||
}
|
||||
return {
|
||||
"success": False,
|
||||
"message": result.get("message", "Trello credential verification failed."),
|
||||
}
|
||||
|
||||
def list_boards(self) -> dict[str, Any]:
|
||||
"""Fetch all boards accessible to the authenticated user.
|
||||
|
||||
Returns:
|
||||
dict with ``success`` bool and a ``boards`` list (each containing
|
||||
``id``, ``name``, ``url``, ``closed``, ``starred``).
|
||||
"""
|
||||
result = self._get("/members/me/boards", params={"fields": "id,name,url,closed,starred"})
|
||||
if not result["success"]:
|
||||
return {
|
||||
"success": False,
|
||||
"message": result.get("message", "Failed to list Trello boards."),
|
||||
}
|
||||
|
||||
boards = [
|
||||
{
|
||||
"id": b.get("id"),
|
||||
"name": b.get("name"),
|
||||
"url": b.get("url"),
|
||||
"closed": b.get("closed", False),
|
||||
"starred": b.get("starred", False),
|
||||
}
|
||||
for b in result["data"]
|
||||
]
|
||||
return {"success": True, "boards": boards}
|
||||
|
||||
def disconnect(self) -> dict[str, Any]:
|
||||
"""Clear credentials from the in-memory client.
|
||||
|
||||
Note: this does NOT revoke the Trello token. The user must invalidate
|
||||
it via Trello's account settings if they want full revocation.
|
||||
"""
|
||||
self.api_key = ""
|
||||
self.token = ""
|
||||
return {
|
||||
"success": True,
|
||||
"message": (
|
||||
"Trello credentials cleared. "
|
||||
"Set TRELLO_API_KEY and TRELLO_TOKEN again to reconnect."
|
||||
),
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def check_requirements() -> bool:
|
||||
"""Check if the required environment variables are set."""
|
||||
return bool(os.environ.get("TRELLO_API_KEY")) and bool(os.environ.get("TRELLO_TOKEN"))
|
||||
@@ -0,0 +1,96 @@
|
||||
"""
|
||||
Trello Plugin — Hermes Agent tool definitions.
|
||||
|
||||
Each function decorated with ``@tool`` registers itself as a Hermes tool
|
||||
that the agent can invoke during a conversation.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
from typing import Any
|
||||
|
||||
from trello_plugin.client import TrelloClient
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Module-level client (lazily initialised so env vars can be overridden)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_client: TrelloClient | None = None
|
||||
|
||||
|
||||
def _get_client() -> TrelloClient:
|
||||
global _client # noqa: PLW0603
|
||||
api_key = os.environ.get("TRELLO_API_KEY", "")
|
||||
token = os.environ.get("TRELLO_TOKEN", "")
|
||||
if _client is None or _client.api_key != api_key or _client.token != token:
|
||||
_client = TrelloClient(api_key=api_key, token=token)
|
||||
return _client
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tool helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _respond(data: dict[str, Any]) -> str:
|
||||
"""Wrap a result dict in a JSON string, the Hermes tool contract."""
|
||||
return json.dumps(data, indent=2)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tools
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def trello_verify_credentials() -> str:
|
||||
"""Verify that the Trello API key and token are valid.
|
||||
|
||||
Checks the ``TRELLO_API_KEY`` and ``TRELLO_TOKEN`` environment variables
|
||||
and attempts a test call to the Trello API. Returns the authenticated
|
||||
user's Trello username on success.
|
||||
"""
|
||||
client = _get_client()
|
||||
result = client.verify_credentials()
|
||||
return _respond(result)
|
||||
|
||||
|
||||
def trello_list_boards() -> str:
|
||||
"""List all Trello boards accessible to the authenticated user.
|
||||
|
||||
Requires ``TRELLO_API_KEY`` and ``TRELLO_TOKEN`` to be set.
|
||||
Returns board id, name, URL, and whether the board is closed/starred.
|
||||
"""
|
||||
client = _get_client()
|
||||
result = client.list_boards()
|
||||
return _respond(result)
|
||||
|
||||
|
||||
def trello_disconnect() -> str:
|
||||
"""Disconnect from Trello by clearing credentials from memory.
|
||||
|
||||
This does NOT revoke the Trello token — invalidate it via
|
||||
Trello's account settings if full revocation is desired.
|
||||
"""
|
||||
global _client # noqa: PLW0603
|
||||
_client = None
|
||||
return _respond({"success": True, "message": "Trello credentials cleared. Reconnect by calling trello_verify_credentials after setting TRELLO_API_KEY and TRELLO_TOKEN."})
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Plugin metadata (used by Hermes plugin loader)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
PLUGIN_NAME = "trello-plugin"
|
||||
PLUGIN_DESCRIPTION = "Hermes Agent plugin for Trello board integration"
|
||||
PLUGIN_VERSION = "0.1.0"
|
||||
PLUGIN_TOOLS = [
|
||||
trello_verify_credentials,
|
||||
trello_list_boards,
|
||||
trello_disconnect,
|
||||
]
|
||||
PLUGIN_REQUIRES_ENV = ["TRELLO_API_KEY", "TRELLO_TOKEN"]
|
||||
|
||||
|
||||
def check_requirements() -> bool:
|
||||
"""Return True when both Trello env vars are present."""
|
||||
return TrelloClient.check_requirements()
|
||||
Reference in New Issue
Block a user