Ctrl K
ComunidadGuías…

src/runner/command.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import type { ControlPlaneDb } from '../control-plane/db.ts';
import { getCommandByName } from '../control-plane/commands.ts';
import type { ExecutionType } from '../control-plane/types.ts';

export type PRTask = 'conversation' | ExecutionType | 'stop' | 'close';
export type ReservedControl = 'stop' | 'close' | 'approval';

export type ParsedIntent =
  | { kind: 'conversation'; repositoryId: string; reason?: 'plain_mention' | 'unknown_command' | 'disabled_command' | 'ambiguous_command' }
  | { kind: 'control'; control: ReservedControl }
  | { kind: 'command'; commandId: string; slashName: string; executionType: ExecutionType; permission: 'read_only' | 'read_write' | 'read_write_approval' };

const STATIC_COMMANDS = new Set(['conflict', 'confict', 'review', 'ci', 'stop', 'close']);
const RESERVED_CONTROLS = new Set<ReservedControl>(['stop', 'close', 'approval']);

interface MentionCommand {
  name: string;
  line: string;
}

function removeNonExecutableText(body: string) {
  const withoutFences = body.replace(/```[\s\S]*?```/g, '');
  return withoutFences.split('\n').filter(line => !/^\s*>/.test(line)).join('\n');
}

function mentionCommands(body: string, botLogin: string): MentionCommand[] {
  const escaped = botLogin.replace(/\[bot\]$/i, '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
  const text = removeNonExecutableText(body);
  const mention = new RegExp(`^[ \\t]*@${escaped}(?:\\[bot\\])?\\s+/(?:([a-z][a-z0-9-]{0,31}))(?=$|\\s)`, 'gim');
  return [...text.matchAll(mention)].map(match => {
    const start = (match.index ?? 0) + match[0].length;
    return { name: match[1].toLowerCase(), line: text.slice(start).split('\n')[0] };
  });
}

function hasAnotherCommand(line: string) {
  return /(?:^|\s)\/[a-z][a-z0-9-]{0,31}(?=$|\s)/i.test(line);
}

function lexicalCommand(body: string, botLogin: string) {
  const matches = mentionCommands(body, botLogin);
  const ambiguous = matches.length > 1 || (matches.length === 1 && hasAnotherCommand(matches[0].line));
  return { command: matches.length === 1 && !ambiguous ? matches[0] : undefined, ambiguous };
}

/**
 * Resolve one @ mention against the repository's control-plane command registry.
 * Unknown and disabled commands deliberately remain conversation so a comment can
 * never authorize a task merely because it resembles a slash command.
 */
export async function parsePRIntent(db: ControlPlaneDb, repositoryId: string, body: string, botLogin: string): Promise<ParsedIntent> {
  const lexical = lexicalCommand(body, botLogin);
  if (lexical.ambiguous) return { kind: 'conversation', repositoryId, reason: 'ambiguous_command' };
  if (!lexical.command) return { kind: 'conversation', repositoryId, reason: 'plain_mention' };
  const candidate = lexical.command!;
  const name = candidate.name === 'confict' ? 'conflict' : candidate.name === 'approve' ? 'approval' : candidate.name;
  if (RESERVED_CONTROLS.has(name as ReservedControl)) return { kind: 'control', control: name as ReservedControl };
  const command = await getCommandByName(db, repositoryId, name);
  if (!command) return { kind: 'conversation', repositoryId, reason: 'unknown_command' };
  if (!command.enabled) return { kind: 'conversation', repositoryId, reason: 'disabled_command' };
  return { kind: 'command', commandId: command.id, slashName: command.slashName,
    executionType: command.executionType, permission: command.permission };
}

// Only a command immediately following the bot mention is executable. Code blocks,
// quotations and ordinary discussion of a command are not task requests.
export function parsePRTask(body: string, botLogin: string): PRTask {
  const candidate = lexicalCommand(body, botLogin).command;
  if (!candidate || !STATIC_COMMANDS.has(candidate.name)) return 'conversation';
  if (candidate.name === 'confict') return 'conflict';
  return candidate.name as PRTask;
}