Source code for tools.gitea_tools

"""Gitea API tools using per-user access tokens.

Provides repository, issue, PR, commit, file, notification, and star
operations via the Gitea REST API. Supports custom base URLs for
self-hosted instances. Requires set_user_api_key service=gitea.
"""

from __future__ import annotations

import base64
import jsonutil as json
import logging
from typing import Any, TYPE_CHECKING
from urllib.parse import quote

import aiohttp

if TYPE_CHECKING:
    from tool_context import ToolContext

logger = logging.getLogger(__name__)


async def _gitea_request(
    method: str,
    path: str,
    token: str,
    base_url: str,
    *,
    params: dict[str, Any] | None = None,
    json_body: dict[str, Any] | None = None,
) -> dict[str, Any] | list[Any] | str:
    """Perform a single authenticated HTTP request against the Gitea REST API.

    The shared transport behind every Gitea tool handler in this module: it
    builds the ``/api/v1`` URL from *base_url* (which must NOT already include
    the ``/api/v1`` suffix), attaches the per-user ``token`` as a Gitea
    ``Authorization`` header, and issues *method* via a freshly opened
    ``aiohttp.ClientSession``. Decoding is normalised so callers get a uniform
    shape: a ``204 No Content`` becomes a small success dict, any ``>= 400``
    status becomes an error dict with a truncated ``detail`` body, an empty
    body becomes ``{}``, valid JSON is parsed, and anything else is returned as
    a truncated raw string. This performs outbound network I/O over HTTP but
    touches no Redis, no knowledge graph, and no event bus.

    Called by every ``gitea_*`` handler in this module (and by the private
    helpers they delegate to); it is not exposed as a tool itself.

    Args:
        method: HTTP verb such as ``GET``, ``POST``, ``PATCH``, or ``DELETE``.
        path: API path beginning with a slash, appended after ``/api/v1``.
        token: Per-user Gitea access token sent in the ``Authorization`` header.
        base_url: Instance base URL without the ``/api/v1`` suffix; a trailing
            slash is stripped.
        params: Optional query-string parameters.
        json_body: Optional JSON request body.

    Returns:
        The decoded JSON object or list on success, a success/error dict for
        no-content or failing responses, or a truncated raw string when the
        body is not JSON.
    """
    base = base_url.rstrip("/")
    url = f"{base}/api/v1{path}"
    headers = {
        "Authorization": f"token {token}",
        "Accept": "application/json",
    }
    async with aiohttp.ClientSession() as session:
        async with session.request(
            method, url, headers=headers, params=params, json=json_body
        ) as resp:
            if resp.status == 204:
                return {"status": "success", "code": 204}
            body = await resp.text()
            if resp.status >= 400:
                return {
                    "error": f"Gitea API error ({resp.status})",
                    "detail": body[:2000],
                }
            if not body:
                return {}
            try:
                return json.loads(body)
            except json.JSONDecodeError:
                return body[:4000]


async def _get_credentials(ctx: ToolContext | None) -> tuple[str, str]:
    """Resolve the calling user's Gitea access token and instance base URL.

    The credential gate in front of every Gitea handler: it pulls the
    ``user_id``, ``redis``, ``channel_id``, and ``config`` off the supplied
    ``ToolContext`` and delegates to ``get_gitea_credentials`` in
    ``tools.manage_api_keys``, which looks up the per-user token (set via the
    ``set_user_api_key`` flow with ``service=gitea``) in Redis and falls back to
    a shared pool token when the user has none. When nothing is configured it
    raises with the human-readable message from ``missing_api_key_error`` so the
    LLM sees actionable guidance rather than an opaque failure. Reads from Redis
    through the credential lookup but performs no writes and no other I/O.

    Called by every ``gitea_*`` tool handler in this module as their first step.

    Args:
        ctx: The ``ToolContext`` for the invocation, or ``None``; its
            ``user_id``, ``redis``, ``channel_id``, and ``config`` attributes
            are read defensively via ``getattr``.

    Returns:
        A ``(token, base_url)`` tuple suitable for passing to
        ``_gitea_request``.

    Raises:
        RuntimeError: If no Gitea credentials are configured for the user and
            no pool fallback is available.
    """
    from tools.manage_api_keys import get_gitea_credentials, missing_api_key_error

    user_id = getattr(ctx, "user_id", None) if ctx else None
    creds = await get_gitea_credentials(
        user_id or "",
        redis_client=getattr(ctx, "redis", None) if ctx else None,
        channel_id=getattr(ctx, "channel_id", None) if ctx else None,
        fallback_to_pool=True,
        config=getattr(ctx, "config", None) if ctx else None,
    )
    if not creds:
        raise RuntimeError(missing_api_key_error("gitea"))
    return creds


def _fmt_repo(r: dict) -> dict:
    """Condense a raw Gitea repository object into a compact summary dict.

    Projects the verbose repository payload returned by the Gitea REST API
    down to the handful of fields the LLM actually needs (full name,
    description, visibility, fork flag, star/fork counts, default branch,
    HTML URL, last-updated timestamp). This keeps tool responses small and
    token-efficient. Pure data transformation with no I/O or side effects;
    it simply reads keys off the supplied mapping via ``dict.get`` so missing
    keys degrade to ``None`` rather than raising.

    This is a private formatting helper used by the repository-returning tool
    handlers in this module: ``gitea_list_repos``, ``gitea_search_repos``,
    ``gitea_get_repo``, ``gitea_create_repo``, ``gitea_create_repo_from_template``,
    and ``gitea_fork_repo`` all pass each repo dict through here before
    serialising. The same helper name also exists independently in
    ``tools/github_tools.py``; the two are unrelated copies.

    Args:
        r (dict): A single repository object as decoded from a Gitea API
            response. Any missing keys are tolerated.

    Returns:
        dict: A flat summary containing ``full_name``, ``description``,
        ``private``, ``fork``, ``stars_count``, ``forks_count``,
        ``default_branch``, ``html_url``, and ``updated_at``.
    """
    return {
        "full_name": r.get("full_name"),
        "description": r.get("description"),
        "private": r.get("private"),
        "fork": r.get("fork"),
        "stars_count": r.get("stars_count"),
        "forks_count": r.get("forks_count"),
        "default_branch": r.get("default_branch"),
        "html_url": r.get("html_url"),
        "updated_at": r.get("updated_at"),
    }


def _fmt_issue(i: dict) -> dict:
    """Condense a raw Gitea issue object into a compact summary dict.

    Projects the verbose issue payload from the Gitea REST API down to the
    fields useful to the LLM: number, title, state, author login, label
    names, timestamps, HTML URL, and a body excerpt. The nested ``user``
    object is flattened to its ``login`` (guarding against non-dict values),
    the ``labels`` list is reduced to label names, and the body is truncated
    to the first 1000 characters to bound response size. Pure transformation
    with no I/O or side effects.

    This is a private formatting helper used by the issue-returning handlers
    ``gitea_list_issues``, ``gitea_get_issue``, ``gitea_create_issue``, and
    ``gitea_update_issue`` before they serialise their results to JSON. An
    unrelated helper of the same name also exists in ``tools/github_tools.py``.

    Args:
        i (dict): A single issue object as decoded from a Gitea API response.
            Missing keys are tolerated and produce ``None`` or empty values.

    Returns:
        dict: A flat summary with ``number``, ``title``, ``state``, ``user``,
        ``labels``, ``created_at``, ``updated_at``, ``html_url``, and a
        body excerpt under ``body``.
    """
    user = i.get("user") or {}
    return {
        "number": i.get("number"),
        "title": i.get("title"),
        "state": i.get("state"),
        "user": user.get("login") if isinstance(user, dict) else None,
        "labels": [l.get("name") for l in i.get("labels", []) if isinstance(l, dict)],
        "created_at": i.get("created_at"),
        "updated_at": i.get("updated_at"),
        "html_url": i.get("html_url"),
        "body": (i.get("body") or "")[:1000],
    }


def _fmt_pr(p: dict) -> dict:
    """Condense a raw Gitea pull-request object into a compact summary dict.

    Projects the verbose pull-request payload from the Gitea REST API down to
    the fields useful to the LLM: number, title, state, author login, draft
    and merged flags, timestamps, HTML URL, and diff statistics (additions,
    deletions, changed files). The nested ``user`` object is flattened to its
    ``login`` while guarding against non-dict values. Pure transformation with
    no I/O or side effects.

    This is a private formatting helper used by the PR-returning handlers
    ``gitea_list_pull_requests``, ``gitea_get_pull_request``, and
    ``gitea_create_pull_request`` before they serialise to JSON
    (``gitea_get_pull_request`` additionally attaches a truncated body on top
    of this summary). An unrelated helper of the same name also exists in
    ``tools/github_tools.py``.

    Args:
        p (dict): A single pull-request object as decoded from a Gitea API
            response. Missing keys are tolerated and produce ``None``.

    Returns:
        dict: A flat summary with ``number``, ``title``, ``state``, ``user``,
        ``draft``, ``merged``, ``created_at``, ``updated_at``, ``html_url``,
        ``additions``, ``deletions``, and ``changed_files``.
    """
    user = p.get("user") or {}
    return {
        "number": p.get("number"),
        "title": p.get("title"),
        "state": p.get("state"),
        "user": user.get("login") if isinstance(user, dict) else None,
        "draft": p.get("draft"),
        "merged": p.get("merged"),
        "created_at": p.get("created_at"),
        "updated_at": p.get("updated_at"),
        "html_url": p.get("html_url"),
        "additions": p.get("additions"),
        "deletions": p.get("deletions"),
        "changed_files": p.get("changed_files"),
    }


# ---------------------------------------------------------------------------
# Tool handlers
# ---------------------------------------------------------------------------


[docs] async def gitea_list_repos( page: int = 1, limit: int = 20, ctx: ToolContext | None = None, ) -> str: """List the authenticated user's own Gitea repositories. Backs the ``gitea_list_repos`` tool. Resolves the caller's token via ``_get_credentials`` and pages the ``GET /user/repos`` endpoint through ``_gitea_request``, condensing each repository through ``_fmt_repo`` so the LLM receives a compact, token-efficient listing rather than the full Gitea payloads. The ``limit`` is clamped to 50 to match the instance ceiling. Performs HTTP and a Redis-backed credential read only; no writes. Registered as the ``gitea_list_repos`` handler in this module's ``TOOLS`` list and dispatched by name from the inference worker's tool loop; not called directly elsewhere in the repo. Args: page: 1-based page number to fetch. limit: Results per page; values above 50 are clamped to 50. ctx: The active ``ToolContext`` supplying user credentials. Returns: A JSON string with a ``count`` and a ``repos`` array of compact repository summaries, or a JSON error object on API failure. """ token, base_url = await _get_credentials(ctx) params = {"page": page, "limit": min(limit, 50)} data = await _gitea_request("GET", "/user/repos", token, base_url, params=params) if isinstance(data, dict) and "error" in data: return json.dumps(data) repos = [_fmt_repo(r) for r in data] if isinstance(data, list) else [] return json.dumps({"count": len(repos), "repos": repos})
[docs] async def gitea_search_repos( q: str, sort: str = "updated", order: str = "desc", page: int = 1, limit: int = 20, ctx: ToolContext | None = None, ) -> str: """Search public and accessible Gitea repositories by keyword. Backs the ``gitea_search_repos`` tool. Resolves credentials via ``_get_credentials`` and queries ``GET /repos/search`` through ``_gitea_request`` with the supplied keyword, sort field, direction, and paging, then condenses each hit through ``_fmt_repo``. Unlike the raw endpoint this unwraps the Gitea ``data`` envelope before formatting. Performs HTTP and a Redis-backed credential read only; no writes. Registered as the ``gitea_search_repos`` handler in this module's ``TOOLS`` list and dispatched by name from the inference worker's tool loop; not called directly elsewhere in the repo. Args: q: Free-text search keyword. sort: Sort field (e.g. ``updated``, ``created``, ``stars``). order: Sort direction, ``asc`` or ``desc``. page: 1-based page number to fetch. limit: Results per page; values above 50 are clamped to 50. ctx: The active ``ToolContext`` supplying user credentials. Returns: A JSON string with a ``count`` and a ``repos`` array of compact repository summaries, or a JSON error object on API failure. """ token, base_url = await _get_credentials(ctx) params = { "q": q, "sort": sort, "order": order, "page": page, "limit": min(limit, 50), } data = await _gitea_request("GET", "/repos/search", token, base_url, params=params) if isinstance(data, dict) and "error" in data: return json.dumps(data) items = data.get("data", []) if isinstance(data, dict) else [] repos = [_fmt_repo(r) for r in items] return json.dumps({"count": len(repos), "repos": repos})
[docs] async def gitea_get_repo( owner: str, repo: str, ctx: ToolContext | None = None, ) -> str: """Fetch the details of a single Gitea repository. Backs the ``gitea_get_repo`` tool. Resolves credentials via ``_get_credentials`` and requests ``GET /repos/{owner}/{repo}`` through ``_gitea_request``, returning the repository condensed through ``_fmt_repo``. Performs HTTP and a Redis-backed credential read only. Registered as the ``gitea_get_repo`` handler in this module's ``TOOLS`` list and dispatched by name from the inference worker's tool loop; not called directly elsewhere in the repo. Args: owner: Repository owner (user or organization). repo: Repository name. ctx: The active ``ToolContext`` supplying user credentials. Returns: A JSON string with the compact repository summary, or a JSON error object on API failure. """ token, base_url = await _get_credentials(ctx) data = await _gitea_request("GET", f"/repos/{owner}/{repo}", token, base_url) if isinstance(data, dict) and "error" in data: return json.dumps(data) return json.dumps(_fmt_repo(data) if isinstance(data, dict) else data)
[docs] async def gitea_create_repo( name: str, description: str = "", private: bool = False, auto_init: bool = True, ctx: ToolContext | None = None, ) -> str: """Create a new repository owned by the authenticated user. Backs the ``gitea_create_repo`` tool. Resolves credentials via ``_get_credentials`` and POSTs to ``/user/repos`` through ``_gitea_request`` with the name, optional description, privacy flag, and an ``auto_init`` flag that seeds a README so the repo has a default branch. This mutates server state on the Gitea instance; locally it performs HTTP and a Redis-backed credential read only. The created repo is returned condensed through ``_fmt_repo``. Registered as the ``gitea_create_repo`` handler in this module's ``TOOLS`` list and dispatched by name from the inference worker's tool loop; not called directly elsewhere in the repo. Args: name: Name for the new repository. description: Optional repository description; omitted when empty. private: Whether the repository should be private. auto_init: Whether to initialise the repository with a README. ctx: The active ``ToolContext`` supplying user credentials. Returns: A JSON string with the compact summary of the created repository, or a JSON error object on API failure. """ token, base_url = await _get_credentials(ctx) payload: dict[str, Any] = {"name": name} if description: payload["description"] = description payload["private"] = private payload["auto_init"] = auto_init data = await _gitea_request( "POST", "/user/repos", token, base_url, json_body=payload ) if isinstance(data, dict) and "error" in data: return json.dumps(data) return json.dumps(_fmt_repo(data) if isinstance(data, dict) else data)
[docs] async def gitea_list_issues( owner: str, repo: str, state: str = "open", labels: str = "", page: int = 1, limit: int = 20, ctx: ToolContext | None = None, ) -> str: """List issues on a Gitea repository, optionally filtered by state and labels. Backs the ``gitea_list_issues`` tool. Resolves credentials via ``_get_credentials`` and pages ``GET /repos/{owner}/{repo}/issues`` through ``_gitea_request``, passing the ``state`` filter (``open``/``closed``/``all``) and an optional comma-separated ``labels`` filter, then condenses each issue through ``_fmt_issue``. Performs HTTP and a Redis-backed credential read only; no writes. Registered as the ``gitea_list_issues`` handler in this module's ``TOOLS`` list and dispatched by name from the inference worker's tool loop; not called directly elsewhere in the repo. Args: owner: Repository owner (user or organization). repo: Repository name. state: Issue state filter: ``open``, ``closed``, or ``all``. labels: Optional comma-separated label names to filter by. page: 1-based page number to fetch. limit: Results per page; values above 50 are clamped to 50. ctx: The active ``ToolContext`` supplying user credentials. Returns: A JSON string with a ``count`` and an ``issues`` array of compact issue summaries, or a JSON error object on API failure. """ token, base_url = await _get_credentials(ctx) params: dict[str, Any] = {"state": state, "page": page, "limit": min(limit, 50)} if labels: params["labels"] = labels data = await _gitea_request( "GET", f"/repos/{owner}/{repo}/issues", token, base_url, params=params ) if isinstance(data, dict) and "error" in data: return json.dumps(data) issues = [_fmt_issue(i) for i in data] if isinstance(data, list) else [] return json.dumps({"count": len(issues), "issues": issues})
[docs] async def gitea_get_issue( owner: str, repo: str, index: int, ctx: ToolContext | None = None, ) -> str: """Fetch a single issue by its per-repository index number. Backs the ``gitea_get_issue`` tool. Resolves credentials via ``_get_credentials`` and requests ``GET /repos/{owner}/{repo}/issues/{index}`` through ``_gitea_request``, returning the issue condensed through ``_fmt_issue``. Performs HTTP and a Redis-backed credential read only. Registered as the ``gitea_get_issue`` handler in this module's ``TOOLS`` list and dispatched by name from the inference worker's tool loop; not called directly elsewhere in the repo. Args: owner: Repository owner (user or organization). repo: Repository name. index: The issue's per-repository number. ctx: The active ``ToolContext`` supplying user credentials. Returns: A JSON string with the compact issue summary, or a JSON error object on API failure. """ token, base_url = await _get_credentials(ctx) data = await _gitea_request( "GET", f"/repos/{owner}/{repo}/issues/{index}", token, base_url ) if isinstance(data, dict) and "error" in data: return json.dumps(data) return json.dumps(_fmt_issue(data) if isinstance(data, dict) else data)
[docs] async def gitea_create_issue( owner: str, repo: str, title: str, body: str = "", labels: list[int] | None = None, assignees: list[str] | None = None, ctx: ToolContext | None = None, ) -> str: """Create a new issue on a Gitea repository. Backs the ``gitea_create_issue`` tool. Resolves credentials via ``_get_credentials`` and POSTs to ``/repos/{owner}/{repo}/issues`` through ``_gitea_request`` with the title and any supplied body, label IDs, and assignee usernames, returning the created issue condensed through ``_fmt_issue``. This mutates state on the Gitea instance; locally it performs HTTP and a Redis-backed credential read only. Registered as the ``gitea_create_issue`` handler in this module's ``TOOLS`` list and dispatched by name from the inference worker's tool loop. It is also imported and awaited directly by ``tools.gitea_integration`` to file issues from higher-level automation flows. Args: owner: Repository owner (user or organization). repo: Repository name. title: Issue title. body: Optional Markdown issue body. labels: Optional list of numeric label IDs to apply. assignees: Optional list of assignee usernames. ctx: The active ``ToolContext`` supplying user credentials. Returns: A JSON string with the compact summary of the created issue, or a JSON error object on API failure. """ token, base_url = await _get_credentials(ctx) payload: dict[str, Any] = {"title": title} if body: payload["body"] = body if labels: payload["labels"] = labels if assignees: payload["assignees"] = assignees data = await _gitea_request( "POST", f"/repos/{owner}/{repo}/issues", token, base_url, json_body=payload ) if isinstance(data, dict) and "error" in data: return json.dumps(data) return json.dumps(_fmt_issue(data) if isinstance(data, dict) else data)
[docs] async def gitea_update_issue( owner: str, repo: str, index: int, title: str | None = None, body: str | None = None, state: str | None = None, ctx: ToolContext | None = None, ) -> str: """Update the title, body, and/or state of an existing issue. Backs the ``gitea_update_issue`` tool. Resolves credentials via ``_get_credentials`` and PATCHes ``/repos/{owner}/{repo}/issues/{index}`` through ``_gitea_request`` with only the fields that were supplied (each ``None`` argument is omitted), returning the updated issue condensed through ``_fmt_issue``. Setting ``state`` to ``closed`` is how issues are closed. This mutates state on the Gitea instance; locally it performs HTTP and a Redis-backed credential read only. Registered as the ``gitea_update_issue`` handler in this module's ``TOOLS`` list and dispatched by name from the inference worker's tool loop. It is also imported and awaited directly by ``tools.gitea_integration`` (with ``state="closed"``) to close issues from higher-level automation flows. Args: owner: Repository owner (user or organization). repo: Repository name. index: The issue's per-repository number. title: New title, or ``None`` to leave unchanged. body: New Markdown body, or ``None`` to leave unchanged. state: New state (``open`` or ``closed``), or ``None`` to leave unchanged. ctx: The active ``ToolContext`` supplying user credentials. Returns: A JSON string with the compact summary of the updated issue, or a JSON error object on API failure. """ token, base_url = await _get_credentials(ctx) payload: dict[str, Any] = {} if title is not None: payload["title"] = title if body is not None: payload["body"] = body if state is not None: payload["state"] = state data = await _gitea_request( "PATCH", f"/repos/{owner}/{repo}/issues/{index}", token, base_url, json_body=payload, ) if isinstance(data, dict) and "error" in data: return json.dumps(data) return json.dumps(_fmt_issue(data) if isinstance(data, dict) else data)
[docs] async def gitea_list_pull_requests( owner: str, repo: str, state: str = "open", page: int = 1, limit: int = 20, ctx: ToolContext | None = None, ) -> str: """List pull requests on a Gitea repository, filtered by state. Backs the ``gitea_list_pull_requests`` tool. Resolves credentials via ``_get_credentials`` and pages ``GET /repos/{owner}/{repo}/pulls`` through ``_gitea_request`` with the ``state`` filter, condensing each PR through ``_fmt_pr``. Performs HTTP and a Redis-backed credential read only; no writes. Registered as the ``gitea_list_pull_requests`` handler in this module's ``TOOLS`` list and dispatched by name from the inference worker's tool loop; not called directly elsewhere in the repo. Args: owner: Repository owner (user or organization). repo: Repository name. state: PR state filter: ``open``, ``closed``, or ``all``. page: 1-based page number to fetch. limit: Results per page; values above 50 are clamped to 50. ctx: The active ``ToolContext`` supplying user credentials. Returns: A JSON string with a ``count`` and a ``pull_requests`` array of compact PR summaries, or a JSON error object on API failure. """ token, base_url = await _get_credentials(ctx) params = {"state": state, "page": page, "limit": min(limit, 50)} data = await _gitea_request( "GET", f"/repos/{owner}/{repo}/pulls", token, base_url, params=params ) if isinstance(data, dict) and "error" in data: return json.dumps(data) prs = [_fmt_pr(p) for p in data] if isinstance(data, list) else [] return json.dumps({"count": len(prs), "pull_requests": prs})
[docs] async def gitea_get_pull_request( owner: str, repo: str, index: int, ctx: ToolContext | None = None, ) -> str: """Fetch a single pull request by index, including a body excerpt. Backs the ``gitea_get_pull_request`` tool. Resolves credentials via ``_get_credentials`` and requests ``GET /repos/{owner}/{repo}/pulls/{index}`` through ``_gitea_request``, condensing the PR through ``_fmt_pr`` and then attaching the first 2000 characters of its body on top of that summary so the LLM can read the description. Performs HTTP and a Redis-backed credential read only. Registered as the ``gitea_get_pull_request`` handler in this module's ``TOOLS`` list and dispatched by name from the inference worker's tool loop; not called directly elsewhere in the repo. Args: owner: Repository owner (user or organization). repo: Repository name. index: The pull request's per-repository number. ctx: The active ``ToolContext`` supplying user credentials. Returns: A JSON string with the compact PR summary plus a truncated ``body``, or a JSON error object on API failure. """ token, base_url = await _get_credentials(ctx) data = await _gitea_request( "GET", f"/repos/{owner}/{repo}/pulls/{index}", token, base_url ) if isinstance(data, dict) and "error" in data: return json.dumps(data) result = _fmt_pr(data) if isinstance(data, dict) else {"raw": data} if isinstance(data, dict) and data.get("body"): result["body"] = (data.get("body") or "")[:2000] return json.dumps(result)
[docs] async def gitea_create_pull_request( owner: str, repo: str, title: str, head: str, base: str = "main", body: str = "", ctx: ToolContext | None = None, ) -> str: """Open a new pull request from a head branch into a base branch. Backs the ``gitea_create_pull_request`` tool. Resolves credentials via ``_get_credentials`` and POSTs to ``/repos/{owner}/{repo}/pulls`` through ``_gitea_request`` with the title, head ref (which may be ``owner:branch`` for cross-fork PRs), base branch, and optional body, returning the created PR condensed through ``_fmt_pr``. This mutates state on the Gitea instance; locally it performs HTTP and a Redis-backed credential read only. Registered as the ``gitea_create_pull_request`` handler in this module's ``TOOLS`` list and dispatched by name from the inference worker's tool loop; not called directly elsewhere in the repo. Args: owner: Repository owner (user or organization). repo: Repository name. title: Pull request title. head: Source branch, or ``owner:branch`` when the head is on a fork. base: Target branch to merge into; defaults to ``main``. body: Optional Markdown description; omitted when empty. ctx: The active ``ToolContext`` supplying user credentials. Returns: A JSON string with the compact summary of the created pull request, or a JSON error object on API failure. """ token, base_url = await _get_credentials(ctx) payload: dict[str, Any] = {"title": title, "head": head, "base": base} if body: payload["body"] = body data = await _gitea_request( "POST", f"/repos/{owner}/{repo}/pulls", token, base_url, json_body=payload ) if isinstance(data, dict) and "error" in data: return json.dumps(data) return json.dumps(_fmt_pr(data) if isinstance(data, dict) else data)
[docs] async def gitea_merge_pull_request( owner: str, repo: str, index: int, merge_style: str = "merge", delete_branch_after_merge: bool = False, ctx: ToolContext | None = None, ) -> str: """Merge a pull request using the requested merge strategy. Backs the ``gitea_merge_pull_request`` tool. Resolves credentials via ``_get_credentials`` and POSTs to ``/repos/{owner}/{repo}/pulls/{index}/merge`` through ``_gitea_request`` with the merge strategy under Gitea's ``Do`` key and an optional flag to delete the head branch afterwards. The supported ``merge_style`` values are ``merge``, ``rebase``, ``rebase-merge``, and ``squash``. On success it returns a fixed status object rather than the raw API body. This mutates repository state (and may delete a branch); locally it performs HTTP and a Redis-backed credential read only. Registered as the ``gitea_merge_pull_request`` handler in this module's ``TOOLS`` list and dispatched by name from the inference worker's tool loop; not called directly elsewhere in the repo. Args: owner: Repository owner (user or organization). repo: Repository name. index: The pull request's per-repository number. merge_style: Merge strategy: ``merge``, ``rebase``, ``rebase-merge``, or ``squash``. delete_branch_after_merge: Whether to delete the head branch once merged. ctx: The active ``ToolContext`` supplying user credentials. Returns: A JSON success object on a successful merge, or a JSON error object on API failure. """ token, base_url = await _get_credentials(ctx) payload: dict[str, Any] = {"Do": merge_style} if delete_branch_after_merge: payload["delete_branch_after_merge"] = True data = await _gitea_request( "POST", f"/repos/{owner}/{repo}/pulls/{index}/merge", token, base_url, json_body=payload, ) if isinstance(data, dict) and "error" in data: return json.dumps(data) return json.dumps({"status": "success", "message": "Pull request merged"})
[docs] async def gitea_get_file( owner: str, repo: str, path: str, ref: str = "", ctx: ToolContext | None = None, ) -> str: """Read a file's contents or list a directory in a Gitea repository. Backs the ``gitea_get_file`` tool. Resolves credentials via ``_get_credentials`` and requests ``GET /repos/{owner}/{repo}/contents/{path}`` through ``_gitea_request`` at an optional ``ref`` (branch, tag, or SHA). The Gitea contents endpoint is polymorphic, so this branches on the response: for a file it base64-decodes the embedded content (falling back to a "(binary file)" marker when decoding fails) and returns name, path, size, SHA, a truncated body, and HTML URL; for a directory it returns a list of name/type/path entries. Performs HTTP and a Redis-backed credential read only. Registered as the ``gitea_get_file`` handler in this module's ``TOOLS`` list and dispatched by name from the inference worker's tool loop; not called directly elsewhere in the repo. Args: owner: Repository owner (user or organization). repo: Repository name. path: File or directory path within the repository. ref: Optional branch, tag, or commit SHA; defaults to the default branch. ctx: The active ``ToolContext`` supplying user credentials. Returns: A JSON string describing either a file (with decoded, truncated ``content``) or a directory listing, or a JSON error object on API failure. """ token, base_url = await _get_credentials(ctx) params = {} if ref: params["ref"] = ref data = await _gitea_request( "GET", f"/repos/{owner}/{repo}/contents/{path}", token, base_url, params=params ) if isinstance(data, dict) and "error" in data: return json.dumps(data) if isinstance(data, dict) and data.get("type") == "file": content_b64 = data.get("content", "") try: content = base64.b64decode(content_b64).decode("utf-8", errors="replace") except Exception: content = "(binary file)" return json.dumps( { "name": data.get("name"), "path": data.get("path"), "size": data.get("size"), "sha": data.get("sha"), "content": content[:16000], "html_url": data.get("html_url"), } ) if isinstance(data, list): entries = [ {"name": e.get("name"), "type": e.get("type"), "path": e.get("path")} for e in data ] return json.dumps({"type": "directory", "entries": entries}) return json.dumps(data if isinstance(data, dict) else {"raw": str(data)[:4000]})
[docs] async def gitea_list_commits( owner: str, repo: str, sha: str = "", page: int = 1, limit: int = 20, ctx: ToolContext | None = None, ) -> str: """List commits in a Gitea repository, optionally from a given ref. Backs the ``gitea_list_commits`` tool. Resolves credentials via ``_get_credentials`` and pages ``GET /repos/{owner}/{repo}/commits`` through ``_gitea_request``, optionally scoped to a branch or SHA. Each commit is flattened inline to its SHA, a truncated message, author name and date, and HTML URL (rather than via a shared formatter). Performs HTTP and a Redis-backed credential read only; no writes. Registered as the ``gitea_list_commits`` handler in this module's ``TOOLS`` list and dispatched by name from the inference worker's tool loop; not called directly elsewhere in the repo. Args: owner: Repository owner (user or organization). repo: Repository name. sha: Optional branch name or commit SHA to list history from. page: 1-based page number to fetch. limit: Results per page; values above 50 are clamped to 50. ctx: The active ``ToolContext`` supplying user credentials. Returns: A JSON string with a ``count`` and a ``commits`` array of compact commit summaries, or a JSON error object on API failure. """ token, base_url = await _get_credentials(ctx) params: dict[str, Any] = {"page": page, "limit": min(limit, 50)} if sha: params["sha"] = sha data = await _gitea_request( "GET", f"/repos/{owner}/{repo}/commits", token, base_url, params=params ) if isinstance(data, dict) and "error" in data: return json.dumps(data) commits = [] for c in (data if isinstance(data, list) else []): commit = c.get("commit") or {} author = commit.get("author") or {} commits.append( { "sha": c.get("sha"), "message": commit.get("message", "")[:500], "author": author.get("name"), "date": author.get("date"), "html_url": c.get("html_url"), } ) return json.dumps({"count": len(commits), "commits": commits})
[docs] async def gitea_get_commit( owner: str, repo: str, sha: str, ctx: ToolContext | None = None, ) -> str: """Fetch a single commit's metadata and stats by SHA or ref. Backs the ``gitea_get_commit`` tool. Resolves credentials via ``_get_credentials`` and requests ``GET /repos/{owner}/{repo}/git/commits/{sha}`` through ``_gitea_request``, returning the SHA, a truncated commit message, author name and date, HTML URL, and the diff ``stats`` block. Defensive ``isinstance`` guards keep the projection safe when the API returns an unexpected shape. Performs HTTP and a Redis-backed credential read only. Registered as the ``gitea_get_commit`` handler in this module's ``TOOLS`` list and dispatched by name from the inference worker's tool loop; not called directly elsewhere in the repo. Args: owner: Repository owner (user or organization). repo: Repository name. sha: Commit SHA or ref to fetch. ctx: The active ``ToolContext`` supplying user credentials. Returns: A JSON string with the compact commit summary including ``stats``, or a JSON error object on API failure. """ token, base_url = await _get_credentials(ctx) data = await _gitea_request( "GET", f"/repos/{owner}/{repo}/git/commits/{sha}", token, base_url ) if isinstance(data, dict) and "error" in data: return json.dumps(data) commit = data.get("commit") or {} if isinstance(data, dict) else {} author = commit.get("author") or {} return json.dumps( { "sha": data.get("sha") if isinstance(data, dict) else None, "message": commit.get("message", "")[:2000], "author": author.get("name"), "date": author.get("date"), "html_url": data.get("html_url") if isinstance(data, dict) else None, "stats": data.get("stats") if isinstance(data, dict) else None, } )
[docs] async def gitea_list_notifications( all_notifications: bool = False, page: int = 1, limit: int = 20, ctx: ToolContext | None = None, ) -> str: """List the authenticated user's Gitea notification feed. Backs the ``gitea_list_notifications`` tool. Resolves credentials via ``_get_credentials`` and pages ``GET /notifications`` through ``_gitea_request``; by default only unread notifications are returned, while ``all_notifications`` includes read ones. Each entry is flattened inline to its id, unread flag, subject title and type, owning repository full name, and update timestamp. Performs HTTP and a Redis-backed credential read only; no writes. Registered as the ``gitea_list_notifications`` handler in this module's ``TOOLS`` list and dispatched by name from the inference worker's tool loop; not called directly elsewhere in the repo. Args: all_notifications: Whether to include already-read notifications. page: 1-based page number to fetch. limit: Results per page; values above 50 are clamped to 50. ctx: The active ``ToolContext`` supplying user credentials. Returns: A JSON string with a ``count`` and a ``notifications`` array of compact notification summaries, or a JSON error object on API failure. """ token, base_url = await _get_credentials(ctx) params = { "all": str(all_notifications).lower(), "page": page, "limit": min(limit, 50), } data = await _gitea_request("GET", "/notifications", token, base_url, params=params) if isinstance(data, dict) and "error" in data: return json.dumps(data) notifs = [] for n in (data if isinstance(data, list) else []): subject = n.get("subject") or {} notifs.append( { "id": n.get("id"), "unread": n.get("unread"), "subject_title": subject.get("title"), "subject_type": subject.get("type"), "repository": ( n.get("repository", {}).get("full_name") if isinstance(n.get("repository"), dict) else None ), "updated_at": n.get("updated_at"), } ) return json.dumps({"count": len(notifs), "notifications": notifs})
[docs] async def gitea_star_repo( owner: str, repo: str, star: bool = True, ctx: ToolContext | None = None, ) -> str: """Star or unstar a Gitea repository for the authenticated user. Backs the ``gitea_star_repo`` tool. Resolves credentials via ``_get_credentials`` and calls ``/user/starred/{owner}/{repo}`` through ``_gitea_request`` using ``PUT`` to star or ``DELETE`` to unstar based on the ``star`` flag. On success it returns a fixed status object naming the action taken. This mutates the user's star set on the Gitea instance; locally it performs HTTP and a Redis-backed credential read only. Registered as the ``gitea_star_repo`` handler in this module's ``TOOLS`` list and dispatched by name from the inference worker's tool loop; not called directly elsewhere in the repo. Args: owner: Repository owner (user or organization). repo: Repository name. star: ``True`` to star the repository, ``False`` to unstar it. ctx: The active ``ToolContext`` supplying user credentials. Returns: A JSON success object describing the ``starred`` or ``unstarred`` action, or a JSON error object on API failure. """ token, base_url = await _get_credentials(ctx) method = "PUT" if star else "DELETE" data = await _gitea_request( method, f"/user/starred/{owner}/{repo}", token, base_url ) if isinstance(data, dict) and "error" in data: return json.dumps(data) action = "starred" if star else "unstarred" return json.dumps( {"status": "success", "action": action, "repo": f"{owner}/{repo}"} )
[docs] async def gitea_list_branches( owner: str, repo: str, page: int = 1, limit: int = 30, ctx: ToolContext | None = None, ) -> str: """List the branches of a Gitea repository. Backs the ``gitea_list_branches`` tool. Resolves credentials via ``_get_credentials`` and pages ``GET /repos/{owner}/{repo}/branches`` through ``_gitea_request``, projecting each branch to its name, protection flag, and head commit SHA. Performs HTTP and a Redis-backed credential read only; no writes. Registered as the ``gitea_list_branches`` handler in this module's ``TOOLS`` list and dispatched by name from the inference worker's tool loop; not called directly elsewhere in the repo. Args: owner: Repository owner (user or organization). repo: Repository name. page: 1-based page number to fetch. limit: Results per page; values above 50 are clamped to 50. ctx: The active ``ToolContext`` supplying user credentials. Returns: A JSON string with a ``count`` and a ``branches`` array of compact branch summaries, or a JSON error object on API failure. """ token, base_url = await _get_credentials(ctx) params = {"page": page, "limit": min(limit, 50)} data = await _gitea_request( "GET", f"/repos/{owner}/{repo}/branches", token, base_url, params=params ) if isinstance(data, dict) and "error" in data: return json.dumps(data) branches = [ { "name": b.get("name"), "protected": b.get("protected"), "commit_sha": (b.get("commit") or {}).get("sha"), } for b in (data if isinstance(data, list) else []) ] return json.dumps({"count": len(branches), "branches": branches})
[docs] async def gitea_create_repo_from_template( template_owner: str, template_repo: str, name: str, description: str = "", private: bool = False, git_content: bool = True, ctx: ToolContext | None = None, ) -> str: """Generate a new repository from an existing template repository. Backs the ``gitea_create_repo_from_template`` tool. Resolves credentials via ``_get_credentials`` and POSTs to ``/repos/{template_owner}/{template_repo}/generate`` through ``_gitea_request`` with the new repo name, privacy flag, an optional description, and a ``git_content`` flag that controls whether the template's git history is copied. The created repo is returned condensed through ``_fmt_repo``. This mutates state on the Gitea instance; locally it performs HTTP and a Redis-backed credential read only. Registered as the ``gitea_create_repo_from_template`` handler in this module's ``TOOLS`` list and dispatched by name from the inference worker's tool loop; not called directly elsewhere in the repo. Args: template_owner: Owner of the template repository. template_repo: Name of the template repository. name: Name for the new repository. description: Optional description for the new repository. private: Whether the new repository should be private. git_content: Whether to copy the template's git content. ctx: The active ``ToolContext`` supplying user credentials. Returns: A JSON string with the compact summary of the generated repository, or a JSON error object on API failure. """ token, base_url = await _get_credentials(ctx) payload: dict[str, Any] = { "name": name, "private": private, "git_content": git_content, } if description: payload["description"] = description data = await _gitea_request( "POST", f"/repos/{template_owner}/{template_repo}/generate", token, base_url, json_body=payload, ) if isinstance(data, dict) and "error" in data: return json.dumps(data) return json.dumps(_fmt_repo(data) if isinstance(data, dict) else data)
[docs] async def gitea_compare_commits( owner: str, repo: str, basehead: str, ctx: ToolContext | None = None, ) -> str: """Compare two refs or commits and report the diff between them. Backs the ``gitea_compare_commits`` tool. Resolves credentials via ``_get_credentials`` and requests ``GET /repos/{owner}/{repo}/compare/{basehead}`` through ``_gitea_request``, where ``basehead`` uses Gitea's compare syntax (e.g. ``main...feature``); the value is URL-quoted while preserving ``:`` and ``.`` so the ``...`` separator survives. Unlike most handlers it returns the raw API response unconditionally (wrapping non-JSON in a truncated ``raw`` field) rather than reformatting. Performs HTTP and a Redis-backed credential read only. Registered as the ``gitea_compare_commits`` handler in this module's ``TOOLS`` list and dispatched by name from the inference worker's tool loop; not called directly elsewhere in the repo. Args: owner: Repository owner (user or organization). repo: Repository name. basehead: Compare expression in ``base...head`` form. ctx: The active ``ToolContext`` supplying user credentials. Returns: A JSON string with the comparison result (commits and changed files), or a truncated raw payload when the response is not structured. """ token, base_url = await _get_credentials(ctx) path = f"/repos/{owner}/{repo}/compare/{quote(basehead, safe=':.')}" data = await _gitea_request("GET", path, token, base_url) return ( json.dumps(data) if isinstance(data, (dict, list)) else json.dumps({"raw": str(data)[:8000]}) )
[docs] async def gitea_list_tags( owner: str, repo: str, page: int = 1, limit: int = 30, ctx: ToolContext | None = None, ) -> str: """List the tags of a Gitea repository. Backs the ``gitea_list_tags`` tool. Resolves credentials via ``_get_credentials`` and pages ``GET /repos/{owner}/{repo}/tags`` through ``_gitea_request``, projecting each tag to its name, id, tagged commit SHA, and a truncated message. Performs HTTP and a Redis-backed credential read only; no writes. Registered as the ``gitea_list_tags`` handler in this module's ``TOOLS`` list and dispatched by name from the inference worker's tool loop; not called directly elsewhere in the repo. Args: owner: Repository owner (user or organization). repo: Repository name. page: 1-based page number to fetch. limit: Results per page; values above 50 are clamped to 50. ctx: The active ``ToolContext`` supplying user credentials. Returns: A JSON string with a ``count`` and a ``tags`` array of compact tag summaries, or a JSON error object on API failure. """ token, base_url = await _get_credentials(ctx) params = {"page": page, "limit": min(limit, 50)} data = await _gitea_request( "GET", f"/repos/{owner}/{repo}/tags", token, base_url, params=params ) if isinstance(data, dict) and "error" in data: return json.dumps(data) tags = [] for t in (data if isinstance(data, list) else []): tags.append( { "name": t.get("name"), "id": t.get("id"), "commit": ( (t.get("commit") or {}).get("sha") if isinstance(t.get("commit"), dict) else None ), "message": (t.get("message") or "")[:500], } ) return json.dumps({"count": len(tags), "tags": tags})
[docs] async def gitea_create_tag( owner: str, repo: str, tag_name: str, target: str, message: str = "", ctx: ToolContext | None = None, ) -> str: """Create a tag pointing at a commit or branch. Backs the ``gitea_create_tag`` tool. Resolves credentials via ``_get_credentials`` and POSTs to ``/repos/{owner}/{repo}/tags`` through ``_gitea_request`` with the tag name and target ref, plus an optional message that makes it an annotated tag. The Gitea instance generally requires a ``write:repository`` scope for this. This mutates repository state; locally it performs HTTP and a Redis-backed credential read only. Registered as the ``gitea_create_tag`` handler in this module's ``TOOLS`` list and dispatched by name from the inference worker's tool loop; not called directly elsewhere in the repo. Args: owner: Repository owner (user or organization). repo: Repository name. tag_name: Name of the tag to create. target: Branch name or commit SHA the tag should point at. message: Optional annotated-tag message; omitted when empty. ctx: The active ``ToolContext`` supplying user credentials. Returns: A JSON string with the created tag object, or a truncated raw payload when the response is not structured. """ token, base_url = await _get_credentials(ctx) payload: dict[str, Any] = {"tag_name": tag_name, "target": target} if message: payload["message"] = message data = await _gitea_request( "POST", f"/repos/{owner}/{repo}/tags", token, base_url, json_body=payload ) return ( json.dumps(data) if isinstance(data, dict) else json.dumps({"raw": str(data)[:4000]}) )
[docs] async def gitea_list_milestones( owner: str, repo: str, state: str = "open", page: int = 1, limit: int = 20, ctx: ToolContext | None = None, ) -> str: """List the milestones of a Gitea repository, filtered by state. Backs the ``gitea_list_milestones`` tool. Resolves credentials via ``_get_credentials`` and pages ``GET /repos/{owner}/{repo}/milestones`` through ``_gitea_request`` with the ``state`` filter, projecting each milestone to id, title, a truncated description, state, open/closed issue counts, and due date. Most Gitea instances require a ``read:issue`` scope for this. Performs HTTP and a Redis-backed credential read only; no writes. Registered as the ``gitea_list_milestones`` handler in this module's ``TOOLS`` list and dispatched by name from the inference worker's tool loop; not called directly elsewhere in the repo. Args: owner: Repository owner (user or organization). repo: Repository name. state: Milestone state filter: ``open``, ``closed``, or ``all``. page: 1-based page number to fetch. limit: Results per page; values above 50 are clamped to 50. ctx: The active ``ToolContext`` supplying user credentials. Returns: A JSON string with a ``count`` and a ``milestones`` array of compact milestone summaries, or a JSON error object on API failure. """ token, base_url = await _get_credentials(ctx) params: dict[str, Any] = {"state": state, "page": page, "limit": min(limit, 50)} data = await _gitea_request( "GET", f"/repos/{owner}/{repo}/milestones", token, base_url, params=params ) if isinstance(data, dict) and "error" in data: return json.dumps(data) out = [] for m in (data if isinstance(data, list) else []): out.append( { "id": m.get("id"), "title": m.get("title"), "description": (m.get("description") or "")[:500], "state": m.get("state"), "open_issues": m.get("open_issues"), "closed_issues": m.get("closed_issues"), "due_on": m.get("due_on"), } ) return json.dumps({"count": len(out), "milestones": out})
[docs] async def gitea_get_milestone( owner: str, repo: str, milestone_id: int, ctx: ToolContext | None = None, ) -> str: """Fetch a single milestone by its numeric id. Backs the ``gitea_get_milestone`` tool. Resolves credentials via ``_get_credentials`` and requests ``GET /repos/{owner}/{repo}/milestones/{milestone_id}`` through ``_gitea_request``, returning the raw milestone object (wrapping a non-JSON response in a truncated ``raw`` field). Performs HTTP and a Redis-backed credential read only. Registered as the ``gitea_get_milestone`` handler in this module's ``TOOLS`` list and dispatched by name from the inference worker's tool loop; not called directly elsewhere in the repo. Args: owner: Repository owner (user or organization). repo: Repository name. milestone_id: Numeric milestone id. ctx: The active ``ToolContext`` supplying user credentials. Returns: A JSON string with the milestone object, or a truncated raw payload when the response is not structured. """ token, base_url = await _get_credentials(ctx) data = await _gitea_request( "GET", f"/repos/{owner}/{repo}/milestones/{milestone_id}", token, base_url ) return ( json.dumps(data) if isinstance(data, dict) else json.dumps({"raw": str(data)[:4000]}) )
[docs] async def gitea_create_milestone( owner: str, repo: str, title: str, description: str = "", due_on: str = "", ctx: ToolContext | None = None, ) -> str: """Create a new milestone on a Gitea repository. Backs the ``gitea_create_milestone`` tool. Resolves credentials via ``_get_credentials`` and POSTs to ``/repos/{owner}/{repo}/milestones`` through ``_gitea_request`` with the title and any supplied description and due date. The ``due_on`` value, when set, must be an ISO 8601 datetime. Creating milestones typically requires a ``write:issue`` scope on the instance. This mutates repository state; locally it performs HTTP and a Redis-backed credential read only. Registered as the ``gitea_create_milestone`` handler in this module's ``TOOLS`` list and dispatched by name from the inference worker's tool loop; not called directly elsewhere in the repo. Args: owner: Repository owner (user or organization). repo: Repository name. title: Milestone title. description: Optional milestone description; omitted when empty. due_on: Optional ISO 8601 due date; omitted when empty. ctx: The active ``ToolContext`` supplying user credentials. Returns: A JSON string with the created milestone object, or a truncated raw payload when the response is not structured. """ token, base_url = await _get_credentials(ctx) payload: dict[str, Any] = {"title": title} if description: payload["description"] = description if due_on: payload["due_on"] = due_on data = await _gitea_request( "POST", f"/repos/{owner}/{repo}/milestones", token, base_url, json_body=payload ) return ( json.dumps(data) if isinstance(data, dict) else json.dumps({"raw": str(data)[:4000]}) )
[docs] async def gitea_update_milestone( owner: str, repo: str, milestone_id: int, title: str | None = None, description: str | None = None, state: str | None = None, due_on: str | None = None, ctx: ToolContext | None = None, ) -> str: """Update the fields of an existing milestone. Backs the ``gitea_update_milestone`` tool. Resolves credentials via ``_get_credentials`` and PATCHes ``/repos/{owner}/{repo}/milestones/{milestone_id}`` through ``_gitea_request`` with only the fields that were supplied (each ``None`` argument is omitted), so it can change the title, description, state, and/or due date in one call. This mutates repository state; locally it performs HTTP and a Redis-backed credential read only. Registered as the ``gitea_update_milestone`` handler in this module's ``TOOLS`` list and dispatched by name from the inference worker's tool loop; not called directly elsewhere in the repo. Args: owner: Repository owner (user or organization). repo: Repository name. milestone_id: Numeric milestone id. title: New title, or ``None`` to leave unchanged. description: New description, or ``None`` to leave unchanged. state: New state (``open`` or ``closed``), or ``None`` to leave unchanged. due_on: New ISO 8601 due date, or ``None`` to leave unchanged. ctx: The active ``ToolContext`` supplying user credentials. Returns: A JSON string with the updated milestone object, or a truncated raw payload when the response is not structured. """ token, base_url = await _get_credentials(ctx) payload: dict[str, Any] = {} if title is not None: payload["title"] = title if description is not None: payload["description"] = description if state is not None: payload["state"] = state if due_on is not None: payload["due_on"] = due_on data = await _gitea_request( "PATCH", f"/repos/{owner}/{repo}/milestones/{milestone_id}", token, base_url, json_body=payload, ) return ( json.dumps(data) if isinstance(data, dict) else json.dumps({"raw": str(data)[:4000]}) )
[docs] async def gitea_delete_milestone( owner: str, repo: str, milestone_id: int, ctx: ToolContext | None = None, ) -> str: """Delete a milestone from a Gitea repository by id. Backs the ``gitea_delete_milestone`` tool. Resolves credentials via ``_get_credentials`` and issues ``DELETE /repos/{owner}/{repo}/milestones/{milestone_id}`` through ``_gitea_request``; on a no-content success it returns a small status object carrying the API detail. This permanently removes the milestone on the Gitea instance; locally it performs HTTP and a Redis-backed credential read only. Registered as the ``gitea_delete_milestone`` handler in this module's ``TOOLS`` list and dispatched by name from the inference worker's tool loop; not called directly elsewhere in the repo. Args: owner: Repository owner (user or organization). repo: Repository name. milestone_id: Numeric milestone id to delete. ctx: The active ``ToolContext`` supplying user credentials. Returns: A JSON string with the API response, or an ``ok`` status object when the delete returns no content. """ token, base_url = await _get_credentials(ctx) data = await _gitea_request( "DELETE", f"/repos/{owner}/{repo}/milestones/{milestone_id}", token, base_url ) return ( json.dumps(data) if isinstance(data, dict) else json.dumps({"status": "ok", "detail": str(data)}) )
[docs] async def gitea_list_labels( owner: str, repo: str, page: int = 1, limit: int = 30, ctx: ToolContext | None = None, ) -> str: """List the issue labels defined on a Gitea repository. Backs the ``gitea_list_labels`` tool. Resolves credentials via ``_get_credentials`` and pages ``GET /repos/{owner}/{repo}/labels`` through ``_gitea_request``, projecting each label to its id, name, hex color, and description. Many instances require a ``read:issue`` scope for this. Performs HTTP and a Redis-backed credential read only; no writes. Registered as the ``gitea_list_labels`` handler in this module's ``TOOLS`` list and dispatched by name from the inference worker's tool loop; not called directly elsewhere in the repo. Args: owner: Repository owner (user or organization). repo: Repository name. page: 1-based page number to fetch. limit: Results per page; values above 50 are clamped to 50. ctx: The active ``ToolContext`` supplying user credentials. Returns: A JSON string with a ``count`` and a ``labels`` array of compact label summaries, or a JSON error object on API failure. """ token, base_url = await _get_credentials(ctx) params = {"page": page, "limit": min(limit, 50)} data = await _gitea_request( "GET", f"/repos/{owner}/{repo}/labels", token, base_url, params=params ) if isinstance(data, dict) and "error" in data: return json.dumps(data) labels = [] for lb in (data if isinstance(data, list) else []): labels.append( { "id": lb.get("id"), "name": lb.get("name"), "color": lb.get("color"), "description": lb.get("description"), } ) return json.dumps({"count": len(labels), "labels": labels})
[docs] async def gitea_create_label( owner: str, repo: str, name: str, color: str, description: str = "", ctx: ToolContext | None = None, ) -> str: """Create a new issue label on a Gitea repository. Backs the ``gitea_create_label`` tool. Resolves credentials via ``_get_credentials`` and POSTs to ``/repos/{owner}/{repo}/labels`` through ``_gitea_request`` with the label name, a hex color (any leading ``#`` is stripped so callers may pass either form), and an optional description. Creating labels typically requires a ``write:issue`` scope on the instance. This mutates repository state; locally it performs HTTP and a Redis-backed credential read only. Registered as the ``gitea_create_label`` handler in this module's ``TOOLS`` list and dispatched by name from the inference worker's tool loop; not called directly elsewhere in the repo. Args: owner: Repository owner (user or organization). repo: Repository name. name: Label name. color: Hex color, with or without a leading ``#`` (e.g. ``e11d21``). description: Optional label description; omitted when empty. ctx: The active ``ToolContext`` supplying user credentials. Returns: A JSON string with the created label object, or a truncated raw payload when the response is not structured. """ token, base_url = await _get_credentials(ctx) payload: dict[str, Any] = {"name": name, "color": color.lstrip("#")} if description: payload["description"] = description data = await _gitea_request( "POST", f"/repos/{owner}/{repo}/labels", token, base_url, json_body=payload ) return ( json.dumps(data) if isinstance(data, dict) else json.dumps({"raw": str(data)[:4000]}) )
[docs] async def gitea_list_issue_comments( owner: str, repo: str, index: int, page: int = 1, limit: int = 30, ctx: ToolContext | None = None, ) -> str: """List the comments on an issue or pull request. Backs the ``gitea_list_issue_comments`` tool. In Gitea issues and pull requests share a single numbering space, so this works for either. Resolves credentials via ``_get_credentials`` and pages ``GET /repos/{owner}/{repo}/issues/{index}/comments`` through ``_gitea_request``, projecting each comment to its id, a truncated body, author login, and timestamps. Performs HTTP and a Redis-backed credential read only; no writes. Registered as the ``gitea_list_issue_comments`` handler in this module's ``TOOLS`` list and dispatched by name from the inference worker's tool loop. It is also awaited directly by ``gitea_list_pull_comments`` in this module, which is just an alias for PR comment threads. Args: owner: Repository owner (user or organization). repo: Repository name. index: The issue or pull request number. page: 1-based page number to fetch. limit: Results per page; values above 50 are clamped to 50. ctx: The active ``ToolContext`` supplying user credentials. Returns: A JSON string with a ``count`` and a ``comments`` array of compact comment summaries, or a JSON error object on API failure. """ token, base_url = await _get_credentials(ctx) params = {"page": page, "limit": min(limit, 50)} data = await _gitea_request( "GET", f"/repos/{owner}/{repo}/issues/{index}/comments", token, base_url, params=params, ) if isinstance(data, dict) and "error" in data: return json.dumps(data) comments = [] for c in (data if isinstance(data, list) else []): u = c.get("user") or {} comments.append( { "id": c.get("id"), "body": (c.get("body") or "")[:2000], "user": u.get("login") if isinstance(u, dict) else None, "created_at": c.get("created_at"), "updated_at": c.get("updated_at"), } ) return json.dumps({"count": len(comments), "comments": comments})
[docs] async def gitea_create_issue_comment( owner: str, repo: str, index: int, body: str, ctx: ToolContext | None = None, ) -> str: """Post a comment on an issue or pull request. Backs the ``gitea_create_issue_comment`` tool. Because Gitea shares one numbering space between issues and PRs, the same ``index`` addresses either. Resolves credentials via ``_get_credentials`` and POSTs the Markdown body to ``/repos/{owner}/{repo}/issues/{index}/comments`` through ``_gitea_request``. This mutates state on the Gitea instance; locally it performs HTTP and a Redis-backed credential read only. Registered as the ``gitea_create_issue_comment`` handler in this module's ``TOOLS`` list and dispatched by name from the inference worker's tool loop. It is also imported and awaited directly by ``tools.gitea_integration`` to post comments from higher-level automation flows. Args: owner: Repository owner (user or organization). repo: Repository name. index: The issue or pull request number. body: Markdown comment body. ctx: The active ``ToolContext`` supplying user credentials. Returns: A JSON string with the created comment object, or a truncated raw payload when the response is not structured. """ token, base_url = await _get_credentials(ctx) data = await _gitea_request( "POST", f"/repos/{owner}/{repo}/issues/{index}/comments", token, base_url, json_body={"body": body}, ) return ( json.dumps(data) if isinstance(data, dict) else json.dumps({"raw": str(data)[:4000]}) )
[docs] async def gitea_list_releases( owner: str, repo: str, draft: bool = False, pre_release: bool = False, page: int = 1, limit: int = 10, ctx: ToolContext | None = None, ) -> str: """List the releases of a Gitea repository. Backs the ``gitea_list_releases`` tool. Resolves credentials via ``_get_credentials`` and pages ``GET /repos/{owner}/{repo}/releases`` through ``_gitea_request``, optionally filtering by draft and pre-release status, and projects each release to its id, tag name, name, draft/prerelease flags, timestamps, and HTML URL. Performs HTTP and a Redis-backed credential read only; no writes. Registered as the ``gitea_list_releases`` handler in this module's ``TOOLS`` list and dispatched by name from the inference worker's tool loop; not called directly elsewhere in the repo. Args: owner: Repository owner (user or organization). repo: Repository name. draft: Whether to include draft releases in the listing. pre_release: Whether to include pre-releases in the listing. page: 1-based page number to fetch. limit: Results per page; values above 50 are clamped to 50. ctx: The active ``ToolContext`` supplying user credentials. Returns: A JSON string with a ``count`` and a ``releases`` array of compact release summaries, or a JSON error object on API failure. """ token, base_url = await _get_credentials(ctx) params: dict[str, Any] = { "draft": str(draft).lower(), "pre-release": str(pre_release).lower(), "page": page, "limit": min(limit, 50), } data = await _gitea_request( "GET", f"/repos/{owner}/{repo}/releases", token, base_url, params=params ) if isinstance(data, dict) and "error" in data: return json.dumps(data) rels = [] for r in (data if isinstance(data, list) else []): rels.append( { "id": r.get("id"), "tag_name": r.get("tag_name"), "name": r.get("name"), "draft": r.get("draft"), "prerelease": r.get("prerelease"), "created_at": r.get("created_at"), "published_at": r.get("published_at"), "html_url": r.get("html_url"), } ) return json.dumps({"count": len(rels), "releases": rels})
[docs] async def gitea_get_release( owner: str, repo: str, release_id: int, ctx: ToolContext | None = None, ) -> str: """Fetch a single release by its numeric id. Backs the ``gitea_get_release`` tool. Resolves credentials via ``_get_credentials`` and requests ``GET /repos/{owner}/{repo}/releases/{release_id}`` through ``_gitea_request``, returning the raw release object (wrapping a non-JSON response in a truncated ``raw`` field). Performs HTTP and a Redis-backed credential read only. Registered as the ``gitea_get_release`` handler in this module's ``TOOLS`` list and dispatched by name from the inference worker's tool loop; not called directly elsewhere in the repo. Args: owner: Repository owner (user or organization). repo: Repository name. release_id: Numeric release id. ctx: The active ``ToolContext`` supplying user credentials. Returns: A JSON string with the release object, or a truncated raw payload when the response is not structured. """ token, base_url = await _get_credentials(ctx) data = await _gitea_request( "GET", f"/repos/{owner}/{repo}/releases/{release_id}", token, base_url ) return ( json.dumps(data) if isinstance(data, dict) else json.dumps({"raw": str(data)[:8000]}) )
[docs] async def gitea_create_release( owner: str, repo: str, tag_name: str, name: str = "", body: str = "", target_commitish: str = "", draft: bool = False, prerelease: bool = False, ctx: ToolContext | None = None, ) -> str: """Create a release for a Gitea repository from a tag. Backs the ``gitea_create_release`` tool. Resolves credentials via ``_get_credentials`` and POSTs to ``/repos/{owner}/{repo}/releases`` through ``_gitea_request`` with the tag name, draft/prerelease flags, and any supplied display name, body, and target commitish (which lets Gitea create the tag on the fly when it does not yet exist). The instance generally requires a ``write:repository`` or release scope. This mutates repository state; locally it performs HTTP and a Redis-backed credential read only. Registered as the ``gitea_create_release`` handler in this module's ``TOOLS`` list and dispatched by name from the inference worker's tool loop; not called directly elsewhere in the repo. Args: owner: Repository owner (user or organization). repo: Repository name. tag_name: Tag the release is associated with. name: Optional display name for the release; omitted when empty. body: Optional Markdown release notes; omitted when empty. target_commitish: Optional branch or SHA to tag if the tag does not yet exist; omitted when empty. draft: Whether to create the release as a draft. prerelease: Whether to mark the release as a pre-release. ctx: The active ``ToolContext`` supplying user credentials. Returns: A JSON string with the created release object, or a truncated raw payload when the response is not structured. """ token, base_url = await _get_credentials(ctx) payload: dict[str, Any] = { "tag_name": tag_name, "draft": draft, "prerelease": prerelease, } if name: payload["name"] = name if body: payload["body"] = body if target_commitish: payload["target_commitish"] = target_commitish data = await _gitea_request( "POST", f"/repos/{owner}/{repo}/releases", token, base_url, json_body=payload ) return ( json.dumps(data) if isinstance(data, dict) else json.dumps({"raw": str(data)[:4000]}) )
[docs] async def gitea_delete_release( owner: str, repo: str, release_id: int, ctx: ToolContext | None = None, ) -> str: """Delete a release from a Gitea repository by id. Backs the ``gitea_delete_release`` tool. Resolves credentials via ``_get_credentials`` and issues ``DELETE /repos/{owner}/{repo}/releases/{release_id}`` through ``_gitea_request``; on a no-content success it returns a small status object carrying the API detail. This permanently removes the release on the Gitea instance; locally it performs HTTP and a Redis-backed credential read only. Registered as the ``gitea_delete_release`` handler in this module's ``TOOLS`` list and dispatched by name from the inference worker's tool loop; not called directly elsewhere in the repo. Args: owner: Repository owner (user or organization). repo: Repository name. release_id: Numeric release id to delete. ctx: The active ``ToolContext`` supplying user credentials. Returns: A JSON string with the API response, or an ``ok`` status object when the delete returns no content. """ token, base_url = await _get_credentials(ctx) data = await _gitea_request( "DELETE", f"/repos/{owner}/{repo}/releases/{release_id}", token, base_url ) return ( json.dumps(data) if isinstance(data, dict) else json.dumps({"status": "ok", "detail": str(data)}) )
[docs] async def gitea_fork_repo( owner: str, repo: str, organization: str = "", name: str = "", ctx: ToolContext | None = None, ) -> str: """Fork a repository into the user's account or a chosen organization. Backs the ``gitea_fork_repo`` tool. Resolves credentials via ``_get_credentials`` and POSTs to ``/repos/{owner}/{repo}/forks`` through ``_gitea_request`` with an optional target organization and an optional new name (an empty payload, sent as ``None``, forks into the user's own account). On success the new fork is condensed through ``_fmt_repo``, while error responses are passed through unchanged. This mutates state on the Gitea instance; locally it performs HTTP and a Redis-backed credential read only. Registered as the ``gitea_fork_repo`` handler in this module's ``TOOLS`` list and dispatched by name from the inference worker's tool loop; not called directly elsewhere in the repo. Args: owner: Owner of the repository to fork. repo: Name of the repository to fork. organization: Optional organization to fork into instead of the user. name: Optional name for the fork; defaults to the source name. ctx: The active ``ToolContext`` supplying user credentials. Returns: A JSON string with the compact summary of the new fork, or a JSON error object on API failure. """ token, base_url = await _get_credentials(ctx) payload: dict[str, Any] = {} if organization: payload["organization"] = organization if name: payload["name"] = name data = await _gitea_request( "POST", f"/repos/{owner}/{repo}/forks", token, base_url, json_body=payload or None, ) return json.dumps( _fmt_repo(data) if isinstance(data, dict) and "error" not in data else data )
[docs] async def gitea_create_branch( owner: str, repo: str, new_branch_name: str, old_branch_name: str = "", old_ref_name: str = "", ctx: ToolContext | None = None, ) -> str: """Create a new branch from an existing branch or ref. Backs the ``gitea_create_branch`` tool. Resolves credentials via ``_get_credentials`` and POSTs to ``/repos/{owner}/{repo}/branches`` through ``_gitea_request`` with the new branch name and, when provided, a source branch (``old_branch_name``) or arbitrary source ref (``old_ref_name``); when neither is given the instance branches from the default branch. This typically requires a ``write:repository`` scope and mutates repository state; locally it performs HTTP and a Redis-backed credential read only. Registered as the ``gitea_create_branch`` handler in this module's ``TOOLS`` list and dispatched by name from the inference worker's tool loop; not called directly elsewhere in the repo. Args: owner: Repository owner (user or organization). repo: Repository name. new_branch_name: Name for the branch to create. old_branch_name: Optional source branch to branch from; omitted when empty. old_ref_name: Optional source ref to branch from, as an alternative to ``old_branch_name``; omitted when empty. ctx: The active ``ToolContext`` supplying user credentials. Returns: A JSON string with the created branch object, or a truncated raw payload when the response is not structured. """ token, base_url = await _get_credentials(ctx) payload: dict[str, Any] = {"new_branch_name": new_branch_name} if old_branch_name: payload["old_branch_name"] = old_branch_name if old_ref_name: payload["old_ref_name"] = old_ref_name data = await _gitea_request( "POST", f"/repos/{owner}/{repo}/branches", token, base_url, json_body=payload ) return ( json.dumps(data) if isinstance(data, dict) else json.dumps({"raw": str(data)[:4000]}) )
[docs] async def gitea_delete_branch( owner: str, repo: str, branch: str, ctx: ToolContext | None = None, ) -> str: """Delete a branch from a Gitea repository. Backs the ``gitea_delete_branch`` tool. Resolves credentials via ``_get_credentials``, URL-encodes the branch name (so names containing slashes survive), and issues ``DELETE /repos/{owner}/{repo}/branches/{branch}`` through ``_gitea_request``; on a no-content success it returns a small status object carrying the API detail. This typically requires a ``write:repository`` scope and removes the branch on the Gitea instance; locally it performs HTTP and a Redis-backed credential read only. Registered as the ``gitea_delete_branch`` handler in this module's ``TOOLS`` list and dispatched by name from the inference worker's tool loop; not called directly elsewhere in the repo. Args: owner: Repository owner (user or organization). repo: Repository name. branch: Name of the branch to delete. ctx: The active ``ToolContext`` supplying user credentials. Returns: A JSON string with the API response, or an ``ok`` status object when the delete returns no content. """ token, base_url = await _get_credentials(ctx) enc = quote(branch, safe="") data = await _gitea_request( "DELETE", f"/repos/{owner}/{repo}/branches/{enc}", token, base_url ) return ( json.dumps(data) if isinstance(data, dict) else json.dumps({"status": "ok", "detail": str(data)}) )
[docs] async def gitea_list_wiki_pages( owner: str, repo: str, ctx: ToolContext | None = None, ) -> str: """List the wiki pages of a Gitea repository. Backs the ``gitea_list_wiki_pages`` tool. Resolves credentials via ``_get_credentials`` and requests ``GET /repos/{owner}/{repo}/wiki/pages`` through ``_gitea_request``, returning the raw page index (wrapping a non-structured response in a truncated ``raw`` field). When the repository has its wiki disabled the underlying request yields a ``404`` error. Performs HTTP and a Redis-backed credential read only; no writes. Registered as the ``gitea_list_wiki_pages`` handler in this module's ``TOOLS`` list and dispatched by name from the inference worker's tool loop; not called directly elsewhere in the repo. Args: owner: Repository owner (user or organization). repo: Repository name. ctx: The active ``ToolContext`` supplying user credentials. Returns: A JSON string with the wiki page list, or a truncated raw payload when the response is not structured. """ token, base_url = await _get_credentials(ctx) data = await _gitea_request( "GET", f"/repos/{owner}/{repo}/wiki/pages", token, base_url ) return ( json.dumps(data) if isinstance(data, (dict, list)) else json.dumps({"raw": str(data)[:4000]}) )
[docs] async def gitea_get_wiki_page( owner: str, repo: str, page_name: str, ctx: ToolContext | None = None, ) -> str: """Fetch the content of a single wiki page by title or slug. Backs the ``gitea_get_wiki_page`` tool. Resolves credentials via ``_get_credentials``, URL-encodes the page name (so titles with spaces or slashes survive), and requests ``GET /repos/{owner}/{repo}/wiki/page/{name}`` through ``_gitea_request``, returning the raw page object (wrapping a non-JSON response in a truncated ``raw`` field). Performs HTTP and a Redis-backed credential read only. Registered as the ``gitea_get_wiki_page`` handler in this module's ``TOOLS`` list and dispatched by name from the inference worker's tool loop; not called directly elsewhere in the repo. Args: owner: Repository owner (user or organization). repo: Repository name. page_name: Wiki page title or slug to fetch. ctx: The active ``ToolContext`` supplying user credentials. Returns: A JSON string with the wiki page object, or a truncated raw payload when the response is not structured. """ token, base_url = await _get_credentials(ctx) enc = quote(page_name, safe="") data = await _gitea_request( "GET", f"/repos/{owner}/{repo}/wiki/page/{enc}", token, base_url ) return ( json.dumps(data) if isinstance(data, dict) else json.dumps({"raw": str(data)[:8000]}) )
[docs] async def gitea_list_actions_tasks( owner: str, repo: str, page: int = 1, limit: int = 20, ctx: ToolContext | None = None, ) -> str: """List Gitea Actions workflow runs (tasks) for a repository. Backs the ``gitea_list_actions_tasks`` tool. Resolves credentials via ``_get_credentials`` and pages ``GET /repos/{owner}/{repo}/actions/tasks`` through ``_gitea_request``. Because the exact response shape varies across Gitea versions, this returns the raw payload as-is (wrapping a non-dict response in a truncated ``raw`` field) rather than projecting fixed fields. Performs HTTP and a Redis-backed credential read only; no writes. Registered as the ``gitea_list_actions_tasks`` handler in this module's ``TOOLS`` list and dispatched by name from the inference worker's tool loop; not called directly elsewhere in the repo. Args: owner: Repository owner (user or organization). repo: Repository name. page: 1-based page number to fetch. limit: Results per page; values above 50 are clamped to 50. ctx: The active ``ToolContext`` supplying user credentials. Returns: A JSON string with the raw Actions tasks payload, or a truncated raw payload when the response is not structured. """ token, base_url = await _get_credentials(ctx) params = {"page": page, "limit": min(limit, 50)} data = await _gitea_request( "GET", f"/repos/{owner}/{repo}/actions/tasks", token, base_url, params=params ) return ( json.dumps(data) if isinstance(data, dict) else json.dumps({"raw": str(data)[:8000]}) )
[docs] async def gitea_list_pull_comments( owner: str, repo: str, index: int, page: int = 1, limit: int = 30, ctx: ToolContext | None = None, ) -> str: """List the comments on a pull request. Backs the ``gitea_list_pull_comments`` tool. Because Gitea exposes pull request conversation comments through the shared issue-comment thread (PRs and issues share one numbering space), this is a thin alias that simply awaits ``gitea_list_issue_comments`` with the same arguments rather than hitting a separate review-comments endpoint. All credential resolution and HTTP happen inside that delegate; this function itself performs no I/O. Registered as the ``gitea_list_pull_comments`` handler in this module's ``TOOLS`` list and dispatched by name from the inference worker's tool loop; not called directly elsewhere in the repo. Args: owner: Repository owner (user or organization). repo: Repository name. index: The pull request number. page: 1-based page number to fetch. limit: Results per page; values above 50 are clamped to 50. ctx: The active ``ToolContext`` supplying user credentials. Returns: A JSON string with a ``count`` and a ``comments`` array of compact comment summaries, or a JSON error object on API failure. """ return await gitea_list_issue_comments(owner, repo, index, page, limit, ctx=ctx)
# --------------------------------------------------------------------------- # Tool registration (multi-tool format) # --------------------------------------------------------------------------- TOOLS = [ { "name": "gitea_list_repos", "description": "List the authenticated user's Gitea repositories. Supports custom base URLs for self-hosted instances.", "parameters": { "type": "object", "properties": { "page": {"type": "integer", "description": "Page number"}, "limit": { "type": "integer", "description": "Results per page (max 50)", }, }, }, "handler": gitea_list_repos, }, { "name": "gitea_search_repos", "description": "Search Gitea repositories by keyword.", "parameters": { "type": "object", "properties": { "q": {"type": "string", "description": "Search keyword"}, "sort": { "type": "string", "enum": [ "alpha", "created", "updated", "size", "stars", "forks", "id", ], "description": "Sort order", }, "order": { "type": "string", "enum": ["asc", "desc"], "description": "Sort direction", }, "page": {"type": "integer", "description": "Page number"}, "limit": {"type": "integer", "description": "Results per page"}, }, "required": ["q"], }, "handler": gitea_search_repos, }, { "name": "gitea_get_repo", "description": "Get details of a Gitea repository.", "parameters": { "type": "object", "properties": { "owner": {"type": "string", "description": "Repository owner"}, "repo": {"type": "string", "description": "Repository name"}, }, "required": ["owner", "repo"], }, "handler": gitea_get_repo, }, { "name": "gitea_create_repo", "description": "Create a new Gitea repository for the authenticated user.", "parameters": { "type": "object", "properties": { "name": {"type": "string", "description": "Repository name"}, "description": { "type": "string", "description": "Repository description", }, "private": { "type": "boolean", "description": "Whether the repo is private", }, "auto_init": { "type": "boolean", "description": "Initialize with README", }, }, "required": ["name"], }, "handler": gitea_create_repo, }, { "name": "gitea_list_issues", "description": "List issues on a Gitea repository with optional filters.", "parameters": { "type": "object", "properties": { "owner": {"type": "string", "description": "Repository owner"}, "repo": {"type": "string", "description": "Repository name"}, "state": { "type": "string", "enum": ["open", "closed", "all"], "description": "Filter by state", }, "labels": { "type": "string", "description": "Comma-separated label names", }, "page": {"type": "integer", "description": "Page number"}, "limit": {"type": "integer", "description": "Results per page"}, }, "required": ["owner", "repo"], }, "handler": gitea_list_issues, }, { "name": "gitea_get_issue", "description": "Get a single issue by index.", "parameters": { "type": "object", "properties": { "owner": {"type": "string", "description": "Repository owner"}, "repo": {"type": "string", "description": "Repository name"}, "index": {"type": "integer", "description": "Issue number"}, }, "required": ["owner", "repo", "index"], }, "handler": gitea_get_issue, }, { "name": "gitea_create_issue", "description": "Create a new issue on a Gitea repository.", "parameters": { "type": "object", "properties": { "owner": {"type": "string", "description": "Repository owner"}, "repo": {"type": "string", "description": "Repository name"}, "title": {"type": "string", "description": "Issue title"}, "body": {"type": "string", "description": "Issue body (Markdown)"}, "labels": { "type": "array", "items": {"type": "integer"}, "description": "Label IDs", }, "assignees": { "type": "array", "items": {"type": "string"}, "description": "Assignee usernames", }, }, "required": ["owner", "repo", "title"], }, "handler": gitea_create_issue, }, { "name": "gitea_update_issue", "description": "Update an existing issue (title, body, state).", "parameters": { "type": "object", "properties": { "owner": {"type": "string", "description": "Repository owner"}, "repo": {"type": "string", "description": "Repository name"}, "index": {"type": "integer", "description": "Issue number"}, "title": {"type": "string", "description": "New title"}, "body": {"type": "string", "description": "New body"}, "state": { "type": "string", "enum": ["open", "closed"], "description": "New state", }, }, "required": ["owner", "repo", "index"], }, "handler": gitea_update_issue, }, { "name": "gitea_list_pull_requests", "description": "List pull requests on a Gitea repository.", "parameters": { "type": "object", "properties": { "owner": {"type": "string", "description": "Repository owner"}, "repo": {"type": "string", "description": "Repository name"}, "state": { "type": "string", "enum": ["open", "closed", "all"], "description": "Filter by state", }, "page": {"type": "integer", "description": "Page number"}, "limit": {"type": "integer", "description": "Results per page"}, }, "required": ["owner", "repo"], }, "handler": gitea_list_pull_requests, }, { "name": "gitea_get_pull_request", "description": "Get details of a specific pull request.", "parameters": { "type": "object", "properties": { "owner": {"type": "string", "description": "Repository owner"}, "repo": {"type": "string", "description": "Repository name"}, "index": {"type": "integer", "description": "Pull request number"}, }, "required": ["owner", "repo", "index"], }, "handler": gitea_get_pull_request, }, { "name": "gitea_create_pull_request", "description": "Create a new pull request.", "parameters": { "type": "object", "properties": { "owner": {"type": "string", "description": "Repository owner"}, "repo": {"type": "string", "description": "Repository name"}, "title": {"type": "string", "description": "PR title"}, "head": { "type": "string", "description": "Head branch (or owner:branch for forks)", }, "base": { "type": "string", "description": "Base branch (default: main)", }, "body": {"type": "string", "description": "PR body (Markdown)"}, }, "required": ["owner", "repo", "title", "head"], }, "handler": gitea_create_pull_request, }, { "name": "gitea_merge_pull_request", "description": "Merge a pull request. merge_style: merge, rebase, rebase-merge, squash.", "parameters": { "type": "object", "properties": { "owner": {"type": "string", "description": "Repository owner"}, "repo": {"type": "string", "description": "Repository name"}, "index": {"type": "integer", "description": "Pull request number"}, "merge_style": { "type": "string", "enum": ["merge", "rebase", "rebase-merge", "squash"], "description": "Merge method", }, "delete_branch_after_merge": { "type": "boolean", "description": "Delete head branch after merge", }, }, "required": ["owner", "repo", "index"], }, "handler": gitea_merge_pull_request, }, { "name": "gitea_get_file", "description": "Get the contents of a file or directory listing from a Gitea repository.", "parameters": { "type": "object", "properties": { "owner": {"type": "string", "description": "Repository owner"}, "repo": {"type": "string", "description": "Repository name"}, "path": {"type": "string", "description": "File or directory path"}, "ref": {"type": "string", "description": "Branch, tag, or commit SHA"}, }, "required": ["owner", "repo", "path"], }, "handler": gitea_get_file, }, { "name": "gitea_list_commits", "description": "List commits in a Gitea repository.", "parameters": { "type": "object", "properties": { "owner": {"type": "string", "description": "Repository owner"}, "repo": {"type": "string", "description": "Repository name"}, "sha": {"type": "string", "description": "Branch or SHA to list from"}, "page": {"type": "integer", "description": "Page number"}, "limit": {"type": "integer", "description": "Results per page"}, }, "required": ["owner", "repo"], }, "handler": gitea_list_commits, }, { "name": "gitea_get_commit", "description": "Get a single commit by SHA.", "parameters": { "type": "object", "properties": { "owner": {"type": "string", "description": "Repository owner"}, "repo": {"type": "string", "description": "Repository name"}, "sha": {"type": "string", "description": "Commit SHA or ref"}, }, "required": ["owner", "repo", "sha"], }, "handler": gitea_get_commit, }, { "name": "gitea_list_notifications", "description": "List the user's Gitea notifications (issues, PRs, etc.).", "parameters": { "type": "object", "properties": { "all_notifications": { "type": "boolean", "description": "Include read notifications", }, "page": {"type": "integer", "description": "Page number"}, "limit": {"type": "integer", "description": "Results per page"}, }, }, "handler": gitea_list_notifications, }, { "name": "gitea_star_repo", "description": "Star or unstar a Gitea repository.", "parameters": { "type": "object", "properties": { "owner": {"type": "string", "description": "Repository owner"}, "repo": {"type": "string", "description": "Repository name"}, "star": { "type": "boolean", "description": "True to star, false to unstar", }, }, "required": ["owner", "repo"], }, "handler": gitea_star_repo, }, { "name": "gitea_list_branches", "description": "List branches in a Gitea repository.", "parameters": { "type": "object", "properties": { "owner": {"type": "string", "description": "Repository owner"}, "repo": {"type": "string", "description": "Repository name"}, "page": {"type": "integer", "description": "Page number"}, "limit": {"type": "integer", "description": "Results per page"}, }, "required": ["owner", "repo"], }, "handler": gitea_list_branches, }, { "name": "gitea_create_repo_from_template", "description": "Create a new repository from a template repository.", "parameters": { "type": "object", "properties": { "template_owner": { "type": "string", "description": "Template repo owner", }, "template_repo": { "type": "string", "description": "Template repo name", }, "name": {"type": "string", "description": "Name for the new repo"}, "description": {"type": "string", "description": "Description"}, "private": {"type": "boolean", "description": "Whether private"}, "git_content": { "type": "boolean", "description": "Include git content from template", }, }, "required": ["template_owner", "template_repo", "name"], }, "handler": gitea_create_repo_from_template, }, { "name": "gitea_compare_commits", "description": "Compare two refs or commits (e.g. main...feature) and list commits/files.", "parameters": { "type": "object", "properties": { "owner": {"type": "string", "description": "Repository owner"}, "repo": {"type": "string", "description": "Repository name"}, "basehead": { "type": "string", "description": "base...head (Gitea compare syntax)", }, }, "required": ["owner", "repo", "basehead"], }, "handler": gitea_compare_commits, }, { "name": "gitea_list_tags", "description": "List tags in a Gitea repository.", "parameters": { "type": "object", "properties": { "owner": {"type": "string"}, "repo": {"type": "string"}, "page": {"type": "integer"}, "limit": {"type": "integer"}, }, "required": ["owner", "repo"], }, "handler": gitea_list_tags, }, { "name": "gitea_create_tag", "description": "Create a tag on a ref (requires write:repository on the instance).", "parameters": { "type": "object", "properties": { "owner": {"type": "string"}, "repo": {"type": "string"}, "tag_name": {"type": "string"}, "target": {"type": "string", "description": "Branch or commit SHA"}, "message": { "type": "string", "description": "Optional annotated tag message", }, }, "required": ["owner", "repo", "tag_name", "target"], }, "handler": gitea_create_tag, }, { "name": "gitea_list_milestones", "description": "List milestones (requires read:issue on many Gitea servers).", "parameters": { "type": "object", "properties": { "owner": {"type": "string"}, "repo": {"type": "string"}, "state": {"type": "string", "enum": ["open", "closed", "all"]}, "page": {"type": "integer"}, "limit": {"type": "integer"}, }, "required": ["owner", "repo"], }, "handler": gitea_list_milestones, }, { "name": "gitea_get_milestone", "description": "Get a milestone by id.", "parameters": { "type": "object", "properties": { "owner": {"type": "string"}, "repo": {"type": "string"}, "milestone_id": {"type": "integer"}, }, "required": ["owner", "repo", "milestone_id"], }, "handler": gitea_get_milestone, }, { "name": "gitea_create_milestone", "description": "Create a milestone.", "parameters": { "type": "object", "properties": { "owner": {"type": "string"}, "repo": {"type": "string"}, "title": {"type": "string"}, "description": {"type": "string"}, "due_on": {"type": "string", "description": "ISO 8601 datetime"}, }, "required": ["owner", "repo", "title"], }, "handler": gitea_create_milestone, }, { "name": "gitea_update_milestone", "description": "Update milestone fields.", "parameters": { "type": "object", "properties": { "owner": {"type": "string"}, "repo": {"type": "string"}, "milestone_id": {"type": "integer"}, "title": {"type": "string"}, "description": {"type": "string"}, "state": {"type": "string", "enum": ["open", "closed"]}, "due_on": {"type": "string"}, }, "required": ["owner", "repo", "milestone_id"], }, "handler": gitea_update_milestone, }, { "name": "gitea_delete_milestone", "description": "Delete a milestone by id.", "parameters": { "type": "object", "properties": { "owner": {"type": "string"}, "repo": {"type": "string"}, "milestone_id": {"type": "integer"}, }, "required": ["owner", "repo", "milestone_id"], }, "handler": gitea_delete_milestone, }, { "name": "gitea_list_labels", "description": "List repository labels (requires read:issue on many servers).", "parameters": { "type": "object", "properties": { "owner": {"type": "string"}, "repo": {"type": "string"}, "page": {"type": "integer"}, "limit": {"type": "integer"}, }, "required": ["owner", "repo"], }, "handler": gitea_list_labels, }, { "name": "gitea_create_label", "description": "Create a label (write:issue typically).", "parameters": { "type": "object", "properties": { "owner": {"type": "string"}, "repo": {"type": "string"}, "name": {"type": "string"}, "color": {"type": "string", "description": "Hex color without #"}, "description": {"type": "string"}, }, "required": ["owner", "repo", "name", "color"], }, "handler": gitea_create_label, }, { "name": "gitea_list_issue_comments", "description": "List comments on an issue or PR (same index in Gitea).", "parameters": { "type": "object", "properties": { "owner": {"type": "string"}, "repo": {"type": "string"}, "index": {"type": "integer"}, "page": {"type": "integer"}, "limit": {"type": "integer"}, }, "required": ["owner", "repo", "index"], }, "handler": gitea_list_issue_comments, }, { "name": "gitea_create_issue_comment", "description": "Post a comment on an issue or PR.", "parameters": { "type": "object", "properties": { "owner": {"type": "string"}, "repo": {"type": "string"}, "index": {"type": "integer"}, "body": {"type": "string"}, }, "required": ["owner", "repo", "index", "body"], }, "handler": gitea_create_issue_comment, }, { "name": "gitea_list_pull_comments", "description": "Alias of issue comments listing for pull request threads.", "parameters": { "type": "object", "properties": { "owner": {"type": "string"}, "repo": {"type": "string"}, "index": {"type": "integer", "description": "Pull request number"}, "page": {"type": "integer"}, "limit": {"type": "integer"}, }, "required": ["owner", "repo", "index"], }, "handler": gitea_list_pull_comments, }, { "name": "gitea_list_releases", "description": "List releases for a repository.", "parameters": { "type": "object", "properties": { "owner": {"type": "string"}, "repo": {"type": "string"}, "draft": {"type": "boolean"}, "pre_release": {"type": "boolean"}, "page": {"type": "integer"}, "limit": {"type": "integer"}, }, "required": ["owner", "repo"], }, "handler": gitea_list_releases, }, { "name": "gitea_get_release", "description": "Get a release by numeric id.", "parameters": { "type": "object", "properties": { "owner": {"type": "string"}, "repo": {"type": "string"}, "release_id": {"type": "integer"}, }, "required": ["owner", "repo", "release_id"], }, "handler": gitea_get_release, }, { "name": "gitea_create_release", "description": "Create a release from a tag (requires write permissions).", "parameters": { "type": "object", "properties": { "owner": {"type": "string"}, "repo": {"type": "string"}, "tag_name": {"type": "string"}, "name": {"type": "string"}, "body": {"type": "string"}, "target_commitish": {"type": "string"}, "draft": {"type": "boolean"}, "prerelease": {"type": "boolean"}, }, "required": ["owner", "repo", "tag_name"], }, "handler": gitea_create_release, }, { "name": "gitea_delete_release", "description": "Delete a release by id.", "parameters": { "type": "object", "properties": { "owner": {"type": "string"}, "repo": {"type": "string"}, "release_id": {"type": "integer"}, }, "required": ["owner", "repo", "release_id"], }, "handler": gitea_delete_release, }, { "name": "gitea_fork_repo", "description": "Fork a repository into the user account or an organization.", "parameters": { "type": "object", "properties": { "owner": {"type": "string"}, "repo": {"type": "string"}, "organization": { "type": "string", "description": "Optional org to fork into", }, "name": {"type": "string", "description": "Optional new repo name"}, }, "required": ["owner", "repo"], }, "handler": gitea_fork_repo, }, { "name": "gitea_create_branch", "description": "Create a new branch from an existing branch or ref (write:repository).", "parameters": { "type": "object", "properties": { "owner": {"type": "string"}, "repo": {"type": "string"}, "new_branch_name": {"type": "string"}, "old_branch_name": {"type": "string"}, "old_ref_name": { "type": "string", "description": "Alternative to old_branch_name", }, }, "required": ["owner", "repo", "new_branch_name"], }, "handler": gitea_create_branch, }, { "name": "gitea_delete_branch", "description": "Delete a branch (write:repository).", "parameters": { "type": "object", "properties": { "owner": {"type": "string"}, "repo": {"type": "string"}, "branch": {"type": "string"}, }, "required": ["owner", "repo", "branch"], }, "handler": gitea_delete_branch, }, { "name": "gitea_list_wiki_pages", "description": "List wiki pages (404 if wiki disabled).", "parameters": { "type": "object", "properties": { "owner": {"type": "string"}, "repo": {"type": "string"}, }, "required": ["owner", "repo"], }, "handler": gitea_list_wiki_pages, }, { "name": "gitea_get_wiki_page", "description": "Get wiki page content by page name.", "parameters": { "type": "object", "properties": { "owner": {"type": "string"}, "repo": {"type": "string"}, "page_name": {"type": "string"}, }, "required": ["owner", "repo", "page_name"], }, "handler": gitea_get_wiki_page, }, { "name": "gitea_list_actions_tasks", "description": "List Gitea Actions workflow runs for a repository.", "parameters": { "type": "object", "properties": { "owner": {"type": "string"}, "repo": {"type": "string"}, "page": {"type": "integer"}, "limit": {"type": "integer"}, }, "required": ["owner", "repo"], }, "handler": gitea_list_actions_tasks, }, ]