/** * pi-gitea Extension — entry point * * Registers Gitea tools (read + write) and optional webhook server. * Supports @mention routing for multi-bot coordination. */ import registerReadTools from "./tools/read-tools.js"; import registerWriteTools from "./tools/write-tools.js"; import { startWebhookServer, stopWebhookServer, startPolling, stopPolling, setSendMessage, getTrackedRepos, setRepoConfig, } from "./webhook/server.js"; import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"; import { Type } from "@sinclair/typebox"; export default function (pi: ExtensionAPI) { registerReadTools(pi); registerWriteTools(pi); // ── Repo config tool ───────────────────────────────────────────────────── pi.registerTool({ name: "gitea_repo_config", label: "Gitea: Configure Repo Response Mode", description: 'Set how the bot responds to events on a repo. ' + '"all" = respond to every event. ' + '"mention" = respond only when @mentioned or assigned. ' + 'Collab repos default to "mention", own repos default to "all".', parameters: Type.Object({ repo: Type.String({ description: "Repository (owner/name)" }), respondTo: Type.String({ description: '"all" or "mention"' }), }), async execute(_id, params) { const mode = params.respondTo as "all" | "mention"; if (mode !== "all" && mode !== "mention") { return { content: [{ type: "text", text: `Invalid mode "${params.respondTo}". Use "all" or "mention".` }], }; } const config = setRepoConfig(params.repo, { respondTo: mode }); return { content: [{ type: "text", text: `✅ ${params.repo}: respondTo = ${config.respondTo}` }], details: { repo: params.repo, config }, }; }, }); pi.registerTool({ name: "gitea_tracked_repos", label: "Gitea: List Tracked Repos", description: "Show all tracked repos, their type (webhook/collab), and response mode.", parameters: Type.Object({}), async execute() { const { webhook, collab, configs } = getTrackedRepos(); const lines: string[] = []; for (const [name] of webhook) { const mode = configs.get(name)?.respondTo ?? "all"; lines.push(`✅ ${name} — webhook (respondTo: ${mode})`); } for (const name of collab) { const mode = configs.get(name)?.respondTo ?? "mention"; lines.push(`📋 ${name} — collab/notification (respondTo: ${mode})`); } return { content: [{ type: "text", text: lines.length > 0 ? lines.join("\n") : "No repos tracked." }], details: { webhook: [...webhook.keys()], collab: [...collab], configs: Object.fromEntries(configs) }, }; }, }); // ── Lifecycle ──────────────────────────────────────────────────────────── pi.on("session_start", async (_event, ctx) => { console.log("[pi-gitea] Session started"); // Auto-detect runtime: pi-bot (persistent session) vs openclaw (hooks endpoint) if (ctx.sendUserMessage) { // pi-bot: inject directly into the running session console.log("[pi-gitea] Delivery: sendUserMessage (pi-bot mode)"); setSendMessage((msg: string) => { ctx.sendUserMessage(msg, { deliverAs: "followUp" }); return Promise.resolve(); }); } else { // openclaw: POST to the hooks endpoint const hooksUrl = process.env.OPENCLAW_HOOKS_URL ?? "http://localhost:3001"; const hooksPath = process.env.OPENCLAW_HOOKS_PATH ?? "/hooks/agent"; const hooksToken = process.env.OPENCLAW_HOOKS_TOKEN ?? ""; console.log(`[pi-gitea] Delivery: openclaw hooks (${hooksUrl}${hooksPath})`); setSendMessage(async (msg: string) => { const res = await fetch(`${hooksUrl}${hooksPath}`, { method: "POST", headers: { "Content-Type": "application/json", ...(hooksToken ? { Authorization: `Bearer ${hooksToken}` } : {}), }, body: JSON.stringify({ message: msg }), }); if (!res.ok) { const body = await res.text().catch(() => ""); throw new Error(`Hooks POST failed: ${res.status} ${body}`); } }); } try { await startWebhookServer(pi); await startPolling(pi); } catch (err) { console.error("[pi-gitea] Failed to start:", err); } }); pi.on("session_shutdown", async () => { console.log("[pi-gitea] Session shutting down"); await stopWebhookServer(); stopPolling(); }); }