feat: consolidated Gitea API client and pi extension
- Pure fetch-based API client (src/) with zero external dependencies - Pi extension adapter (pi-extension/) registering 17 tools - Standalone CLI (cli.ts) replacing gitea-scripts/gitea.js - Token auth everywhere (no HMAC secrets) - SKILL.md for agent auto-discovery - TOOL.md with full parameter reference Consolidates pi-bot/extensions/pi-gitea and clawbot/gitea-scripts into a single shared package.
This commit is contained in:
233
pi-extension/tools/read-tools.ts
Normal file
233
pi-extension/tools/read-tools.ts
Normal file
@@ -0,0 +1,233 @@
|
||||
/**
|
||||
* 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 },
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user