Ctrl K
ComunidadGuías…

server/bot-overview.ts

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

Archivo original ↗Preguntar a KiBot
import type { BotRecord } from "./store.ts";
import type { Routine } from "./routines.ts";
import type { RoutineRequestSchedule } from "../shared/routine-request.ts";
import { approvalModeFor } from "../shared/approval-mode.ts";
import { cronScheduleLabel } from "../shared/cron-label.ts";

export interface BotOverview {
  who: { name: string; title: string; blurb: string; soulLead: string };
  does: string[];
  doesRoutines?: OverviewRoutineLine[];
  /** The departments a Chief of Staff may also coordinate, "" for the unnamed department the English line calls "General". */
  managedSections?: string[];
  reaches: string[];
  wont: string[];
  recent: Array<{ at: number; summary: string }>;
  grants?: BotOverviewGrant[];
  setup: SetupStep[];
}

export interface OverviewRoutineLine {
  name: string;
  paused?: true;
  schedule: OverviewFacts["routines"][number]["schedule"];
  nextRunAt?: number;
  lastRun?: { status: string; at: number };
}

export interface BotOverviewGrant {
  slug: string;
  level: "all" | "partial" | "none";
  /** Granted tool count; 0 unless level is "partial". */
  toolCount: number;
}

export type SetupStepId = "identity" | "soul" | "folder" | "apps" | "schedule";
export type SetupStepSection = "identity" | "soul" | "access" | "routines";

export interface SetupStep {
  id: SetupStepId;
  label: string;
  done: boolean;
  section?: SetupStepSection;
}

export interface OverviewFacts {
  bot: Pick<
    BotRecord,
    | "name"
    | "title"
    | "description"
    | "soul"
    | "computer"
    | "cloudBackend"
    | "cwd"
    | "autoApprove"
    | "approvalMode"
    | "composio"
    | "connectorTools"
    | "browser"
    | "chiefOfStaff"
    | "managedSections"
  >;
  routines: Array<{
    id: string;
    name: string;
    enabled: boolean;
    schedule: RoutineRequestSchedule | Routine["schedule"];
    nextRunAt: number | null;
  }>;
  runs: Array<{ routineId: string; status: string; finishedAt?: number; startedAt?: number; scheduledFor: number }>;
  webhooks: Array<{ name: string; enabled: boolean }>;
  skills: Array<{ name: string; description: string; enabled: boolean }>;
  engine: { agentsMcp?: boolean; composioMcp?: boolean; browserMcp?: boolean; computerMcp?: boolean } | null;
  browserEnabled?: boolean;
  cloudHome?: boolean;
  connectedApps: { configured: boolean; authoritative: boolean; services: string[] };
  sectionPeers: number;
  timeZone: string;
  recent: Array<{ at: number; summary: string }>;
}

export function soulLead(soul: string | undefined): string {
  const trimmed = (soul ?? "").trim();
  if (!trimmed) return "";
  const paragraph = (trimmed.split(/\r?\n\s*\r?\n/)[0] ?? trimmed).trim();
  return paragraph.length > 240 ? `${paragraph.slice(0, 240).trimEnd()}…` : paragraph;
}

function lowerFirst(value: string): string {
  return value.length ? value[0]!.toLowerCase() + value.slice(1) : value;
}

function time(at: number, timeZone: string): string {
  return new Date(at).toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit", timeZone });
}

const DAY_NAMES = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];

function clockTime(hhmm: string): string {
  const [h = 0, m = 0] = hhmm.split(":").map((part) => Number.parseInt(part, 10) || 0);
  return new Date(Date.UTC(2000, 0, 1, h, m)).toLocaleTimeString("en-US", {
    hour: "numeric",
    minute: "2-digit",
    timeZone: "UTC",
  });
}

function schedulePhrase(schedule: OverviewFacts["routines"][number]["schedule"], timeZone: string): string {
  if (schedule.type === "cron") return cronScheduleLabel(schedule);
  if (schedule.type === "once") {
    const date = new Date(schedule.at).toLocaleDateString("en-US", { month: "long", day: "numeric", timeZone });
    return `Once on ${date} at ${time(schedule.at, timeZone)}`;
  }
  if (schedule.type === "limit_reset") return schedule.repeat === "every_reset" ? "Each time the AI usage limit resets" : "When the AI usage limit resets";
  if (schedule.type === "interval") {
    const minutes = schedule.everyMinutes;
    if (minutes < 60) return `Every ${minutes} minute${minutes === 1 ? "" : "s"}`;
    if (minutes === 60) return "Every hour";
    if (minutes % 60 === 0) return `Every ${minutes / 60} hours`;
    return `Every ${Math.floor(minutes / 60)} hour${Math.floor(minutes / 60) === 1 ? "" : "s"} ${minutes % 60} minutes`;
  }
  const days = schedule.weekdays;
  const at = ` at ${clockTime(schedule.time)}`;
  if (days.length === 7) return `Every day${at}`;
  if (days.join(",") === "1,2,3,4,5") return `Every weekday${at}`;
  if (days.length === 1) return `Weekly on ${DAY_NAMES[days[0]!]}${at}`;
  return `${days.map((day) => DAY_NAMES[day]).join(", ")}${at}`;
}

function latestRunFor(runs: OverviewFacts["runs"], routineId: string): OverviewFacts["runs"][number] | undefined {
  let best: OverviewFacts["runs"][number] | undefined;
  let bestAt = -Infinity;
  for (const run of runs) {
    if (run.routineId !== routineId) continue;
    const at = run.finishedAt ?? run.startedAt ?? run.scheduledFor;
    if (at > bestAt) {
      bestAt = at;
      best = run;
    }
  }
  return best;
}

function routineLines(facts: OverviewFacts): OverviewRoutineLine[] {
  return facts.routines.map((routine): OverviewRoutineLine => {
    if (!routine.enabled) return { name: routine.name, paused: true, schedule: routine.schedule };
    const latest = latestRunFor(facts.runs, routine.id);
    return {
      name: routine.name, schedule: routine.schedule, ...(routine.nextRunAt != null ? { nextRunAt: routine.nextRunAt } : {}),
      ...(latest ? { lastRun: { status: latest.status, at: latest.finishedAt ?? latest.startedAt ?? latest.scheduledFor } } : {}),
    };
  });
}

function doesLines(facts: OverviewFacts): string[] {
  const lines: string[] = [];
  for (const routine of facts.routines) {
    if (!routine.enabled) {
      lines.push(`Paused: ${routine.name}.`);
      continue;
    }
    const nextRun = routine.nextRunAt != null ? ` Next run ${time(routine.nextRunAt, facts.timeZone)}.` : "";
    const latest = latestRunFor(facts.runs, routine.id);
    const at = latest ? latest.finishedAt ?? latest.startedAt ?? latest.scheduledFor : undefined;
    const lastRun = latest ? ` Last run ${latest.status} at ${time(at!, facts.timeZone)}.` : "";
    lines.push(`${schedulePhrase(routine.schedule, facts.timeZone)}: ${routine.name}.${nextRun}${lastRun}`);
  }
  for (const skill of facts.skills) {
    if (!skill.enabled) continue;
    lines.push(`Knows how to ${lowerFirst(skill.description || skill.name)}.`);
  }
  for (const webhook of facts.webhooks) {
    if (!webhook.enabled) continue;
    lines.push(`Listens for “${webhook.name}” webhooks.`);
  }
  return lines;
}

export async function connectedAppsFacts(
  configured: boolean,
  availability: "configured" | "unconfigured" | "unreadable",
  read: () => Promise<Record<string, { connected: boolean }>>,
): Promise<OverviewFacts["connectedApps"]> {
  let authoritative = availability !== "unreadable";
  let services: string[] = [];
  if (configured && authoritative) {
    try {
      services = Object.entries(await read())
        .filter(([, state]) => state.connected)
        .map(([slug]) => slug);
    } catch {
      authoritative = false;
    }
  }
  return { configured, authoritative, services };
}

function couldUseApps(facts: OverviewFacts): boolean {
  return facts.bot.composio !== false && facts.connectedApps.configured && Boolean(facts.engine?.composioMcp);
}

export function grantsSummary(connectorTools: BotRecord["connectorTools"]): BotOverviewGrant[] | undefined {
  if (connectorTools === undefined) return undefined;
  return Object.entries(connectorTools)
    .map(([slug, grant]): BotOverviewGrant => {
      if (grant.tools === "*") return { slug, level: "all", toolCount: 0 };
      if (grant.tools.length > 0) return { slug, level: "partial", toolCount: grant.tools.length };
      return { slug, level: "none", toolCount: 0 };
    })
    .sort((a, b) => (a.slug < b.slug ? -1 : 1));
}

function computerReach(computer: BotRecord["computer"], cloudHome = false): string | null {
  const unavailable = cloudHome ? ", which isn't available on My Cloud" : "";
  switch (computer) {
    case "cloud":
      return "Computer preference: cloud computer.";
    case "vm":
      return `Computer preference: Local VM${unavailable}.`;
    case "local":
      return `Computer preference: this computer${unavailable}.`;
    case "browser":
      return "Computer preference: browser only.";
    case "off":
      return null;
    default:
      return "Computer preference: Auto; availability is checked when a task starts.";
  }
}

function reachesLines(facts: OverviewFacts): string[] {
  const lines: string[] = [];
  const computer = computerReach(facts.bot.computer, facts.cloudHome);
  if (computer) lines.push(computer);
  lines.push(facts.bot.cwd ? `Works in ${facts.bot.cwd}.` : "Works in its private workspace.");
  const apps = facts.connectedApps;
  if (couldUseApps(facts) && apps.authoritative && apps.services.length > 0) {
    const count = apps.services.length;
    lines.push(`Can use ${count} connected app${count === 1 ? "" : "s"}: ${apps.services.join(", ")}.`);
  } else if (couldUseApps(facts) && !apps.authoritative) {
    lines.push("Connected apps could not be checked.");
  }
  if (facts.browserEnabled && facts.engine?.browserMcp && facts.bot.browser !== false && facts.bot.computer !== "off") lines.push("Has the built-in browser.");
  const peers = facts.sectionPeers;
  if (facts.engine?.agentsMcp && peers > 0) lines.push(peers === 1 ? "Can talk to the other bot." : `Can talk to all ${peers} other bots, in any department.`);
  if (facts.bot.chiefOfStaff) lines.push("Coordinates its department as Chief of Staff.");
  if (facts.bot.chiefOfStaff && facts.bot.managedSections?.length) {
    lines.push(`Also in charge of these departments: ${facts.bot.managedSections.map(name => name || "General").join(", ")}.`);
  }
  return lines;
}

function wontLines(facts: OverviewFacts): string[] {
  const lines: string[] = [];
  const mode = approvalModeFor(facts.bot);
  if (mode === "ask") lines.push("Command approvals use Ask mode; saved permissions and provider rules still apply.");
  if (mode === "custom") lines.push("Command approvals follow the provider's custom configuration.");
  if (!couldUseApps(facts) || (facts.connectedApps.authoritative && facts.connectedApps.services.length === 0)) {
    lines.push("Has no connected apps.");
  }
  if (facts.bot.computer === "off") lines.push("Can't use a computer.");
  if (!facts.routines.some((routine) => routine.enabled)) lines.push("Won't act on a schedule.");
  if (mode !== "full") lines.push("Profile proposal cards require your approval.");
  return lines;
}

export function buildBotOverview(facts: OverviewFacts): BotOverview {
  return {
    who: {
      name: facts.bot.name,
      title: facts.bot.title,
      blurb: facts.bot.description,
      soulLead: soulLead(facts.bot.soul),
    },
    does: doesLines(facts),
    doesRoutines: routineLines(facts),
    ...(facts.bot.chiefOfStaff && facts.bot.managedSections?.length ? { managedSections: [...facts.bot.managedSections] } : {}),
    reaches: reachesLines(facts),
    wont: wontLines(facts),
    recent: facts.recent,
    grants: grantsSummary(facts.bot.connectorTools),
    setup: setupSteps(facts),
  };
}

const DEFAULT_BOT_NAMES = /^(new bot|bot|untitled)(\s*\d+)?$/i;

export function setupSteps(facts: OverviewFacts): SetupStep[] {
  const named = facts.bot.name.trim() !== "" && !DEFAULT_BOT_NAMES.test(facts.bot.name.trim());
  const described = facts.bot.title.trim() !== "" || facts.bot.description.trim() !== "";
  const apps = facts.connectedApps;
  const routines = facts.routines.some((routine) => routine.enabled);
  const webhooks = facts.webhooks.some((webhook) => webhook.enabled);
  const steps: SetupStep[] = [
    { id: "identity", label: "Give it a name and a role", done: named && described, section: "identity" },
    { id: "soul", label: "Write its standing instructions", done: (facts.bot.soul ?? "").trim() !== "", section: "soul" },
    { id: "folder", label: "Choose a working folder", done: Boolean(facts.bot.cwd), section: "access" },
  ];
  if (couldUseApps(facts)) {
    steps.push({
      id: "apps",
      label: "Connect the apps it needs",
      done: apps.authoritative && apps.services.length > 0,
      section: "access",
    });
  }
  steps.push({ id: "schedule", label: "Add a schedule or a trigger", done: routines || webhooks, section: "routines" });
  return steps;
}