Julius Marmingeterminal links33276599 months agoHistory
252 lines · 7.0 KB · TypeScript
RawDownload
import { randomUUID } from "node:crypto";import fs from "node:fs/promises";import os from "node:os";import path from "node:path"; import type {  CommitMessageGenerationInput,  CommitMessageGenerationResult,  PrContentGenerationInput,  PrContentGenerationResult,  TextGenerationService,} from "./coreServices";import { type ProcessRunOptions, type ProcessRunResult, runProcess } from "./processRunner"; type ProcessRunner = (  command: string,  args: readonly string[],  options?: ProcessRunOptions,) => Promise<ProcessRunResult>; const CODEX_MODEL = "gpt-5.3-codex";const CODEX_REASONING_EFFORT = "low"; const COMMIT_OUTPUT_SCHEMA_JSON = {  type: "object",  properties: {    subject: { type: "string" },    body: { type: "string" },  },  required: ["subject", "body"],  additionalProperties: false,} as const; const PR_OUTPUT_SCHEMA_JSON = {  type: "object",  properties: {    title: { type: "string" },    body: { type: "string" },  },  required: ["title", "body"],  additionalProperties: false,} as const; function parseCommitOutput(raw: unknown): { subject: string; body: string } {  if (!raw || typeof raw !== "object") {    throw new Error("Codex returned a non-object commit message payload.");  }  const record = raw as Record<string, unknown>;  const subject = typeof record.subject === "string" ? record.subject.trim() : "";  const body = typeof record.body === "string" ? record.body : "";  if (subject.length === 0) {    throw new Error("Codex returned an empty commit subject.");  }  return { subject, body };} function parsePrOutput(raw: unknown): { title: string; body: string } {  if (!raw || typeof raw !== "object") {    throw new Error("Codex returned a non-object PR payload.");  }  const record = raw as Record<string, unknown>;  const title = typeof record.title === "string" ? record.title.trim() : "";  const body = typeof record.body === "string" ? record.body.trim() : "";  if (title.length === 0 || body.length === 0) {    throw new Error("Codex returned an invalid PR title/body payload.");  }  return { title, body };} function limitSection(value: string, maxChars: number): string {  if (value.length <= maxChars) return value;  const truncated = value.slice(0, maxChars);  return `${truncated}\n\n[truncated]`;} async function writeTempFile(prefix: string, content: string): Promise<string> {  const filePath = path.join(os.tmpdir(), `t3code-${prefix}-${process.pid}-${randomUUID()}.tmp`);  await fs.writeFile(filePath, content, "utf8");  return filePath;} async function safeUnlink(filePath: string): Promise<void> {  try {    await fs.unlink(filePath);  } catch {    // Best-effort cleanup.  }} function sanitizeCommitSubject(raw: string): string {  const singleLine = raw.trim().split(/\r?\n/g)[0]?.trim() ?? "";  const withoutTrailingPeriod = singleLine.replace(/[.]+$/g, "").trim();  if (withoutTrailingPeriod.length === 0) {    return "Update project files";  }   if (withoutTrailingPeriod.length <= 72) {    return withoutTrailingPeriod;  }  return withoutTrailingPeriod.slice(0, 72).trimEnd();} function sanitizePrTitle(raw: string): string {  const singleLine = raw.trim().split(/\r?\n/g)[0]?.trim() ?? "";  if (singleLine.length > 0) {    return singleLine;  }  return "Update project changes";} async function runCodexJson<T>({  cwd,  prompt,  outputSchemaJson,  parse,  run,}: {  cwd: string;  prompt: string;  outputSchemaJson: object;  parse: (raw: unknown) => T;  run: ProcessRunner;}): Promise<T> {  const schemaPath = await writeTempFile("codex-schema", JSON.stringify(outputSchemaJson));  let outputPath: string | null = null;   try {    outputPath = await writeTempFile("codex-output", "");    await run(      "codex",      [        "exec",        "--ephemeral",        "-s",        "read-only",        "--model",        CODEX_MODEL,        "--config",        `model_reasoning_effort="${CODEX_REASONING_EFFORT}"`,        "--output-schema",        schemaPath,        "--output-last-message",        outputPath,        "-",      ],      {        cwd,        timeoutMs: 180_000,        stdin: prompt,      },    );     const rawOutput = (await fs.readFile(outputPath, "utf8")).trim();    if (rawOutput.length === 0) {      throw new Error("Codex returned an empty response.");    }     let parsedJson: unknown;    try {      parsedJson = JSON.parse(rawOutput);    } catch {      throw new Error("Codex returned invalid JSON output.");    }     return parse(parsedJson);  } finally {    await Promise.all([safeUnlink(schemaPath), ...(outputPath ? [safeUnlink(outputPath)] : [])]);  }} interface CodexTextGeneratorDeps {  runProcess?: ProcessRunner;} export class CodexTextGenerator implements TextGenerationService {  private readonly run: ProcessRunner;   constructor(deps: CodexTextGeneratorDeps = {}) {    this.run = deps.runProcess ?? runProcess;  }   async generateCommitMessage(    input: CommitMessageGenerationInput,  ): Promise<CommitMessageGenerationResult> {    const prompt = [      "You write concise git commit messages.",      "Return a JSON object with keys: subject, body.",      "Rules:",      "- subject must be imperative, <= 72 chars, and no trailing period",      "- body can be empty string or short bullet points",      "- capture the primary user-visible or developer-visible change",      "",      `Branch: ${input.branch ?? "(detached)"}`,      "",      "Staged files:",      limitSection(input.stagedSummary, 6_000),      "",      "Staged patch:",      limitSection(input.stagedPatch, 40_000),    ].join("\n");     const generated = await runCodexJson({      cwd: input.cwd,      prompt,      outputSchemaJson: COMMIT_OUTPUT_SCHEMA_JSON,      parse: (raw) => parseCommitOutput(raw),      run: this.run,    });     return {      subject: sanitizeCommitSubject(generated.subject),      body: generated.body.trim(),    };  }   async generatePrContent(input: PrContentGenerationInput): Promise<PrContentGenerationResult> {    const prompt = [      "You write GitHub pull request content.",      "Return a JSON object with keys: title, body.",      "Rules:",      "- title should be concise and specific",      "- body must be markdown and include headings '## Summary' and '## Testing'",      "- under Summary, provide short bullet points",      "- under Testing, include bullet points with concrete checks or 'Not run' where appropriate",      "",      `Base branch: ${input.baseBranch}`,      `Head branch: ${input.headBranch}`,      "",      "Commits:",      limitSection(input.commitSummary, 12_000),      "",      "Diff stat:",      limitSection(input.diffSummary, 12_000),      "",      "Diff patch:",      limitSection(input.diffPatch, 40_000),    ].join("\n");     const generated = await runCodexJson({      cwd: input.cwd,      prompt,      outputSchemaJson: PR_OUTPUT_SCHEMA_JSON,      parse: (raw) => parsePrOutput(raw),      run: this.run,    });     return {      title: sanitizePrTitle(generated.title),      body: generated.body.trim(),    };  }}