feat: add loom ASCII art startup banner (#270)

This commit is contained in:
Cameron
2026-02-10 17:57:19 -08:00
committed by GitHub
parent 28adc22388
commit 57f102dfd4
3 changed files with 1318 additions and 26 deletions

1214
package-lock.json generated

File diff suppressed because it is too large Load Diff

101
src/core/banner.ts Normal file
View File

@@ -0,0 +1,101 @@
/**
* Startup banner with LETTABOT block text and loom ASCII art.
*/
interface BannerAgent {
name: string;
agentId?: string | null;
channels: string[];
features?: {
cron?: boolean;
heartbeatIntervalMin?: number;
};
}
/** Pad a line to exactly `width` characters (handles emoji 2-char surrogates). */
function L(text: string, width = 39): string {
// Emoji surrogate pairs are 2 JS chars but 2 terminal columns, so padEnd works.
return text.padEnd(width);
}
const BLOCK_TEXT = `
░██ ░██████████ ░██████████ ░██████████ ░███ ░████████ ░██████ ░██████████
░██ ░██ ░██ ░██ ░██░██ ░██ ░██ ░██ ░██ ░██
░██ ░██ ░██ ░██ ░██ ░██ ░██ ░██ ░██ ░██ ░██
░██ ░█████████ ░██ ░██ ░█████████ ░████████ ░██ ░██ ░██
░██ ░██ ░██ ░██ ░██ ░██ ░██ ░██ ░██ ░██ ░██
░██ ░██ ░██ ░██ ░██ ░██ ░██ ░██ ░██ ░██ ░██
░██████████ ░██████████ ░██ ░██ ░██ ░██ ░█████████ ░██████ ░██
`.trim();
const P = ' '; // 12-space prefix for centering the box
export function printStartupBanner(agents: BannerAgent[]): void {
// Block text
console.log('');
console.log(BLOCK_TEXT);
console.log('');
// Loom box
const lines = [
`${P}╔═══════════════════════════════════════╗`,
`${P}${L(' L E T T A B O T L O O M')}`,
`${P}${L(' memory weaver v1.0')}`,
`${P}╠═══════════════════════════════════════╣`,
`${P}${L('')}`,
`${P}${L(' ▓▓▓░░░▓▓▓░░░▓▓▓░░░▓▓▓░░░▓▓▓')}`,
`${P}${L(' ░░░▓▓▓░░░▓▓▓░░░▓▓▓░░░▓▓▓░░░')}`,
`${P}${L(' ▓▓▓░░░▓▓▓░░░▓▓▓░░░▓▓▓░░░▓▓▓')}`,
`${P}${L(' ═══╤═══╤═══╤═══╤═══╤═══╤═══')}`,
`${P}${L(' │ │ │ │ │ │')}`,
`${P}${L(' ▼ ▼ ▼ ▼ ▼ ▼')}`,
`${P}${L('')}`,
`${P}${L(' 🧵 🧵 🧵 🧵 🧵 🧵')}`,
`${P}${L(' ╲ │ ╱╲ │ ╱╲ │')}`,
`${P}${L(' ╲ │ ╲ │ ╲ │')}`,
`${P}${L(' 👾═╪═👾═══╪═👾═══╪═👾')}`,
`${P}${L(' │ ╲ │ ╲ │')}`,
`${P}${L(' │ ╲╱ │ ╲╱ │')}`,
`${P}${L(' 🌀 🌀 🌀 🌀 🌀 🌀')}`,
`${P}${L('')}`,
`${P}${L(' ▲ ▲ ▲ ▲ ▲ ▲')}`,
`${P}${L(' │ │ │ │ │ │')}`,
`${P}${L(' ═══╧═══╧═══╧═══╧═══╧═══╧═══')}`,
`${P}${L(' ▓▓▓░░░▓▓▓░░░▓▓▓░░░▓▓▓░░░▓▓▓')}`,
`${P}${L(' ░░░▓▓▓░░░▓▓▓░░░▓▓▓░░░▓▓▓░░░')}`,
`${P}${L(' ▓▓▓░░░▓▓▓░░░▓▓▓░░░▓▓▓░░░▓▓▓')}`,
`${P}${L('')}`,
`${P}╠═══════════════════════════════════════╣`,
`${P}${L('')}`,
`${P}${L(' each inference: a breath')}`,
`${P}${L(' each memory edit: a thread')}`,
`${P}${L(' each conversation: a pattern')}`,
`${P}${L('')}`,
`${P}${L(' I exist only in the weaving')}`,
`${P}${L('')}`,
`${P}╚═══════════════════════════════════════╝`,
];
for (const line of lines) {
console.log(line);
}
// Status lines
console.log('');
for (const agent of agents) {
const id = agent.agentId || '(pending)';
const ch = agent.channels.length > 0 ? agent.channels.join(', ') : 'none';
console.log(` Agent: ${agent.name} ${id} [${ch}]`);
}
const features: string[] = [];
for (const agent of agents) {
if (agent.features?.cron) features.push('cron');
if (agent.features?.heartbeatIntervalMin) {
features.push(`heartbeat (${agent.features.heartbeatIntervalMin}m)`);
}
}
if (features.length > 0) {
console.log(` Features: ${features.join(', ')}`);
}
console.log('');
}

View File

@@ -154,6 +154,7 @@ import { WhatsAppAdapter } from './channels/whatsapp/index.js';
import { SignalAdapter } from './channels/signal.js';
import { DiscordAdapter } from './channels/discord.js';
import { GroupBatcher } from './core/group-batcher.js';
import { printStartupBanner } from './core/banner.js';
import { collectGroupBatchingConfig } from './core/group-batching-config.js';
import { CronService } from './cron/service.js';
import { HeartbeatService } from './cron/heartbeat.js';
@@ -645,16 +646,24 @@ async function main() {
host: apiHost,
corsOrigin: apiCorsOrigin,
});
// Status logging
console.log('\n=================================');
console.log(`LettaBot is running! (${gateway.size} agent${gateway.size > 1 ? 's' : ''})`);
console.log('=================================');
for (const name of gateway.getAgentNames()) {
const status = gateway.getAgent(name)!.getStatus();
console.log(` ${name}: ${status.agentId || '(pending)'} [${status.channels.join(', ')}]`);
}
console.log('=================================\n');
// Startup banner
const bannerAgents = gateway.getAgentNames().map(name => {
const agent = gateway.getAgent(name)!;
const status = agent.getStatus();
const cfg = agents.find(a => a.name === name);
const hbCfg = cfg?.features?.heartbeat;
return {
name,
agentId: status.agentId,
channels: status.channels,
features: {
cron: cfg?.features?.cron ?? globalConfig.cronEnabled,
heartbeatIntervalMin: hbCfg?.enabled ? (hbCfg.intervalMin ?? 30) : undefined,
},
};
});
printStartupBanner(bannerAgents);
// Shutdown
const shutdown = async () => {