server/bot-overview.ts
Rama main · réplica de lectura del repositorio original.
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;
}