130 lines · 5.8 KB
import { z } from "zod";
import { handleEditorRoutes } from "../../editor/api";
import { mintEditorToken } from "../../editor/session";
import { HttpError } from "../../errors";
import { credentialAllows } from "../../token-scope";
import type { McpCaller } from "../protocol";
import { defineTool, repoArgs, scopedRepo, withTrailers } from "./base";
import { resolveRef } from "./content";

const MAX_EDITS = 200;
/** The room answers stale edits with 409 plus the current text; tools return that to the agent. */
const STALE_STATUS = 409;
const path = z.string().min(1).max(4096).describe("File path in the repository, e.g. src/app.ts");
const ref = z.string().min(1).max(250).optional().describe("Branch to co-edit (default branch when omitted)");

/**
 * Mints a short-lived editor token for this agent, so every tool call goes through the same
 * /api/editor checks as browsers: live permission and PAT scope, branch protection, LFS and path guards.
 */
async function editorToken(caller: McpCaller, owner: string, repoName: string, refName: string | undefined) {
  const { repo, permission } = await scopedRepo(caller, owner, repoName, "contents", "read");
  const revision = await resolveRef(repo, refName);
  if (revision.kind !== "branch") throw new HttpError(400, "Live editing needs a branch");
  const grant = caller.credential.grant;
  const minted = await mintEditorToken(caller.viewer, repo, revision.name, {
    ceiling: permission,
    canOpenPull: await credentialAllows(repo, caller.credential, "pulls", "write"),
    patId: grant && grant.kind !== "deploy" ? grant.tokenId : null,
  });
  return minted.token;
}

async function editorCall(
  caller: McpCaller,
  token: string,
  action: string,
  body?: unknown,
  search?: Record<string, string>,
) {
  const url = new URL(`${caller.origin}/api/editor/${action}`);