import type { agentContextObject, allAgentTool, AutomationLevel, configAgentTool, notifType, ResponsiblePartyType, } from "./types.ts"; import { configAgentTools, requiredAgentTools, validAutomationLevels, validNotifTypes, } from "./const.ts"; import { msFrom } from "./time.ts"; import { bsky } from "./bsky.ts"; import { isAgentAsleep as checkIsAsleep, isAgentAwake as checkIsAwake, } from "./sleepWakeHelpers.ts"; export const getLettaApiKey = (): string => { const value = Deno.env.get("LETTA_API_KEY")?.trim(); if (!value?.length) { throw Error( "Letta API key not provided in `.env`. add variable `LETTA_API_KEY=`.", ); } else if (!value.startsWith("sk")) { throw Error( "Letta API key is not formed correctly, check variable `LETTA_API_KEY", ); } return value; }; export const getLettaAgentID = (): string => { const value = Deno.env.get("LETTA_AGENT_ID")?.trim(); if (!value?.length) { throw Error( "Letta Agent ID not provided in `.env`. add variable `LETTA_AGENT_ID=`.", ); } else if (!value.startsWith("agent-")) { throw Error( "Letta Agent ID is not formed correctly, check variable `LETTA_AGENT_ID`", ); } return value; }; const getLettaProjectID = (): string => { const value = Deno.env.get("LETTA_PROJECT_ID")?.trim(); if (!value?.length) { throw Error( "Letta Project ID not provided in `.env`. add variable `LETTA_PROJECT_ID=`.", ); } else if (!value.includes("-")) { throw Error( "Letta Project ID is not formed correctly, check variable `LETTA_PROJECT_ID`", ); } return value; }; const getAgentBskyHandle = (): string => { const value = Deno.env.get("BSKY_USERNAME")?.trim(); if (!value?.length) { throw Error( "Bluesky Handle for agent not provided in `.env`. add variable `BSKY_USERNAME=`", ); } const cleanHandle = value.startsWith("@") ? value.slice(1) : value; if (!cleanHandle.includes(".")) { throw Error( `Invalid handle format: ${value}. Expected format: user.bsky.social`, ); } return cleanHandle; }; const getAgentBskyName = async (): Promise => { try { const profile = await bsky.getProfile({ actor: getAgentBskyHandle() }); const displayName = profile?.data.displayName?.trim(); if (displayName) { return displayName; } throw Error(`No display name found for ${getAgentBskyHandle()}`); } catch (error) { throw Error(`Failed to get display name: ${error}`); } }; const getResponsiblePartyName = (): string => { const value = Deno.env.get("RESPONSIBLE_PARTY_NAME")?.trim(); if (!value?.length) { throw Error("RESPONSIBLE_PARTY_NAME environment variable is not set"); } return value; }; const getResponsiblePartyContact = (): string => { const value = Deno.env.get("RESPONSIBLE_PARTY_CONTACT")?.trim(); if (!value?.length) { throw Error("RESPONSIBLE_PARTY_CONTACT environment variable is not set"); } return value; }; const getAutomationLevel = (): AutomationLevel => { const value = Deno.env.get("AUTOMATION_LEVEL")?.trim(); const valid = validAutomationLevels; if (!value) { return "automated"; } if (!valid.includes(value as typeof valid[number])) { throw Error( `Invalid automation level: ${value}. Must be one of: ${valid.join(", ")}`, ); } return value as AutomationLevel; }; const setAgentBskyDID = (): string => { if (!bsky.did) { throw Error(`couldn't get DID for ${getAgentBskyHandle()}`); } else { return bsky.did; } }; const getBskyServiceUrl = (): string => { const value = Deno.env.get("BSKY_SERVICE_URL")?.trim(); if (!value?.length || !value?.startsWith("https://")) { return "https://bsky.social"; } return value; }; const getSupportedNotifTypes = (): notifType[] => { const value = Deno.env.get("BSKY_NOTIFICATION_TYPES"); if (!value?.length) { return ["mention", "reply"]; } const notifList = value.split(",").map((type) => type.trim()); for (const notifType of notifList) { if ( !validNotifTypes.includes(notifType as typeof validNotifTypes[number]) ) { throw Error( `"${notifType}" is not a valid notification type. check "BSKY_NOTIFICATION_TYPES" variable in your \`.env\` file.`, ); } } return notifList as notifType[]; }; const getSupportedTools = (): allAgentTool[] => { const value = Deno.env.get("BSKY_SUPPORTED_TOOLS"); const defaultTools: configAgentTool[] = [ "create_bluesky_post", "update_bluesky_profile", ]; if (!value?.length) { return [...defaultTools, ...requiredAgentTools] as allAgentTool[]; } const toolList = value.split(",").map((type) => type.trim()); for (const tool of toolList) { if (!configAgentTools.includes(tool as typeof configAgentTools[number])) { throw Error( `"${tool}" is not a valid tool name. check "BSKY_SUPPORTED_TOOLS" variable in your \`.env\` file.`, ); } else if ( requiredAgentTools.includes(tool as typeof requiredAgentTools[number]) ) { throw Error( `${tool} is always included and does not need to be added to "BSKY_SUPPORTED_TOOLS" in \`env\`.`, ); } } return toolList.concat(requiredAgentTools) as allAgentTool[]; }; const getNotifDelayMinimum = (): number => { const value = msFrom.parse(Deno.env.get("NOTIF_DELAY_MINIMUM")); if (isNaN(value) || value < msFrom.seconds(1) || value > msFrom.hours(24)) { return msFrom.seconds(10); } return value; }; const getNotifDelayMaximum = (): number => { const value = msFrom.parse(Deno.env.get("NOTIF_DELAY_MAXIMUM")); if (isNaN(value) || value < msFrom.seconds(5) || value > msFrom.hours(24)) { return msFrom.minutes(90); } const minimum = getNotifDelayMinimum(); if (value <= minimum) { throw Error( `"NOTIF_DELAY_MAXIMUM" cannot be less than or equal to "NOTIF_DELAY_MINIMUM"`, ); } return value; }; const getNotifDelayMultiplier = (): number => { const value = Number(Deno.env.get("NOTIF_DELAY_MULTIPLIER")); if (isNaN(value) || value < 0 || value > 500) { return 1.12; } return (value / 100) + 1; }; const getMaxThreadPosts = (): number => { const value = Number(Deno.env.get("MAX_THREAD_POSTS")); if (isNaN(value) || value < 5 || value > 250) { return 25; } return Math.round(value); }; const getReflectionDelayMinimum = (): number => { const value = msFrom.parse(Deno.env.get("REFLECTION_DELAY_MINIMUM")); if (isNaN(value) || value < msFrom.minutes(30) || value > msFrom.hours(24)) { return msFrom.hours(3); } return value; }; const getReflectionDelayMaximum = (): number => { const value = msFrom.parse(Deno.env.get("REFLECTION_DELAY_MAXIMUM")); const minimum = getReflectionDelayMinimum(); if (isNaN(value) || value < msFrom.minutes(60) || value > msFrom.hours(24)) { return msFrom.hours(14); } if (value <= minimum) { throw Error( `"REFLECTION_DELAY_MAXIMUM" cannot be less than or equal to "REFLECTION_DELAY_MINIMUM"`, ); } return value; }; const getProactiveDelayMinimum = (): number => { const value = msFrom.parse(Deno.env.get("PROACTIVE_DELAY_MINIMUM")); if (isNaN(value) || value < msFrom.hours(1) || value > msFrom.hours(24)) { return msFrom.hours(3); } return value; }; const getProactiveDelayMaximum = (): number => { const value = msFrom.parse(Deno.env.get("PROACTIVE_DELAY_MAXIMUM")); const minimum = getProactiveDelayMinimum(); if (isNaN(value) || value < msFrom.hours(3) || value > msFrom.hours(24)) { return msFrom.hours(14); } if (value <= minimum) { throw Error( `"PROACTIVE_DELAY_MAXIMUM" cannot be less than or equal to "PROACTIVE_DELAY_MINIMUM"`, ); } return value; }; const getWakeTime = (): number => { const envValue = Deno.env.get("WAKE_TIME"); if (envValue === undefined || envValue === null || envValue === "") { return 8; } const value = Math.round(Number(envValue)); if (isNaN(value)) { throw Error(`"WAKE_TIME" must be a valid number, got: "${envValue}"`); } if (value > 23) { throw Error(`"WAKE_TIME" cannot be greater than 23 (11pm)`); } if (value < 0) { throw Error(`"WAKE_TIME" cannot be less than 0 (midnight)`); } return value; }; const getSleepTime = (): number => { const envValue = Deno.env.get("SLEEP_TIME"); if (envValue === undefined || envValue === null || envValue === "") { return 10; } const value = Math.round(Number(envValue)); if (isNaN(value)) { throw Error(`"SLEEP_TIME" must be a valid number, got: "${envValue}"`); } if (value > 23) { throw Error(`"SLEEP_TIME" cannot be greater than 23 (11pm)`); } if (value < 0) { throw Error(`"SLEEP_TIME" cannot be less than 0 (midnight)`); } return value; }; const getTimeZone = (): string => { const value = Deno.env.get("TIMEZONE")?.trim(); if (!value?.length) { return "America/Los_Angeles"; } try { Intl.DateTimeFormat(undefined, { timeZone: value }); return value; } catch { throw Error( `Invalid timezone: ${value}. Must be a valid IANA timezone like "America/New_York"`, ); } }; const getResponsiblePartyType = (): ResponsiblePartyType => { const value = Deno.env.get("RESPONSIBLE_PARTY_TYPE")?.trim().toLowerCase(); if (value === "person" || value === "organization") { return value; } return "person"; }; const setReflectionEnabled = (): boolean => { const reflectionMinVal = Deno.env.get("REFLECTION_DELAY_MINIMUM"); const reflectionMaxVal = Deno.env.get("REFLECTION_DELAY_MAXIMUM"); if (reflectionMinVal?.length && reflectionMaxVal?.length) { return true; } return false; }; const setProactiveEnabled = (): boolean => { const proactiveMinVal = Deno.env.get("PROACTIVE_DELAY_MINIMUM"); const proactiveMaxVal = Deno.env.get("PROACTIVE_DELAY_MAXIMUM"); if (proactiveMinVal?.length && proactiveMaxVal?.length) { return true; } return false; }; const setSleepEnabled = (): boolean => { const sleep = Deno.env.get("SLEEP_TIME"); const wake = Deno.env.get("WAKE_TIME"); if (sleep?.length && wake?.length) { return true; } return false; }; const getPreserveMemoryBlocks = (): boolean => { const value = Deno.env.get("PRESERVE_MEMORY_BLOCKS")?.trim().toLowerCase(); if (!value?.length) { return false; } return value === "true" || value === "1"; }; export const getBskyAppPassword = (): string => { const value = Deno.env.get("BSKY_APP_PASSWORD")?.trim(); if (!value?.length) { throw Error( "Bluesky app password not provided in `.env`. add variable `BSKY_APP_PASSWORD=`", ); } const hyphenCount = value.split("-").length - 1; if (value.length !== 19 || hyphenCount !== 2) { throw Error( "You are likely not using an app password. App passwords are 19 characters with 2 hyphens (format: xxxx-xxxx-xxxx). You can generate one at https://bsky.app/settings/app-passwords", ); } return value; }; export const getAutomationDescription = (): string | undefined => { const value = Deno.env.get("AUTOMATION_DESCRIPTION")?.trim(); if (!value?.length) { return undefined; } if (value.length < 10) { throw Error( "Automation description must be at least 10 characters long", ); } return value; }; export const getDisclosureUrl = (): string | undefined => { const value = Deno.env.get("DISCLOSURE_URL")?.trim(); if (!value?.length) { return undefined; } if (value.length < 6) { throw Error( "Disclosure URL must be at least 6 characters long", ); } return value; }; export const getResponsiblePartyBsky = async (): Promise< string | undefined > => { const value = Deno.env.get("RESPONSIBLE_PARTY_BSKY")?.trim(); if (!value?.length) { return undefined; } // If it's already a DID, return it if (value.startsWith("did:")) { return value; } // If it looks like a handle (contains a dot), resolve it to a DID if (value.includes(".")) { try { const profile = await bsky.getProfile({ actor: value }); return profile.data.did; } catch (error) { throw Error( `Failed to resolve DID for handle "${value}": ${error}`, ); } } // Not a DID and not a handle throw Error( `Invalid RESPONSIBLE_PARTY_BSKY value: "${value}". Must be either a DID (starts with "did:") or a handle (contains ".")`, ); }; export const getExternalServices = (): string[] | undefined => { const value = Deno.env.get("EXTERNAL_SERVICES")?.trim(); if (!value?.length) { return undefined; } // Parse comma-separated list const services = value .split(",") .map((service) => service.trim()) .filter((service) => service.length > 0); if (services.length === 0) { return undefined; } // Validate each service string for (const service of services) { if (service.length > 200) { throw Error( `External service name too long: "${ service.substring(0, 50) }..." (max 200 characters)`, ); } } // Validate array length if (services.length > 20) { throw Error( `Too many external services specified: ${services.length} (max 20)`, ); } return services; }; const populateAgentContext = async (): Promise => { console.log("🔹 building new agentContext object…"); const context: agentContextObject = { // state busy: false, sleeping: false, checkCount: 0, reflectionCount: 0, processingCount: 0, proactiveCount: 0, likeCount: 0, repostCount: 0, followCount: 0, mentionCount: 0, replyCount: 0, quoteCount: 0, notifCount: 0, // required with manual variables lettaProjectIdentifier: getLettaProjectID(), agentBskyHandle: getAgentBskyHandle(), agentBskyName: await getAgentBskyName(), agentBskyDID: setAgentBskyDID(), responsiblePartyName: getResponsiblePartyName(), responsiblePartyContact: getResponsiblePartyContact(), agentBskyServiceUrl: getBskyServiceUrl(), automationLevel: getAutomationLevel(), supportedNotifTypes: getSupportedNotifTypes(), supportedTools: getSupportedTools(), notifDelayMinimum: getNotifDelayMinimum(), notifDelayMaximum: getNotifDelayMaximum(), notifDelayMultiplier: getNotifDelayMultiplier(), reflectionDelayMinimum: getReflectionDelayMinimum(), reflectionDelayMaximum: getReflectionDelayMaximum(), proactiveDelayMinimum: getProactiveDelayMinimum(), proactiveDelayMaximum: getProactiveDelayMaximum(), wakeTime: getWakeTime(), sleepTime: getSleepTime(), timeZone: getTimeZone(), responsiblePartyType: getResponsiblePartyType(), preserveAgentMemory: getPreserveMemoryBlocks(), maxThreadPosts: getMaxThreadPosts(), reflectionEnabled: setReflectionEnabled(), proactiveEnabled: setProactiveEnabled(), sleepEnabled: setSleepEnabled(), notifDelayCurrent: getNotifDelayMinimum(), }; const automationDescription = getAutomationDescription(); if (automationDescription) { context.automationDescription = automationDescription; } const disclosureUrl = getDisclosureUrl(); if (disclosureUrl) { context.disclosureUrl = disclosureUrl; } const responsiblePartyBsky = await getResponsiblePartyBsky(); if (responsiblePartyBsky) { context.responsiblePartyBsky = responsiblePartyBsky; } const externalServices = getExternalServices(); if (externalServices) { context.externalServices = externalServices; } console.log( `🔹 \`agentContext\` object built for ${context.agentBskyName}, BEGINING TASKS…`, ); return context; }; export const agentContext = await populateAgentContext(); export const claimTaskThread = () => { if (agentContext.busy) return false; agentContext.busy = true; return true; }; export const releaseTaskThread = () => { agentContext.busy = false; }; export const resetAgentContextCounts = () => { agentContext.likeCount = 0; agentContext.repostCount = 0; agentContext.followCount = 0; agentContext.mentionCount = 0; agentContext.replyCount = 0; agentContext.quoteCount = 0; }; export const isAgentAwake = (hour: number): boolean => { return checkIsAwake(hour, agentContext.wakeTime, agentContext.sleepTime); }; export const isAgentAsleep = (hour: number): boolean => { return checkIsAsleep(hour, agentContext.wakeTime, agentContext.sleepTime); };