- files.ts: Use POST for new files, PUT for updates (Gitea 1.25 requires this) - issues.ts: Add editIssue() for state/title/body changes - write-tools.ts: Add gitea_edit_issue tool (open/close/edit issues) - webhook/server.ts: Persist lastPollAt to disk to prevent duplicate events on reload; use followUp delivery to queue events during LLM turns - index.ts: Use deliverAs:'followUp' for sendUserMessage
274 lines
10 KiB
TypeScript
274 lines
10 KiB
TypeScript
/**
|
|
* Write tools — create/update 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 webhooks from "../../src/webhooks.js";
|
|
import * as files from "../../src/files.js";
|
|
|
|
const client = new GiteaClient();
|
|
|
|
export default function (pi: ExtensionAPI) {
|
|
// ── Branches ──
|
|
|
|
pi.registerTool({
|
|
name: "gitea_create_branch",
|
|
label: "Gitea: Create Branch",
|
|
description: "Create a new branch from an existing ref.",
|
|
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})` })),
|
|
branch: Type.String({ description: "Name for the new branch" }),
|
|
ref: Type.Optional(Type.String({ description: "Source branch (default: main)" })),
|
|
}),
|
|
async execute(_id, params) {
|
|
const { owner, repo } = client.resolve(params.owner, params.repo);
|
|
const branch = await files.createBranch(client, owner, repo, {
|
|
name: params.branch,
|
|
oldRef: params.ref,
|
|
});
|
|
return {
|
|
content: [{ type: "text", text: `Branch created: ${branch.name}` }],
|
|
details: { branch },
|
|
};
|
|
},
|
|
});
|
|
|
|
// ── Files ──
|
|
|
|
pi.registerTool({
|
|
name: "gitea_get_file_content",
|
|
label: "Gitea: Get File Content",
|
|
description: "Get file content from a repository at a specific path.",
|
|
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})` })),
|
|
path: Type.String({ description: "File path (e.g., 'src/index.ts')" }),
|
|
ref: Type.Optional(Type.String({ description: "Commit SHA or branch (default: default branch)" })),
|
|
}),
|
|
async execute(_id, params) {
|
|
const { owner, repo } = client.resolve(params.owner, params.repo);
|
|
const file = await files.getFileContent(client, owner, repo, params.path, { ref: params.ref });
|
|
// Decode base64 content
|
|
const decoded = file.encoding === "base64" ? Buffer.from(file.content, "base64").toString("utf-8") : file.content;
|
|
return {
|
|
content: [{ type: "text", text: `File: ${params.path} (ref: ${params.ref ?? "HEAD"}, sha: ${file.sha})\n\n${decoded}` }],
|
|
details: { path: params.path, ref: params.ref, sha: file.sha, encoding: file.encoding },
|
|
};
|
|
},
|
|
});
|
|
|
|
pi.registerTool({
|
|
name: "gitea_update_file",
|
|
label: "Gitea: Update File",
|
|
description: "Update or create a file in a repository. Requires the file's current SHA for updates.",
|
|
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})` })),
|
|
path: Type.String({ description: "File path" }),
|
|
content: Type.String({ description: "File content" }),
|
|
message: Type.String({ description: "Commit message" }),
|
|
branch: Type.Optional(Type.String({ description: "Target branch (default: main)" })),
|
|
sha: Type.Optional(Type.String({ description: "Current file SHA (required for updates)" })),
|
|
}),
|
|
async execute(_id, params) {
|
|
const { owner, repo } = client.resolve(params.owner, params.repo);
|
|
const result = await files.updateFile(client, owner, repo, params.path, {
|
|
content: params.content,
|
|
message: params.message,
|
|
branch: params.branch,
|
|
sha: params.sha,
|
|
});
|
|
return {
|
|
content: [{ type: "text", text: `File updated: ${params.path}` }],
|
|
details: { commit: result.commit, content: result.content },
|
|
};
|
|
},
|
|
});
|
|
|
|
// ── Repos ──
|
|
|
|
pi.registerTool({
|
|
name: "gitea_create_repo",
|
|
label: "Gitea: Create Repository",
|
|
description: "Create a new repository for the authenticated user.",
|
|
parameters: Type.Object({
|
|
name: Type.String({ description: "Repository name" }),
|
|
private: Type.Optional(Type.Boolean({ description: "Private repo (default: false)" })),
|
|
description: Type.Optional(Type.String({ description: "Repository description" })),
|
|
}),
|
|
async execute(_id, params) {
|
|
const repo = await repos.createRepo(client, {
|
|
name: params.name,
|
|
private: params.private,
|
|
description: params.description,
|
|
});
|
|
return {
|
|
content: [{ type: "text", text: `✅ Created: ${repo.html_url}` }],
|
|
details: { repo },
|
|
};
|
|
},
|
|
});
|
|
|
|
pi.registerTool({
|
|
name: "gitea_ensure_repo",
|
|
label: "Gitea: Ensure Repository",
|
|
description: "Get a repository if it exists, create it if not. Returns clone URL.",
|
|
parameters: Type.Object({
|
|
owner: Type.Optional(Type.String({ description: `Repo owner (default: ${client.defaultOwner})` })),
|
|
name: Type.String({ description: "Repository name" }),
|
|
private: Type.Optional(Type.Boolean({ description: "Private if creating (default: false)" })),
|
|
}),
|
|
async execute(_id, params) {
|
|
const owner = params.owner ?? client.defaultOwner;
|
|
const { repo, created } = await repos.ensureRepo(client, owner, params.name, {
|
|
private: params.private,
|
|
});
|
|
return {
|
|
content: [{ type: "text", text: `${created ? "Created" : "Exists"}: ${repo.clone_url}` }],
|
|
details: { repo, created },
|
|
};
|
|
},
|
|
});
|
|
|
|
// ── Issues ──
|
|
|
|
pi.registerTool({
|
|
name: "gitea_create_issue",
|
|
label: "Gitea: Create Issue",
|
|
description: "Create a new issue in 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})` })),
|
|
title: Type.String({ description: "Issue title" }),
|
|
body: Type.Optional(Type.String({ description: "Issue body (Markdown)" })),
|
|
}),
|
|
async execute(_id, params) {
|
|
const { owner, repo } = client.resolve(params.owner, params.repo);
|
|
const issue = await issues.createIssue(client, owner, repo, {
|
|
title: params.title,
|
|
body: params.body,
|
|
});
|
|
return {
|
|
content: [{ type: "text", text: `✅ Issue #${issue.number}: ${issue.html_url}` }],
|
|
details: { issue },
|
|
};
|
|
},
|
|
});
|
|
|
|
pi.registerTool({
|
|
name: "gitea_edit_issue",
|
|
label: "Gitea: Edit Issue",
|
|
description: "Edit an issue — change title, body, or state (open/closed). Use this to close issues when work is done.",
|
|
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})` })),
|
|
index: Type.Number({ description: "Issue number" }),
|
|
title: Type.Optional(Type.String({ description: "New title" })),
|
|
body: Type.Optional(Type.String({ description: "New body (Markdown)" })),
|
|
state: Type.Optional(Type.String({ description: "New state: 'open' or 'closed'" })),
|
|
}),
|
|
async execute(_id, params) {
|
|
const { owner, repo } = client.resolve(params.owner, params.repo);
|
|
const updated = await issues.editIssue(client, owner, repo, params.index, {
|
|
title: params.title,
|
|
body: params.body,
|
|
state: params.state as "open" | "closed" | undefined,
|
|
});
|
|
return {
|
|
content: [{ type: "text", text: `Issue #${updated.number} updated (state: ${updated.state}): ${updated.html_url}` }],
|
|
details: { issue: updated },
|
|
};
|
|
},
|
|
});
|
|
|
|
// ── Pull Requests ──
|
|
|
|
pi.registerTool({
|
|
name: "gitea_create_pr",
|
|
label: "Gitea: Create PR",
|
|
description: "Create a pull request from a source branch to a target branch.",
|
|
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})` })),
|
|
title: Type.String({ description: "PR title" }),
|
|
head: Type.String({ description: "Source branch" }),
|
|
base: Type.String({ description: "Target branch" }),
|
|
body: Type.Optional(Type.String({ description: "PR description" })),
|
|
}),
|
|
async execute(_id, params) {
|
|
const { owner, repo } = client.resolve(params.owner, params.repo);
|
|
const pr = await pulls.createPullRequest(client, owner, repo, {
|
|
title: params.title,
|
|
head: params.head,
|
|
base: params.base,
|
|
body: params.body,
|
|
});
|
|
return {
|
|
content: [{ type: "text", text: `PR created: ${pr.html_url}` }],
|
|
details: { pr },
|
|
};
|
|
},
|
|
});
|
|
|
|
pi.registerTool({
|
|
name: "gitea_merge_pr",
|
|
label: "Gitea: Merge PR",
|
|
description: "Merge a pull request into its base branch.",
|
|
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})` })),
|
|
index: Type.Number({ description: "PR number" }),
|
|
merge_method: Type.Optional(
|
|
Type.Union([Type.Literal("merge"), Type.Literal("rebase"), Type.Literal("squash")], {
|
|
description: "Merge method (default: merge)",
|
|
}),
|
|
),
|
|
}),
|
|
async execute(_id, params) {
|
|
const { owner, repo } = client.resolve(params.owner, params.repo);
|
|
await pulls.mergePullRequest(client, owner, repo, params.index, {
|
|
method: params.merge_method as any,
|
|
});
|
|
return {
|
|
content: [{ type: "text", text: `PR #${params.index} merged.` }],
|
|
details: { index: params.index },
|
|
};
|
|
},
|
|
});
|
|
|
|
// ── Webhooks ──
|
|
|
|
pi.registerTool({
|
|
name: "gitea_create_webhook",
|
|
label: "Gitea: Create Webhook",
|
|
description: "Create a webhook on 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})` })),
|
|
url: Type.String({ description: "URL where Gitea will send webhooks" }),
|
|
token: Type.Optional(Type.String({ description: "Bearer token for webhook auth" })),
|
|
events: Type.Optional(
|
|
Type.Array(Type.String(), { description: "Events to listen for (default: issues, issue_comment, pull_request, push)" }),
|
|
),
|
|
}),
|
|
async execute(_id, params) {
|
|
const { owner, repo } = client.resolve(params.owner, params.repo);
|
|
const hook = await webhooks.createWebhook(client, owner, repo, {
|
|
url: params.url,
|
|
token: params.token,
|
|
events: params.events,
|
|
});
|
|
return {
|
|
content: [{ type: "text", text: `Webhook created: ID ${hook.id}` }],
|
|
details: { webhook: hook },
|
|
};
|
|
},
|
|
});
|
|
}
|