One module answers 'did my change go out' for a commit and orders a cross-repository change: one compiled state table, its state on the bus, every transition said, noted on the commit and shown on the pull request. The forge's holder gains the note, view and status tools it asks with, and says closed pull requests and a merge's head and statuses.
535 lines
26 KiB
TypeScript
535 lines
26 KiB
TypeScript
// gitea's tools — moved here from the shared sdk (novox/hq ADR 0039), importing gitea's own client.
|
|
// They return structured data; the mesh serves them through the sdk's tool harness.
|
|
//
|
|
// One tool emits an event at the natural point of the action it takes (novox/hq ADR 0041/0042):
|
|
// create-issue emits issue.opened. pull.merged and repo.created are deliberately NOT emitted here: a
|
|
// merge or a repository is as often made in the web UI or by a plain API call as by these tools, so the
|
|
// events entrypoint (index.ts) owns both by polling, which catches every path. Announcing a merge here
|
|
// as well announced every merge made through this tool twice — the tool's at once, the poll's moments
|
|
// later (novox/hq issue 250).
|
|
|
|
import { registerModuleTools, type ToolDefinition } from "@novox/mesh-sdk/tools";
|
|
import { emit } from "@novox/mesh-sdk/events";
|
|
import { GiteaClient, type BranchProtection } from "../client.js";
|
|
import { appendNote, deliveryStatus, run, viewBody, viewComment } from "../delivery.js";
|
|
|
|
/** The forge's container, where its repositories and git are: the note is written there (novox/hq ADR 0239). */
|
|
const forgeContainer = process.env.MESH_GITEA_CONTAINER || "gitea";
|
|
|
|
/** A protection rule as a person reads it: what it guards, not every field the forge keeps. */
|
|
export function summarised(p: BranchProtection) {
|
|
return {
|
|
rule: p.rule_name ?? p.branch_name,
|
|
push: p.enable_push ?? false,
|
|
required_statuses: p.enable_status_check ? (p.status_check_contexts ?? []) : [],
|
|
required_approvals: p.required_approvals ?? 0,
|
|
admin_may_override: !(p.block_admin_merge_override ?? false),
|
|
};
|
|
}
|
|
|
|
/** Coerce a comma-separated label string into names; empty/absent yields none. */
|
|
function parseLabels(raw: unknown): string[] {
|
|
if (raw === undefined || raw === null || raw === "") return [];
|
|
return String(raw)
|
|
.split(",")
|
|
.map((s) => s.trim())
|
|
.filter(Boolean);
|
|
}
|
|
|
|
export function getGiteaTools(gitea: GiteaClient): ToolDefinition[] {
|
|
return [
|
|
// ---- Repositories ----
|
|
{
|
|
name: "gitea_list_repos",
|
|
description: "List repositories for the authenticated Gitea user.",
|
|
input: {
|
|
page: { type: "number", description: "page number (default 1)" },
|
|
limit: { type: "number", description: "how many per page (default 20)" },
|
|
},
|
|
run: async (args) => ({
|
|
repos: await gitea.listRepos(args.page ? Number(args.page) : 1, args.limit ? Number(args.limit) : 20),
|
|
}),
|
|
},
|
|
{
|
|
name: "gitea_create_repo",
|
|
description: "Create a repository owned by the authenticated user.",
|
|
input: {
|
|
name: { type: "string", description: "the repository name" },
|
|
description: { type: "string", description: "an optional description" },
|
|
private: { type: "boolean", description: "private repo (default true)" },
|
|
auto_init: { type: "boolean", description: "initialise with a README (default true)" },
|
|
},
|
|
run: async (args) => {
|
|
const repo = await gitea.createRepo({
|
|
name: String(args.name),
|
|
description: args.description ? String(args.description) : undefined,
|
|
private: args.private === undefined ? true : Boolean(args.private),
|
|
auto_init: args.auto_init === undefined ? true : Boolean(args.auto_init),
|
|
});
|
|
return { repo };
|
|
},
|
|
},
|
|
{
|
|
name: "gitea_delete_repo",
|
|
description: "Delete a repository. Destructive and irreversible — requires confirm=true.",
|
|
input: {
|
|
owner: { type: "string", description: "the repository owner" },
|
|
name: { type: "string", description: "the repository name" },
|
|
confirm: { type: "boolean", description: "must be true to actually delete" },
|
|
},
|
|
run: async (args) => {
|
|
if (!args.confirm) return { deleted: false, reason: "confirm must be true to delete a repository" };
|
|
await gitea.deleteRepo(String(args.owner), String(args.name));
|
|
return { deleted: true, repo: `${String(args.owner)}/${String(args.name)}` };
|
|
},
|
|
},
|
|
|
|
// ---- Issues ----
|
|
{
|
|
name: "gitea_list_issues",
|
|
description: "List issues for a repository, filterable by state and labels.",
|
|
input: {
|
|
owner: { type: "string", description: "the repository owner" },
|
|
repo: { type: "string", description: "the repository name" },
|
|
state: { type: "string", description: "open | closed | all (default open)" },
|
|
labels: { type: "string", description: "comma-separated label names to filter by" },
|
|
page: { type: "number", description: "page number (default 1)" },
|
|
},
|
|
run: async (args) => {
|
|
const params: Record<string, string> = {
|
|
state: args.state ? String(args.state) : "open",
|
|
page: String(args.page ? Number(args.page) : 1),
|
|
};
|
|
if (args.labels) params.labels = String(args.labels);
|
|
return { issues: await gitea.listIssues(String(args.owner), String(args.repo), params) };
|
|
},
|
|
},
|
|
{
|
|
name: "gitea_get_issue",
|
|
description: "Get a single issue by its number.",
|
|
input: {
|
|
owner: { type: "string", description: "the repository owner" },
|
|
repo: { type: "string", description: "the repository name" },
|
|
number: { type: "number", description: "the issue number" },
|
|
},
|
|
run: async (args) => ({
|
|
issue: await gitea.getIssue(String(args.owner), String(args.repo), Number(args.number)),
|
|
}),
|
|
},
|
|
{
|
|
name: "gitea_create_issue",
|
|
description: "Open a new issue. Label names are resolved to ids, creating any that are missing.",
|
|
input: {
|
|
owner: { type: "string", description: "the repository owner" },
|
|
repo: { type: "string", description: "the repository name" },
|
|
title: { type: "string", description: "the issue title" },
|
|
body: { type: "string", description: "the issue body (markdown)" },
|
|
labels: { type: "string", description: "comma-separated label names" },
|
|
},
|
|
run: async (args) => {
|
|
const owner = String(args.owner);
|
|
const repo = String(args.repo);
|
|
const names = parseLabels(args.labels);
|
|
const labelIds = names.length
|
|
? await Promise.all(names.map((n) => gitea.getOrCreateLabel(owner, repo, n)))
|
|
: undefined;
|
|
const issue = await gitea.createIssue(owner, repo, {
|
|
title: String(args.title),
|
|
body: args.body ? String(args.body) : undefined,
|
|
labels: labelIds,
|
|
});
|
|
// The mesh just opened an issue — announce it the moment it exists.
|
|
await emit("issue.opened", {
|
|
owner,
|
|
repo,
|
|
number: issue.number,
|
|
title: issue.title,
|
|
user: issue.user,
|
|
html_url: issue.html_url,
|
|
});
|
|
return { issue };
|
|
},
|
|
},
|
|
{
|
|
name: "gitea_close_issue",
|
|
description: "Close an open issue.",
|
|
input: {
|
|
owner: { type: "string", description: "the repository owner" },
|
|
repo: { type: "string", description: "the repository name" },
|
|
number: { type: "number", description: "the issue number" },
|
|
},
|
|
run: async (args) => ({
|
|
issue: await gitea.setIssueState(String(args.owner), String(args.repo), Number(args.number), "closed"),
|
|
}),
|
|
},
|
|
{
|
|
name: "gitea_add_comment",
|
|
description: "Add a comment to an issue or pull request.",
|
|
input: {
|
|
owner: { type: "string", description: "the repository owner" },
|
|
repo: { type: "string", description: "the repository name" },
|
|
number: { type: "number", description: "the issue or PR number" },
|
|
body: { type: "string", description: "the comment body (markdown)" },
|
|
},
|
|
run: async (args) => ({
|
|
comment: await gitea.addComment(String(args.owner), String(args.repo), Number(args.number), String(args.body)),
|
|
}),
|
|
},
|
|
|
|
// ---- Pull requests ----
|
|
{
|
|
name: "gitea_list_pull_requests",
|
|
description: "List pull requests for a repository.",
|
|
input: {
|
|
owner: { type: "string", description: "the repository owner" },
|
|
repo: { type: "string", description: "the repository name" },
|
|
state: { type: "string", description: "open | closed | all (default open)" },
|
|
page: { type: "number", description: "page number (default 1)" },
|
|
limit: { type: "number", description: "how many per page (default 20)" },
|
|
},
|
|
run: async (args) => ({
|
|
pulls: await gitea.listPullRequests(String(args.owner), String(args.repo), {
|
|
state: args.state ? String(args.state) : "open",
|
|
page: String(args.page ? Number(args.page) : 1),
|
|
limit: String(args.limit ? Number(args.limit) : 20),
|
|
}),
|
|
}),
|
|
},
|
|
{
|
|
name: "gitea_get_pull_request",
|
|
description: "Get a single pull request by its number.",
|
|
input: {
|
|
owner: { type: "string", description: "the repository owner" },
|
|
repo: { type: "string", description: "the repository name" },
|
|
number: { type: "number", description: "the PR number" },
|
|
},
|
|
run: async (args) => ({
|
|
pull: await gitea.getPullRequest(String(args.owner), String(args.repo), Number(args.number)),
|
|
}),
|
|
},
|
|
{
|
|
name: "gitea_create_pull_request",
|
|
description: "Open a pull request from a head branch into a base branch.",
|
|
input: {
|
|
owner: { type: "string", description: "the repository owner" },
|
|
repo: { type: "string", description: "the repository name" },
|
|
title: { type: "string", description: "the PR title" },
|
|
body: { type: "string", description: "the PR body (markdown)" },
|
|
head: { type: "string", description: "the source branch" },
|
|
base: { type: "string", description: "the target branch (default main)" },
|
|
},
|
|
run: async (args) => ({
|
|
pull: await gitea.createPullRequest(String(args.owner), String(args.repo), {
|
|
title: String(args.title),
|
|
body: args.body ? String(args.body) : undefined,
|
|
head: String(args.head),
|
|
base: args.base ? String(args.base) : "main",
|
|
}),
|
|
}),
|
|
},
|
|
{
|
|
name: "gitea_merge_pull_request",
|
|
description: "Merge a pull request, optionally deleting the source branch afterwards.",
|
|
input: {
|
|
owner: { type: "string", description: "the repository owner" },
|
|
repo: { type: "string", description: "the repository name" },
|
|
number: { type: "number", description: "the PR number" },
|
|
method: { type: "string", description: "merge | rebase | squash (default merge)" },
|
|
delete_branch: { type: "boolean", description: "delete the source branch after merge (default true)" },
|
|
},
|
|
run: async (args) => {
|
|
const owner = String(args.owner);
|
|
const repo = String(args.repo);
|
|
const number = Number(args.number);
|
|
const method = args.method ? String(args.method) : "merge";
|
|
const deleteBranch = args.delete_branch === undefined ? true : Boolean(args.delete_branch);
|
|
await gitea.mergePullRequest(owner, repo, number, method, deleteBranch);
|
|
// The merge commit only exists now; answered so the caller can follow what is built from it.
|
|
// pull.merged is the events entrypoint's to announce (index.ts), once, within its poll.
|
|
const merged = await gitea.getPullRequest(owner, repo, number);
|
|
return { merged: true, number, method, deleted_branch: deleteBranch, merge_commit_sha: merged.merge_commit_sha };
|
|
},
|
|
},
|
|
|
|
{
|
|
name: "gitea_close_pull_request",
|
|
description: "Close a pull request without merging it — one whose work landed elsewhere, or was abandoned.",
|
|
input: {
|
|
owner: { type: "string", description: "the repository owner" },
|
|
repo: { type: "string", description: "the repository name" },
|
|
number: { type: "number", description: "the PR number" },
|
|
},
|
|
run: async (args) => ({
|
|
pull: await gitea.setPullState(String(args.owner), String(args.repo), Number(args.number), "closed"),
|
|
}),
|
|
},
|
|
{
|
|
name: "gitea_reopen_pull_request",
|
|
description: "Reopen a closed, unmerged pull request.",
|
|
input: {
|
|
owner: { type: "string", description: "the repository owner" },
|
|
repo: { type: "string", description: "the repository name" },
|
|
number: { type: "number", description: "the PR number" },
|
|
},
|
|
run: async (args) => ({
|
|
pull: await gitea.setPullState(String(args.owner), String(args.repo), Number(args.number), "open"),
|
|
}),
|
|
},
|
|
{
|
|
name: "gitea_update_pull_request",
|
|
description: "Change a pull request's title or body; a field not given is left as it is.",
|
|
input: {
|
|
owner: { type: "string", description: "the repository owner" },
|
|
repo: { type: "string", description: "the repository name" },
|
|
number: { type: "number", description: "the PR number" },
|
|
title: { type: "string", description: "the new title (optional)" },
|
|
body: { type: "string", description: "the new body, markdown (optional)" },
|
|
},
|
|
run: async (args) => ({
|
|
pull: await gitea.updatePullRequest(String(args.owner), String(args.repo), Number(args.number), {
|
|
title: args.title === undefined ? undefined : String(args.title),
|
|
body: args.body === undefined ? undefined : String(args.body),
|
|
}),
|
|
}),
|
|
},
|
|
{
|
|
name: "gitea_pull_request_files",
|
|
description: "The files a pull request changes, as paths from the repository's root (up to 100; says when there are more).",
|
|
input: {
|
|
owner: { type: "string", description: "the repository owner" },
|
|
repo: { type: "string", description: "the repository name" },
|
|
number: { type: "number", description: "the PR number" },
|
|
},
|
|
run: async (args) => gitea.listPullFiles(String(args.owner), String(args.repo), Number(args.number)),
|
|
},
|
|
{
|
|
name: "gitea_pull_request_diff",
|
|
description: "A pull request's unified diff, as text — for reviewing it without a checkout.",
|
|
input: {
|
|
owner: { type: "string", description: "the repository owner" },
|
|
repo: { type: "string", description: "the repository name" },
|
|
number: { type: "number", description: "the PR number" },
|
|
},
|
|
run: async (args) => ({
|
|
diff: await gitea.pullDiff(String(args.owner), String(args.repo), Number(args.number)),
|
|
}),
|
|
},
|
|
{
|
|
name: "gitea_list_comments",
|
|
description: "Every comment on an issue or pull request, oldest first.",
|
|
input: {
|
|
owner: { type: "string", description: "the repository owner" },
|
|
repo: { type: "string", description: "the repository name" },
|
|
number: { type: "number", description: "the issue or PR number" },
|
|
},
|
|
run: async (args) => ({
|
|
comments: await gitea.listComments(String(args.owner), String(args.repo), Number(args.number)),
|
|
}),
|
|
},
|
|
{
|
|
name: "gitea_reopen_issue",
|
|
description: "Reopen a closed issue.",
|
|
input: {
|
|
owner: { type: "string", description: "the repository owner" },
|
|
repo: { type: "string", description: "the repository name" },
|
|
number: { type: "number", description: "the issue number" },
|
|
},
|
|
run: async (args) => ({
|
|
issue: await gitea.setIssueState(String(args.owner), String(args.repo), Number(args.number), "open"),
|
|
}),
|
|
},
|
|
|
|
// ---- Contents and branches ----
|
|
{
|
|
name: "gitea_get_file",
|
|
description: "One file's contents from a repository, decoded, at a branch, tag or commit (default the repository's default branch).",
|
|
input: {
|
|
owner: { type: "string", description: "the repository owner" },
|
|
repo: { type: "string", description: "the repository name" },
|
|
path: { type: "string", description: "the file's path from the repository's root" },
|
|
ref: { type: "string", description: "branch, tag or commit (optional)" },
|
|
},
|
|
run: async (args) => ({
|
|
file: await gitea.getFile(String(args.owner), String(args.repo), String(args.path), args.ref ? String(args.ref) : undefined),
|
|
}),
|
|
},
|
|
{
|
|
name: "gitea_list_branches",
|
|
description: "Every branch of a repository with the commit it points at.",
|
|
input: {
|
|
owner: { type: "string", description: "the repository owner" },
|
|
repo: { type: "string", description: "the repository name" },
|
|
},
|
|
run: async (args) => ({ branches: await gitea.listBranches(String(args.owner), String(args.repo)) }),
|
|
},
|
|
{
|
|
name: "gitea_delete_branch",
|
|
description: "Delete a branch — a feature branch whose pull request was closed rather than merged. Refused by the forge for a protected branch.",
|
|
input: {
|
|
owner: { type: "string", description: "the repository owner" },
|
|
repo: { type: "string", description: "the repository name" },
|
|
branch: { type: "string", description: "the branch name" },
|
|
},
|
|
run: async (args) => {
|
|
await gitea.deleteBranch(String(args.owner), String(args.repo), String(args.branch));
|
|
return { deleted: true, branch: String(args.branch) };
|
|
},
|
|
},
|
|
|
|
// ---- Branch protection (novox/hq ADR 0237) ----
|
|
{
|
|
name: "gitea_branch_protection_get",
|
|
description: "A repository's branch protection: the rule for one branch (null when it has none) or every rule — whether direct pushes are refused, which commit statuses a pull request must have succeeded to merge (e.g. mesh/merge-gate), approvals, and whether an administrator may merge past them.",
|
|
input: {
|
|
owner: { type: "string", description: "the repository owner" },
|
|
repo: { type: "string", description: "the repository name" },
|
|
branch: { type: "string", description: "the branch (rule name); every rule when not given" },
|
|
},
|
|
run: async (args) => {
|
|
const owner = String(args.owner), repo = String(args.repo);
|
|
if (!args.branch) return { rules: (await gitea.branchProtections(owner, repo)).map(summarised) };
|
|
const rule = await gitea.branchProtection(owner, repo, String(args.branch));
|
|
return { branch: String(args.branch), rule: rule ? summarised(rule) : null };
|
|
},
|
|
},
|
|
{
|
|
name: "gitea_branch_protection_set",
|
|
description: "Make a branch require commit statuses before a pull request merges into it — the mesh's merge check sets `mesh/merge-gate` (the module graph's gate) and `mesh/repo-check` (the repository's own tests). Edits the branch's rule (its required statuses replaced by these, everything else kept unless given) or creates one, which refuses direct pushes unless push=true. Answers the rule before and after.",
|
|
input: {
|
|
owner: { type: "string", description: "the repository owner" },
|
|
repo: { type: "string", description: "the repository name" },
|
|
branch: { type: "string", description: "the branch to protect, e.g. main" },
|
|
status_checks: { type: "string", description: "comma-separated statuses required to merge, e.g. mesh/merge-gate,mesh/repo-check; empty requires none" },
|
|
push: { type: "boolean", description: "whether a person may push to the branch directly (a new rule refuses it when not given)" },
|
|
block_admin_override: { type: "boolean", description: "stop an administrator merging past a status that has not succeeded (left as it is when not given)" },
|
|
},
|
|
run: async (args) => {
|
|
const owner = String(args.owner), repo = String(args.repo), branch = String(args.branch ?? "").trim();
|
|
if (!branch) throw new Error("name the branch to protect");
|
|
const before = await gitea.branchProtection(owner, repo, branch);
|
|
const { created, rule } = await gitea.setBranchProtection(owner, repo, branch, {
|
|
statusChecks: String(args.status_checks ?? "").split(","),
|
|
push: args.push === undefined ? undefined : Boolean(args.push),
|
|
blockAdminOverride: args.block_admin_override === undefined ? undefined : Boolean(args.block_admin_override),
|
|
});
|
|
return { branch, created, before: before ? summarised(before) : null, after: summarised(rule) };
|
|
},
|
|
},
|
|
|
|
// ---- A delivery's note, view and statuses (novox/hq ADR 0239), asked by mesh-delivery ----
|
|
{
|
|
name: "gitea_note_append",
|
|
description: "Append one line to a commit's git note under refs/notes/<ref> (mesh-plan for a delivery), written in the forge's own repository as the forge's own account; a line the note already holds is not added again. `git log --notes=mesh-plan` shows it.",
|
|
input: {
|
|
owner: { type: "string", description: "the repository owner" },
|
|
repo: { type: "string", description: "the repository name" },
|
|
sha: { type: "string", description: "the commit" },
|
|
ref: { type: "string", description: "the notes ref's name (default mesh-plan)" },
|
|
line: { type: "string", description: "the line, one line" },
|
|
},
|
|
run: async (args) => {
|
|
const said = await appendNote(run, forgeContainer, String(args.owner), String(args.repo), String(args.sha),
|
|
String(args.ref ?? "mesh-plan") || "mesh-plan", String(args.line ?? ""));
|
|
return { commit: String(args.sha), ...said };
|
|
},
|
|
},
|
|
{
|
|
name: "gitea_delivery_view",
|
|
description: "Keep a delivery's view on its pull request: one comment, marked as the delivery's, created the first time and edited in place after — the page every status of the delivery links to.",
|
|
input: {
|
|
owner: { type: "string", description: "the repository owner" },
|
|
repo: { type: "string", description: "the repository name" },
|
|
number: { type: "number", description: "the pull request's number" },
|
|
body: { type: "string", description: "the view, markdown" },
|
|
},
|
|
run: async (args) => {
|
|
const owner = String(args.owner), repo = String(args.repo), number = Number(args.number);
|
|
const body = viewBody(String(args.body ?? ""));
|
|
const existing = viewComment(await gitea.listComments(owner, repo, number));
|
|
if (existing) return { edited: await gitea.editComment(owner, repo, existing.id, body) };
|
|
return { created: await gitea.addComment(owner, repo, number, body) };
|
|
},
|
|
},
|
|
{
|
|
name: "gitea_commit_status",
|
|
description: "Set one of the mesh's statuses on a commit (mesh/delivery, mesh/delivery-group): pending, success, error, failure or warning, a short description, and the page it links to.",
|
|
input: {
|
|
owner: { type: "string", description: "the repository owner" },
|
|
repo: { type: "string", description: "the repository name" },
|
|
sha: { type: "string", description: "the commit" },
|
|
context: { type: "string", description: "the status's name, mesh/…" },
|
|
state: { type: "string", description: "pending | success | error | failure | warning" },
|
|
description: { type: "string", description: "one line" },
|
|
target_url: { type: "string", description: "the page it links to (the pull request)" },
|
|
},
|
|
run: async (args) => {
|
|
const status = deliveryStatus(String(args.context), String(args.state), String(args.description ?? ""),
|
|
args.target_url ? String(args.target_url) : undefined);
|
|
await gitea.setCommitStatus(String(args.owner), String(args.repo), String(args.sha), status);
|
|
return { set: status };
|
|
},
|
|
},
|
|
|
|
// ---- Labels ----
|
|
{
|
|
name: "gitea_list_labels",
|
|
description: "List every label defined in a repository.",
|
|
input: {
|
|
owner: { type: "string", description: "the repository owner" },
|
|
repo: { type: "string", description: "the repository name" },
|
|
},
|
|
run: async (args) => ({ labels: await gitea.listLabels(String(args.owner), String(args.repo)) }),
|
|
},
|
|
{
|
|
name: "gitea_create_label",
|
|
description: "Create a label in a repository.",
|
|
input: {
|
|
owner: { type: "string", description: "the repository owner" },
|
|
repo: { type: "string", description: "the repository name" },
|
|
name: { type: "string", description: "the label name" },
|
|
color: { type: "string", description: "hex colour, e.g. #0075ca" },
|
|
description: { type: "string", description: "an optional description" },
|
|
},
|
|
run: async (args) => ({
|
|
label: await gitea.createLabel(String(args.owner), String(args.repo), {
|
|
name: String(args.name),
|
|
color: String(args.color),
|
|
description: args.description ? String(args.description) : undefined,
|
|
}),
|
|
}),
|
|
},
|
|
|
|
// ---- Escape hatch ----
|
|
{
|
|
name: "gitea_api",
|
|
description: "Make an authenticated Gitea API call for any endpoint without a dedicated tool. Path is relative to /api/v1.",
|
|
input: {
|
|
path: { type: "string", description: "API path relative to /api/v1, e.g. /repos/owner/repo/branches" },
|
|
method: { type: "string", description: "GET | POST | PUT | PATCH | DELETE (default GET)" },
|
|
body: { type: "object", description: "JSON request body for POST/PUT/PATCH" },
|
|
},
|
|
run: async (args) => {
|
|
const method = args.method ? String(args.method) : "GET";
|
|
const result = await gitea.api(String(args.path), {
|
|
method,
|
|
...(args.body ? { body: JSON.stringify(args.body) } : {}),
|
|
});
|
|
return { result };
|
|
},
|
|
},
|
|
];
|
|
}
|
|
|
|
// The tools exist when the client has a way to a token: one configured, or the admin account to mint
|
|
// one with (token.ts). The mint itself happens on the first call, not here — a contributor is
|
|
// synchronous, and a forge not yet answering must not keep the runtime from serving. Without either
|
|
// way, gitea contributes none rather than failing the whole runtime, and says why.
|
|
registerModuleTools("gitea", (env) => {
|
|
try {
|
|
return getGiteaTools(GiteaClient.fromEnv(env));
|
|
} catch (err) {
|
|
console.log(`[gitea] no tools — ${err instanceof Error ? err.message : String(err)}`);
|
|
return [];
|
|
}
|
|
});
|