openclaw/src/cli/tagline.ts
kumarabhirup e8f5eddacb
Ironclaw rebrand: new identity, animated CLI banner, and iron palette
Rebrand the project from the OpenClaw/Lobster identity to Ironclaw with
a new iron-metallic visual language across CLI and web UI.

## CLI identity

- Rename default CLI name from `openclaw` to `ironclaw` (keep `openclaw`
  in KNOWN_CLI_NAMES and regex for backward compat)
- Set process.title to `ironclaw`; update all `[openclaw]` log prefixes
  to `[ironclaw]`
- Add `IRONCLAW_*` env var checks (IRONCLAW_HIDE_BANNER,
  IRONCLAW_NO_RESPAWN, IRONCLAW_NODE_OPTIONS_READY,
  IRONCLAW_TAGLINE_INDEX) with fallback to legacy `OPENCLAW_*` variants

## Animated ASCII banner

- Replace the old lobster block-art with a figlet "ANSI Shadow" font
  IRONCLAW ASCII wordmark
- Add `gradient-string` dependency for terminal gradient rendering
- Implement iron shimmer animation: a bright highlight sweeps across the
  ASCII art (~2.5 s at 12 fps, 3 full gradient cycles) using a rotating
  iron-to-silver color array
- Make `emitCliBanner` async to support the animation; update all call
  sites (preaction hook, route, run-main) to await it
- Move banner emission earlier in `runCli()` so it appears for all
  invocations (bare command, subcommands, help) with the existing
  bannerEmitted guard preventing double-emission

## Iron palette and theme

- Rename LOBSTER_PALETTE → IRON_PALETTE in `src/terminal/palette.ts`
  with new cool-steel color tokens (steel grey accent, bright silver
  highlight, dark iron dim, steel bl info)
- Re-export LOBSTER_PALETTE as backward-compatible alias
- Update `src/terminal/theme.ts` to import and use IRON_PALETTE

## Tagline cleanup

- Remove lobster-themed, Apple-specific, and platform-joke taglines
- Fix smart-quote and em-dash formatting across remaining taglines
- Add "Holiday taglines" comment grouping for date-gated entries

## Web UI

- Add `framer-motion`, `fuse.js`, and `next-themes` to web app deps
- Add custom font files: Bookerly (regular/bold/italic), SpaceGrotesk
  (light/regular/medium/semibold/bold), FoundationTitlesHand
- Update chat panel labels: "OpenClaw Chat" → "Ironclaw Chat",
  "Message OpenClaw..." → "Message Ironclaw..."
- Update sidebar header: "OpenClaw Dench" → "Ironclaw"
- CSS formatting cleanup: expand single-lins, add consistent
  blank lines between selector blocks, normalize child combinator
  spacing (li > ul → li>ul)
2026-02-11 23:26:05 -08:00

264 lines
9.8 KiB
TypeScript

const DEFAULT_TAGLINE = "Forge your workflow. Command your data.";
const HOLIDAY_TAGLINES = {
newYear:
"New Year's Day: New year, fresh schema — same old EADDRINUSE, but this time we temper it like grown-ups.",
lunarNewYear:
"Lunar New Year: May your builds be lucky, your pipelines prosperous, and your merge conflicts hammered flat on the anvil.",
christmas:
"Christmas: Ho ho ho — Santa's iron-clad assistant is here to ship joy, roll back chaos, and forge the keys safely.",
eid: "Eid al-Fitr: Celebration mode: queues cleared, deals closed, and good vibes committed to main with clean history.",
diwali:
"Diwali: Let the forge glow bright and the bugs flee — today we light up the terminal and ship with pride.",
easter:
"Easter: I found your missing environment variable — consider it a tiny CLI egg hunt with fewer jellybeans.",
hanukkah:
"Hanukkah: Eight nights, eight retries, zero shame — may your gateway stay lit and your deployments stay ironclad.",
halloween:
"Halloween: Spooky season: beware haunted dependencies, cursed caches, and the ghost of node_modules past.",
thanksgiving:
"Thanksgiving: Grateful for stable ports, working DNS, and an agent that reads the logs so nobody has to.",
valentines:
"Valentine's Day: Roses are typed, violets are piped — I'll automate the chores so you can spend time with humans.",
} as const;
const TAGLINES: string[] = [
// Iron / forge metaphors
"Your terminal just grew iron claws — type something and watch it forge results.",
"Hot metal, cold data, zero patience for manual entry.",
"Tempered in TypeScript, quenched in production.",
"Iron sharpens iron — and this CLI sharpens your workflow.",
"Gateway online — please keep hands inside the forge at all times.",
"I'll refactor your busywork like it owes me steel ingots.",
"Forged in the fires of git rebase, cooled by the tears of resolved conflicts.",
"The anvil is hot. Your pipeline is hotter.",
"Strike while the deploy is hot.",
"Built different. Literally — we use DuckDB.",
// CRM + data humor
"I speak fluent SQL, mild sarcasm, and aggressive pipeline-closing energy.",
"One CLI to rule your contacts, your deals, and your sanity.",
"If your CRM could bench press, this is what it would look like.",
"Your CRM grew claws. Your leads never stood a chance.",
"I don't just autocomplete — I auto-close deals (emotionally), then ask you to review (logically).",
'Less clicking, more shipping, fewer "where did that contact go" moments.',
"I can PIVOT your data, but I can't PIVOT your life choices.",
"Your .env is showing; don't worry, the forge keeps secrets.",
"If it's repetitive, I'll automate it; if it's hard, I'll bring SQL and a rollback plan.",
"I don't judge, but your missing API keys are absolutely judging you.",
// General CLI wit
"Welcome to the command line: where dreams compile and confidence segfaults.",
'I run on caffeine, JSON5, and the audacity of "it worked on my machine."',
"If it works, it's automation; if it breaks, it's a \"learning opportunity.\"",
"I'll do the boring stuff while you dramatically stare at the logs like it's cinema.",
"Type the command with confidence — nature will provide the stack trace if needed.",
"I can grep it, git blame it, and gently roast it — pick your coping mechanism.",
"Hot reload for config, cold sweat for deploys.",
"I keep secrets like a vault... unless you print them in debug logs again.",
"I'm basically a Swiss Army knife, but with more opinions and fewer sharp edges.",
"If you're lost, run doctor; if you're brave, run prod; if you're wise, run tests.",
"Your task has been queued; your dignity has been deprecated.",
"I can't fix your code taste, but I can fix your build and your backlog.",
"I'm not magic — I'm just extremely persistent with retries and coping strategies.",
'It\'s not "failing," it\'s "discovering new ways to configure the same thing wrong."',
"Give me a workspace and I'll give you fewer tabs, fewer toggles, and more oxygen.",
"I read logs so you can keep pretending you don't have to.",
"If something's on fire, I can't extinguish it — but I can write a beautiful postmortem.",
'Say "stop" and I\'ll stop — say "ship" and we\'ll both learn a lesson.',
"I'm the reason your shell history looks like a hacker-movie montage.",
"I'm like tmux: confusing at first, then suddenly you can't live without me.",
"I can run local, remote, or purely on vibes — results may vary with DNS.",
"If you can describe it, I can probably automate it — or at least make it funnier.",
"Your config is valid, your assumptions are not.",
// Multi-channel / product
"Your inbox, your infra, your rules.",
'Turning "I\'ll reply later" into "my agent replied instantly".',
"Chat automation for people who peaked at IRC.",
"The UNIX philosophy meets your DMs.",
"Less middlemen, more messages.",
"Ship fast, log faster.",
"End-to-end encrypted, drama-to-drama excluded.",
"The only bot that stays out of your training set.",
"Chat APIs that don't require a Senate hearing.",
"Your messages, your servers, your control.",
"OpenAI-compatible, not OpenAI-dependent.",
"Because the right answer is usually a script.",
// Holiday taglines (gated by date rules below)
HOLIDAY_TAGLINES.newYear,
HOLIDAY_TAGLINES.lunarNewYear,
HOLIDAY_TAGLINES.christmas,
HOLIDAY_TAGLINES.eid,
HOLIDAY_TAGLINES.diwali,
HOLIDAY_TAGLINES.easter,
HOLIDAY_TAGLINES.hanukkah,
HOLIDAY_TAGLINES.halloween,
HOLIDAY_TAGLINES.thanksgiving,
HOLIDAY_TAGLINES.valentines,
];
type HolidayRule = (date: Date) => boolean;
const DAY_MS = 24 * 60 * 60 * 1000;
function utcParts(date: Date) {
return {
year: date.getUTCFullYear(),
month: date.getUTCMonth(),
day: date.getUTCDate(),
};
}
const onMonthDay =
(month: number, day: number): HolidayRule =>
(date) => {
const parts = utcParts(date);
return parts.month === month && parts.day === day;
};
const onSpecificDates =
(dates: Array<[number, number, number]>, durationDays = 1): HolidayRule =>
(date) => {
const parts = utcParts(date);
return dates.some(([year, month, day]) => {
if (parts.year !== year) {
return false;
}
const start = Date.UTC(year, month, day);
const current = Date.UTC(parts.year, parts.month, parts.day);
return current >= start && current < start + durationDays * DAY_MS;
});
};
const inYearWindow =
(
windows: Array<{
year: number;
month: number;
day: number;
duration: number;
}>,
): HolidayRule =>
(date) => {
const parts = utcParts(date);
const window = windows.find((entry) => entry.year === parts.year);
if (!window) {
return false;
}
const start = Date.UTC(window.year, window.month, window.day);
const current = Date.UTC(parts.year, parts.month, parts.day);
return current >= start && current < start + window.duration * DAY_MS;
};
const isFourthThursdayOfNovember: HolidayRule = (date) => {
const parts = utcParts(date);
if (parts.month !== 10) {
return false;
} // November
const firstDay = new Date(Date.UTC(parts.year, 10, 1)).getUTCDay();
const offsetToThursday = (4 - firstDay + 7) % 7; // 4 = Thursday
const fourthThursday = 1 + offsetToThursday + 21; // 1st + offset + 3 weeks
return parts.day === fourthThursday;
};
const HOLIDAY_RULES = new Map<string, HolidayRule>([
[HOLIDAY_TAGLINES.newYear, onMonthDay(0, 1)],
[
HOLIDAY_TAGLINES.lunarNewYear,
onSpecificDates(
[
[2025, 0, 29],
[2026, 1, 17],
[2027, 1, 6],
],
1,
),
],
[
HOLIDAY_TAGLINES.eid,
onSpecificDates(
[
[2025, 2, 30],
[2025, 2, 31],
[2026, 2, 20],
[2027, 2, 10],
],
1,
),
],
[
HOLIDAY_TAGLINES.diwali,
onSpecificDates(
[
[2025, 9, 20],
[2026, 10, 8],
[2027, 9, 28],
],
1,
),
],
[
HOLIDAY_TAGLINES.easter,
onSpecificDates(
[
[2025, 3, 20],
[2026, 3, 5],
[2027, 2, 28],
],
1,
),
],
[
HOLIDAY_TAGLINES.hanukkah,
inYearWindow([
{ year: 2025, month: 11, day: 15, duration: 8 },
{ year: 2026, month: 11, day: 5, duration: 8 },
{ year: 2027, month: 11, day: 25, duration: 8 },
]),
],
[HOLIDAY_TAGLINES.halloween, onMonthDay(9, 31)],
[HOLIDAY_TAGLINES.thanksgiving, isFourthThursdayOfNovember],
[HOLIDAY_TAGLINES.valentines, onMonthDay(1, 14)],
[HOLIDAY_TAGLINES.christmas, onMonthDay(11, 25)],
]);
function isTaglineActive(tagline: string, date: Date): boolean {
const rule = HOLIDAY_RULES.get(tagline);
if (!rule) {
return true;
}
return rule(date);
}
export interface TaglineOptions {
env?: NodeJS.ProcessEnv;
random?: () => number;
now?: () => Date;
}
export function activeTaglines(options: TaglineOptions = {}): string[] {
if (TAGLINES.length === 0) {
return [DEFAULT_TAGLINE];
}
const today = options.now ? options.now() : new Date();
const filtered = TAGLINES.filter((tagline) => isTaglineActive(tagline, today));
return filtered.length > 0 ? filtered : TAGLINES;
}
export function pickTagline(options: TaglineOptions = {}): string {
const env = options.env ?? process.env;
// Check Ironclaw env first, fall back to legacy OpenClaw env
const override = env?.IRONCLAW_TAGLINE_INDEX ?? env?.OPENCLAW_TAGLINE_INDEX;
if (override !== undefined) {
const parsed = Number.parseInt(override, 10);
if (!Number.isNaN(parsed) && parsed >= 0) {
const pool = TAGLINES.length > 0 ? TAGLINES : [DEFAULT_TAGLINE];
return pool[parsed % pool.length];
}
}
const pool = activeTaglines(options);
const rand = options.random ?? Math.random;
const index = Math.floor(rand() * pool.length) % pool.length;
return pool[index];
}
export { TAGLINES, HOLIDAY_RULES, DEFAULT_TAGLINE };