61 lines
4.6 KiB
TypeScript
61 lines
4.6 KiB
TypeScript
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<string, unknown>; 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<string, unknown> : { data } });
|
|
export const asStandardKnowledgeResult = (data: Record<string, unknown>): ToolResult => ({ content: [{ type: "text", text: JSON.stringify(data) }] });
|
|
export const asCompactResult = (data: Record<string, unknown>, 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 = <T>(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<string, unknown> => {
|
|
if (!value || typeof value !== "object" || Array.isArray(value)) return { value };
|
|
const item = value as Record<string, unknown>;
|
|
const spec = item.spec && typeof item.spec === "object" && !Array.isArray(item.spec) ? item.spec as Record<string, unknown> : 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]) };
|
|
};
|