import { GiteaApiError } from "../integrations/gitea/client.js"; import type { GiteaContent } from "../integrations/gitea/types.js"; export type ToolResult = { content: Array<{ type: "text"; text: string }>; structuredContent?: Record; isError?: boolean }; export const MAX_MCP_READ_BYTES = 512 * 1024; export const asResult = (data: unknown, message = "Operation completed"): ToolResult => ({ content: [{ type: "text", text: message }], structuredContent: data && typeof data === "object" && !Array.isArray(data) ? data as Record : { data } }); export const asStandardKnowledgeResult = (data: Record): ToolResult => ({ content: [{ type: "text", text: JSON.stringify(data) }] }); export const asCompactResult = (data: Record, message: string): ToolResult => ({ content: [{ type: "text", text: message }], structuredContent: data }); export const errorCode = (error: unknown) => error instanceof GiteaApiError ? error.code : error instanceof Error ? error.message : "MCP_OPERATION_FAILED"; export const operationRecovery = (code: string) => { if (/CHECKS_PENDING/.test(code)) return { action: "wait_and_resume", retryable: true, detail: "Required checks are still queued/running. Keep the same delivery, branch and PR; inspect status after the indicated wait and retry the merge with the current head." }; if (/CI_|CHECKS_FAILED|VALIDATION_FAILED/.test(code)) return { action: "repair_and_revalidate", retryable: true, detail: "Read exact-head validation runs and the selected failed-job evidence. Fix the cause on the existing delivery branch. For canceled/transient runs, rerun the affected workflow. Sync successful evidence, then retry this PR's merge; never create a replacement branch or weaken checks." }; if (/HEAD_CONFLICT|FILE_CONFLICT|AUTO_MODE_CONFLICT/.test(code)) return { action: "reload_current_state", retryable: true, detail: "Reload the existing delivery, branch head and affected file SHAs. Reconcile actual progress before retrying; do not replay stale writes." }; if (/MERGE_UNCONFIRMED|PULL_REQUEST_NOT_READY/.test(code)) return { action: "reconcile_existing_pr", retryable: true, detail: "Read the existing PR and retry merge_feature_delivery with its expected head. An already merged PR is reconciled without another merge." }; return { action: "inspect_existing_delivery", retryable: false, detail: "Read the existing delivery and repository state before acting. Do not create another branch to work around a tool error." }; }; export const pageCollection = (items: T[], cursor: number, pageSize: number) => ({ items: items.slice(cursor, cursor + pageSize), total: items.length, nextCursor: cursor + pageSize < items.length ? String(cursor + pageSize) : undefined }); export const compactRecord = (value: unknown): Record => { if (!value || typeof value !== "object" || Array.isArray(value)) return { value }; const item = value as Record; const spec = item.spec && typeof item.spec === "object" && !Array.isArray(item.spec) ? item.spec as Record : undefined; const summaryCandidate = [item.summary, item.contextSummary, item.rule, item.intent, item.content] .find((candidate): candidate is string => typeof candidate === "string" && candidate.length > 0); return Object.fromEntries(Object.entries({ id: item.id, projectId: item.projectId, title: item.title, status: item.status ?? spec?.status, approvalState: item.approvalState, kind: item.kind, category: item.category, scope: item.scope, severity: item.severity, confidence: item.confidence, priority: item.priority, branch: spec?.branch, pullRequest: spec?.pullRequest, summary: summaryCandidate ? `${summaryCandidate.slice(0, 600)}${summaryCandidate.length > 600 ? "…" : ""}` : undefined, updatedAt: item.updatedAt, }).filter(([, field]) => field !== undefined)); }; export const compactDelivery = (value: unknown) => compactRecord(value); export const decodeFile = (content: GiteaContent) => { if (content.type !== "file" || content.encoding !== "base64" || typeof content.content !== "string") throw new Error("GITEA_CONTENT_NOT_FILE"); if (content.size > MAX_MCP_READ_BYTES) throw new Error("MCP_FILE_READ_TOO_LARGE"); return Buffer.from(content.content.replace(/\s/g, ""), "base64").toString("utf8"); }; export const parseFetchId = (id: string) => { const match = id.match(/^gitea:\/\/([^/]+)\/([^/]+)\/(.+)$/); if (!match) throw new Error("MCP_FETCH_ID_INVALID"); return { projectId: decodeURIComponent(match[1]), ref: decodeURIComponent(match[2]), path: decodeURIComponent(match[3]) }; };