feat(cli): add pull request creation with AI-generated messages (#2)

Add a new `gai pr` subcommand that generates pull request titles and descriptions using AI, then creates the PR via GitHub CLI (`gh`) or Gitea CLI (`tea`). This extends the existing commit-generation system by reusing retry logic and prompt infrastructure, and introduces a `callAI` function that returns raw output (instead of pre-cleaned messages) to support structured PR responses.

Reviewed-on: #2
This commit was merged in pull request #2.
This commit is contained in:
2026-06-11 00:39:20 +08:00
parent 76a5bac11a
commit 6ff541284e
5 changed files with 585 additions and 11 deletions
+40 -10
View File
@@ -39,11 +39,10 @@ async function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
export async function generateCommitMessage(
export async function callAI(
config: Config,
systemPrompt: string,
userPrompt: string,
retries = MAX_RETRIES,
): Promise<string> {
const url = `${config.apiBase.replace(/\/$/, "")}/chat/completions`;
@@ -52,7 +51,7 @@ export async function generateCommitMessage(
{ role: "user", content: userPrompt },
];
for (let attempt = 1; attempt <= retries; attempt++) {
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
try {
const response = await fetch(url, {
method: "POST",
@@ -70,7 +69,7 @@ export async function generateCommitMessage(
if (!response.ok) {
const text = await response.text();
if (response.status === 429 && attempt < retries) {
if (response.status === 429 && attempt < MAX_RETRIES) {
await sleep(RETRY_DELAY * attempt);
continue;
}
@@ -89,7 +88,7 @@ export async function generateCommitMessage(
const finishReason = data.choices?.[0]?.finish_reason;
if (raw && raw.trim()) {
return cleanMessage(raw);
return raw;
}
if (finishReason === "length") {
@@ -102,22 +101,53 @@ export async function generateCommitMessage(
throw new Error("Response blocked by content filter.");
}
if (attempt < retries) {
if (attempt < MAX_RETRIES) {
await sleep(RETRY_DELAY * attempt);
continue;
}
throw new Error(
`Empty response from AI after ${retries} attempts. finish_reason: ${finishReason ?? "unknown"}`,
`Empty response from AI after ${MAX_RETRIES} attempts. finish_reason: ${finishReason ?? "unknown"}`,
);
} catch (err) {
if (attempt >= retries) throw err;
if (attempt >= MAX_RETRIES) throw err;
if (err instanceof Error && err.message.startsWith("API error")) throw err;
if (err instanceof Error && err.message.includes("max_tokens")) throw err;
if (err instanceof Error && err.message.includes("content filter")) throw err;
if (err instanceof Error && err.message.includes("content filter"))
throw err;
await sleep(RETRY_DELAY * attempt);
}
}
throw new Error("Failed to generate commit message");
throw new Error("Failed to generate response");
}
export async function generateCommitMessage(
config: Config,
systemPrompt: string,
userPrompt: string,
): Promise<string> {
const raw = await callAI(config, systemPrompt, userPrompt);
return cleanMessage(raw);
}
export async function generatePRMessage(
config: Config,
systemPrompt: string,
userPrompt: string,
): Promise<{ title: string; body: string }> {
const raw = await callAI(config, systemPrompt, userPrompt);
const cleaned = cleanMessage(raw);
const lines = cleaned.split("\n");
const title = lines[0]?.trim() || "Update";
let bodyStart = 1;
while (bodyStart < lines.length && lines[bodyStart]?.trim() === "") {
bodyStart++;
}
const body = lines.slice(bodyStart).join("\n").trim();
return { title, body };
}