/** * Read-only tools — list/get operations */ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"; import { Type } from "@sinclair/typebox"; import { GiteaClient } from "../../src/client.js"; import * as repos from "../../src/repos.js"; import * as issues from "../../src/issues.js"; import * as pulls from "../../src/pulls.js"; import * as actions from "../../src/actions.js"; const client = new GiteaClient(); export default function (pi: ExtensionAPI) { // ── Repositories ── pi.registerTool({ name: "gitea_list_repos", label: "Gitea: List Repositories", description: "List repositories. Defaults to repositories accessible to the authenticated user (you). Can specify an owner to list their public repositories.", parameters: Type.Object({ owner: Type.Optional(Type.String({ description: "Owner to list repos for. Omit for your own repos." })), limit: Type.Optional(Type.Number({ description: "Max results (default: 50)" })), }), async execute(_id, params) { const result = await repos.listRepos(client, { owner: params.owner, limit: params.limit }); const lines = result.map((r) => { const vis = r.private ? "🔒" : "🌐"; const updated = r.updated_at?.split("T")[0] ?? "unknown"; return `${vis} ${r.full_name} — ⭐ ${r.stars_count ?? 0} | Updated: ${updated}`; }); return { content: [{ type: "text", text: lines.join("\n") || "No repositories found." }], details: { repos: result, count: result.length }, }; }, }); // ── Issues ── pi.registerTool({ name: "gitea_list_issues", label: "Gitea: List Issues", description: "List issues for a repository.", parameters: Type.Object({ owner: Type.Optional(Type.String({ description: `Repo owner (default: ${client.defaultOwner})` })), repo: Type.Optional(Type.String({ description: `Repo name (default: ${client.defaultRepo})` })), state: Type.Optional( Type.Union([Type.Literal("open"), Type.Literal("closed"), Type.Literal("all")], { description: "Issue state (default: open)", }), ), limit: Type.Optional(Type.Number({ description: "Max results (default: 20)" })), }), async execute(_id, params) { const { owner, repo } = client.resolve(params.owner, params.repo); const result = await issues.listIssues(client, owner, repo, { state: params.state as "open" | "closed" | "all", limit: params.limit, }); const lines = result.map((i) => `#${i.number} [${i.state}] ${i.title} (@${i.user?.login ?? "?"})`); return { content: [{ type: "text", text: lines.join("\n") || "No issues found." }], details: { issues: result }, }; }, }); pi.registerTool({ name: "gitea_get_issue", label: "Gitea: Get Issue", description: "Get an issue by number, including all comments.", parameters: Type.Object({ index: Type.Number({ description: "Issue number" }), owner: Type.Optional(Type.String({ description: `Repo owner (default: ${client.defaultOwner})` })), repo: Type.Optional(Type.String({ description: `Repo name (default: ${client.defaultRepo})` })), }), async execute(_id, params) { const { owner, repo } = client.resolve(params.owner, params.repo); const [issue, comments] = await Promise.all([ issues.getIssue(client, owner, repo, params.index), issues.getIssueComments(client, owner, repo, params.index), ]); let text = `# Issue #${issue.number}: ${issue.title}\n`; text += `State: ${issue.state} | Author: @${issue.user?.login ?? "?"} | Created: ${issue.created_at}\n`; if (issue.labels?.length) text += `Labels: ${issue.labels.map((l) => l.name).join(", ")}\n`; text += `\n${issue.body ?? "(no body)"}\n`; if (comments.length > 0) { text += `\n--- ${comments.length} comment(s) ---\n`; for (const c of comments) { text += `\n@${c.user?.login ?? "?"} (${c.created_at}):\n${c.body}\n`; } } return { content: [{ type: "text", text }], details: { issue, comments }, }; }, }); pi.registerTool({ name: "gitea_create_issue_comment", label: "Gitea: Comment on Issue", description: "Post a comment on an issue or pull request.", parameters: Type.Object({ index: Type.Number({ description: "Issue/PR number" }), body: Type.String({ description: "Comment body (Markdown)" }), owner: Type.Optional(Type.String({ description: `Repo owner (default: ${client.defaultOwner})` })), repo: Type.Optional(Type.String({ description: `Repo name (default: ${client.defaultRepo})` })), }), async execute(_id, params) { const { owner, repo } = client.resolve(params.owner, params.repo); const comment = await issues.createIssueComment(client, owner, repo, params.index, params.body); return { content: [{ type: "text", text: `Comment posted: ${comment.html_url}` }], details: { comment }, }; }, }); // ── Pull Requests ── pi.registerTool({ name: "gitea_list_prs", label: "Gitea: List PRs", description: "List pull requests for a repository.", parameters: Type.Object({ owner: Type.Optional(Type.String({ description: `Repo owner (default: ${client.defaultOwner})` })), repo: Type.Optional(Type.String({ description: `Repo name (default: ${client.defaultRepo})` })), state: Type.Optional( Type.Union([Type.Literal("open"), Type.Literal("closed"), Type.Literal("all")], { description: "PR state (default: open)", }), ), }), async execute(_id, params) { const { owner, repo } = client.resolve(params.owner, params.repo); const result = await pulls.listPullRequests(client, owner, repo, { state: params.state as any }); const lines = result.map( (p) => `#${p.number} [${p.state}] ${p.title} (@${p.user?.login ?? "?"}) ${p.head?.label ?? ""} → ${p.base?.label ?? ""}`, ); return { content: [{ type: "text", text: lines.join("\n") || "No PRs found." }], details: { prs: result }, }; }, }); pi.registerTool({ name: "gitea_get_pr", label: "Gitea: Get PR", description: "Get a pull request by number, including comments.", parameters: Type.Object({ index: Type.Number({ description: "PR number" }), owner: Type.Optional(Type.String({ description: `Repo owner (default: ${client.defaultOwner})` })), repo: Type.Optional(Type.String({ description: `Repo name (default: ${client.defaultRepo})` })), }), async execute(_id, params) { const { owner, repo } = client.resolve(params.owner, params.repo); const [pr, comments] = await Promise.all([ pulls.getPullRequest(client, owner, repo, params.index), issues.getIssueComments(client, owner, repo, params.index), ]); let text = `# PR #${pr.number}: ${pr.title}\n`; text += `State: ${pr.state} | Author: @${pr.user?.login ?? "?"} | Created: ${pr.created_at}\n`; text += `Base: ${pr.base?.label ?? "?"} ← Head: ${pr.head?.label ?? "?"}\n`; if (pr.merged) text += `Merged: ${pr.merged_at}\n`; text += `\n${pr.body ?? "(no body)"}\n`; if (comments.length > 0) { text += `\n--- ${comments.length} comment(s) ---\n`; for (const c of comments) { text += `\n@${c.user?.login ?? "?"} (${c.created_at}):\n${c.body}\n`; } } return { content: [{ type: "text", text }], details: { pr, comments }, }; }, }); // ── Actions / CI ── pi.registerTool({ name: "gitea_list_runs", label: "Gitea: List Runs", description: "List recent workflow runs for a repository.", parameters: Type.Object({ owner: Type.Optional(Type.String({ description: `Repo owner (default: ${client.defaultOwner})` })), repo: Type.Optional(Type.String({ description: `Repo name (default: ${client.defaultRepo})` })), limit: Type.Optional(Type.Number({ description: "Max results (default: 10)" })), }), async execute(_id, params) { const { owner, repo } = client.resolve(params.owner, params.repo); const { runs, total } = await actions.listRuns(client, owner, repo, { limit: params.limit }); const lines = runs.map( (r) => `#${r.run_number} [${r.id}] ${r.conclusion ?? r.status} — ${r.display_title} (${r.head_branch} / ${r.event}) ${r.completed_at ?? r.started_at}`, ); return { content: [{ type: "text", text: lines.join("\n") || "No runs found." }], details: { runs, total }, }; }, }); pi.registerTool({ name: "gitea_get_run_logs", label: "Gitea: Get Run Logs", description: "Get full log output for a workflow run. Fetches all jobs then downloads each job's logs.", parameters: Type.Object({ run_id: Type.Number({ description: "Workflow run ID (the numeric id, not run_number)" }), owner: Type.Optional(Type.String({ description: `Repo owner (default: ${client.defaultOwner})` })), repo: Type.Optional(Type.String({ description: `Repo name (default: ${client.defaultRepo})` })), }), async execute(_id, params) { const { owner, repo } = client.resolve(params.owner, params.repo); const { jobs, logs } = await actions.getRunLogs(client, owner, repo, params.run_id); return { content: [{ type: "text", text: logs }], details: { runId: params.run_id, jobs }, }; }, }); }