fix: POST for file creation (Gitea 1.25+), add edit_issue tool, persist poll state
- 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
This commit is contained in:
@@ -16,7 +16,11 @@ export default function (pi: ExtensionAPI) {
|
||||
pi.on("session_start", async (_event, ctx) => {
|
||||
console.log("[pi-gitea] Session started");
|
||||
|
||||
const sendMessageFn = ctx.sendUserMessage || ((_msg: string) => Promise.resolve());
|
||||
const sendMessageFn = (msg: string) => {
|
||||
// Use followUp so events queue when the LLM is already processing
|
||||
ctx.sendUserMessage(msg, { deliverAs: "followUp" });
|
||||
return Promise.resolve();
|
||||
};
|
||||
setSendMessage(sendMessageFn);
|
||||
|
||||
try {
|
||||
|
||||
@@ -161,6 +161,32 @@ export default function (pi: ExtensionAPI) {
|
||||
},
|
||||
});
|
||||
|
||||
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({
|
||||
|
||||
@@ -20,6 +20,8 @@ const POLL_INTERVAL = parseInt(process.env.PI_BOT_POLL_INTERVAL ?? "300", 10);
|
||||
const EVENT_POLL_INTERVAL = parseInt(process.env.PI_EVENT_POLL_INTERVAL ?? "60", 10);
|
||||
const BOT_USER = process.env.PI_GIT_USER ?? "";
|
||||
|
||||
const POLL_STATE_FILE = "/home/pibot/.pi/agent/gitea-poll-state.json";
|
||||
|
||||
let server: Server | null = null;
|
||||
let processingQueue: Array<{ event: any; timestamp: number }> = [];
|
||||
let maxQueueDepth = 50;
|
||||
@@ -37,6 +39,31 @@ let pollOnlyRepos: Map<string, { addedAt: number; lastPollAt: string }> = new Ma
|
||||
/** Track all known repos so we don't re-attempt webhook install every cycle */
|
||||
let knownRepos: Set<string> = new Set();
|
||||
|
||||
/** Load poll timestamps from disk (survives reloads) */
|
||||
async function loadPollState(): Promise<Record<string, string>> {
|
||||
try {
|
||||
const fs = await import("node:fs/promises");
|
||||
const data = await fs.readFile(POLL_STATE_FILE, "utf-8");
|
||||
return JSON.parse(data);
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
/** Save poll timestamps to disk */
|
||||
async function savePollState(): Promise<void> {
|
||||
try {
|
||||
const fs = await import("node:fs/promises");
|
||||
const state: Record<string, string> = {};
|
||||
for (const [name, s] of pollOnlyRepos) {
|
||||
state[name] = s.lastPollAt;
|
||||
}
|
||||
await fs.writeFile(POLL_STATE_FILE, JSON.stringify(state), "utf-8");
|
||||
} catch (err) {
|
||||
console.error("[gitea-polling] Error saving poll state:", err instanceof Error ? err.message : err);
|
||||
}
|
||||
}
|
||||
|
||||
export function setSendMessage(fn: (message: string) => Promise<void>) {
|
||||
sendMessage = fn;
|
||||
}
|
||||
@@ -273,6 +300,7 @@ async function discoverRepos() {
|
||||
try {
|
||||
const client = new GiteaClient();
|
||||
const repos = await client.get<any[]>("/user/repos?limit=100");
|
||||
const savedState = await loadPollState();
|
||||
|
||||
let newWebhooks = 0;
|
||||
let newPollOnly = 0;
|
||||
@@ -303,11 +331,11 @@ async function discoverRepos() {
|
||||
} catch (err) {
|
||||
if (err instanceof GiteaError && err.status === 403) {
|
||||
// No admin access — fall back to polling
|
||||
// Set lastPollAt to 5 minutes ago so we catch recent events
|
||||
const fiveMinAgo = new Date(Date.now() - 5 * 60 * 1000).toISOString();
|
||||
// Use persisted timestamp if available, otherwise 5 min ago
|
||||
const lastPoll = savedState[name] ?? new Date(Date.now() - 5 * 60 * 1000).toISOString();
|
||||
pollOnlyRepos.set(name, {
|
||||
addedAt: Date.now(),
|
||||
lastPollAt: fiveMinAgo,
|
||||
lastPollAt: lastPoll,
|
||||
});
|
||||
knownRepos.add(name);
|
||||
newPollOnly++;
|
||||
@@ -319,10 +347,10 @@ async function discoverRepos() {
|
||||
}
|
||||
} else {
|
||||
// No webhook URL configured — all repos are poll-only
|
||||
const fiveMinAgo = new Date(Date.now() - 5 * 60 * 1000).toISOString();
|
||||
const lastPoll = savedState[name] ?? new Date(Date.now() - 5 * 60 * 1000).toISOString();
|
||||
pollOnlyRepos.set(name, {
|
||||
addedAt: Date.now(),
|
||||
lastPollAt: fiveMinAgo,
|
||||
lastPollAt: lastPoll,
|
||||
});
|
||||
knownRepos.add(name);
|
||||
newPollOnly++;
|
||||
@@ -430,6 +458,9 @@ async function pollForEvents() {
|
||||
console.error(`[gitea-poll-events] Error polling ${repoName}: ${err instanceof Error ? err.message : err}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Persist poll timestamps
|
||||
await savePollState();
|
||||
}
|
||||
|
||||
// ── Public API ───────────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user