feat: add loom ASCII art startup banner (#270)
This commit is contained in:
1214
package-lock.json
generated
1214
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
101
src/core/banner.ts
Normal file
101
src/core/banner.ts
Normal 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('');
|
||||
}
|
||||
29
src/main.ts
29
src/main.ts
@@ -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 () => {
|
||||
|
||||
Reference in New Issue
Block a user