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)
This commit is contained in:
kumarabhirup 2026-02-11 23:26:05 -08:00
parent 7c9f7aa2d2
commit e8f5eddacb
No known key found for this signature in database
GPG Key ID: DB7CA2289CAB0167
26 changed files with 631 additions and 167 deletions

View File

@ -0,0 +1,324 @@
---
name: Full Web UI Redesign
overview: "Complete redesign of the OpenClaw web app (apps/web/) to match the Dench design system: switch from dark theme to light, adopt Instrument Serif + Inter fonts, port the Dench color palette and layout patterns, and rewrite every component and page from the ground up."
todos:
- id: foundation
content: "Phase 1: Rewrite globals.css (light theme, HSL tokens, font imports) and layout.tsx (next/font, remove dark mode)"
status: pending
- id: landing
content: "Phase 2: Rewrite app/page.tsx as Dench-style landing page (navbar, hero, demo sections, footer)"
status: pending
- id: layout-shell
content: "Phase 3: Create app-navbar.tsx, rewrite workspace/page.tsx layout with top navbar + sidebar grid"
status: pending
- id: sidebar
content: "Phase 4: Redesign workspace-sidebar.tsx and file-manager-tree.tsx to match Dench sidebar"
status: pending
- id: data-table
content: "Phase 5: Redesign object-table.tsx with Dench-style toolbar, sticky headers, pagination, enum badges"
status: pending
- id: kanban
content: "Phase 6: Redesign object-kanban.tsx with light cards, columns, board header"
status: pending
- id: entry-detail
content: "Phase 7: Redesign entry-detail-modal.tsx as right-panel slide-out with properties list"
status: pending
- id: dashboard-chat
content: "Phase 8a: Build dashboard view with greeting, centered chat input, suggestion chips, and animate-down-to-bottom Framer Motion layoutId transition"
status: pending
- id: chat
content: "Phase 8b: Restyle chat-panel.tsx, chat-message.tsx, chain-of-thought.tsx for light theme + bottom composer"
status: pending
- id: remaining
content: "Phase 9: Restyle all remaining components (breadcrumbs, document-view, file-viewer, database-viewer, empty-state, markdown, context-menu, slash-command, charts, etc.)"
status: pending
- id: deps
content: "Phase 10: Add framer-motion dependency, verify fonts work, test build"
status: pending
isProject: false
---
# Full Web UI Redesign — Dench Design System
## Current State
The OpenClaw web app is a **dark-themed** Next.js 15 app with:
- Dark background (`#0a0a0a`), dark surfaces (`#141414`), orange accent (`#e85d3a`)
- Inter font only, no serif headings
- Minimal homepage (centered text + CTA)
- Workspace layout: left sidebar (260px) + content + optional chat panel
- Custom table/kanban/viewer components, all dark-styled
- Tailwind v4 (CSS-based config), no shadcn/ui
## Target State (Dench Design)
Per the screenshots and Dench source:
- **Light theme**`bg-neutral-50` layout, white cards, `bg-neutral-100` sidebar/navbar
- **Instrument Serif** for headings/titles, **Inter** for body text, **Lora** for branding
- **Top navbar** (grid 3-col, with Dashboard/Workflows/Integrations tabs, org logo, user menu)
- **Left sidebar** (260px, `bg-neutral-100`, collapsible knowledge tree with item counts)
- **Data tables** with: sticky header, column borders, search bar, filter/column controls, enum badges, relation chips, pagination
- **Kanban board** with rounded cards, priority badges, assignee avatars
- **Entry detail** right-panel slide-out with property list
- **Landing page** with hero section, demo sections, clean navigation bar
- **Dashboard chat UX** — centered greeting ("Good evening, Kumar?") in Instrument Serif + centered chat input with suggestion chips; on first message, the input animates down to a bottom-docked composer via Framer Motion shared `layoutId` spring transition
- HSL-based CSS variables (shadcn pattern), `--radius: 0.5rem`, neutral base color
## Architecture Decision: Tailwind v4
The OpenClaw app uses **Tailwind v4** (CSS-based config via `@import "tailwindcss"`), while Dench uses Tailwind v3 (JS config). We will keep Tailwind v4 but port all design tokens into `globals.css` using `@theme` blocks and CSS custom properties. No downgrade needed.
## Architecture Decision: Light + Dark Theme
Dench is light-only. We will use Dench's light palette as the `:root` default AND create a custom dark palette under `.dark` (class-based toggle via `<html class="dark">`). All components will use CSS variable references (e.g. `bg-background`, `text-foreground`, `border-border`) so they automatically adapt. No hardcoded hex/rgb in components.
**Light palette** (from Dench):
- `--background: 0 0% 96%` (neutral-50 feel)
- `--foreground: 0 0% 3.9%`
- `--card: 0 0% 100%` / `--card-foreground: 0 0% 3.9%`
- `--muted: 0 0% 96.1%` / `--muted-foreground: 0 0% 45.1%`
- `--border: 0 0% 89.8%`
- `--primary: 0 0% 9%` / `--primary-foreground: 0 0% 98%`
- `--accent: 0 0% 96.1%` / `--accent-foreground: 0 0% 9%`
- `--destructive: 0 84.2% 60.2%`
**Dark palette** (custom, designed to complement Dench's light theme):
- `--background: 0 0% 7%` (#121212 — rich near-black, not pure black)
- `--foreground: 0 0% 93%` (#ededed)
- `--card: 0 0% 10%` (#1a1a1a) / `--card-foreground: 0 0% 93%`
- `--muted: 0 0% 14%` (#242424) / `--muted-foreground: 0 0% 55%` (#8c8c8c)
- `--border: 0 0% 18%` (#2e2e2e)
- `--primary: 0 0% 93%` / `--primary-foreground: 0 0% 9%`
- `--accent: 0 0% 16%` (#292929) / `--accent-foreground: 0 0% 93%`
- `--destructive: 0 62% 55%`
- Sidebar: `--sidebar-bg: 0 0% 9%` (#171717)
- Navbar: similar to sidebar, subtle `border-b` at `--border`
Sidebar/navbar in dark mode use a slightly elevated surface (`#171717`) rather than pure background, for depth.
**Theme toggle:** add a sun/moon toggle button in the navbar (right side, near user avatar). Use `next-themes` or a simple `useEffect` + `localStorage` approach to persist preference and apply `.dark` class on `<html>`.
---
## Files to Change
### Phase 1 — Foundation (Theme, Fonts, Layout Shell)
**[app/globals.css](apps/web/app/globals.css)** — Complete rewrite:
- `:root` block: Dench's light-theme HSL palette (background, foreground, card, primary, secondary, muted, accent, destructive, border, ring, sidebar, chart-1 through chart-5)
- `.dark` block: custom dark palette (see "Architecture Decision: Light + Dark Theme" above) — all same variable names, dark values
- Add `@theme` block for Tailwind v4 mapping CSS vars to utility classes (`bg-background`, `text-foreground`, `border-border`, `bg-card`, `text-muted-foreground`, etc.)
- Import Instrument Serif from Google Fonts
- Add `.font-instrument` utility class
- Port scrollbar, prose, editor, and slash-command styles using CSS variables (theme-aware, not hardcoded)
- Port workflow state colors (`--workflow-active`, `--workflow-processing`, `--workflow-idle`)
**[app/layout.tsx](apps/web/app/layout.tsx)** — Rewrite:
- Import Inter and Lora via `next/font/google`
- Set CSS variables `--font-corporate` and `--font-lora`
- Default to light: no `className="dark"` on `<html>` (let theme provider handle it)
- Apply `font-corporate` to `<body>`
- Add `suppressHydrationWarning` on `<html>` for theme flash prevention
- Add inline script or `next-themes` `ThemeProvider` for class-based dark mode toggle with `localStorage` persistence
- Update metadata title/description to "Dench" branding
**New: `app/hooks/use-theme.ts**` — Simple theme hook:
- Read/write `localStorage` key `"theme"` (`"light"` | `"dark"` | `"system"`)
- Apply/remove `.dark` class on `document.documentElement`
- Expose `theme`, `setTheme`, `resolvedTheme` for components
### Phase 2 — Landing Page
**[app/page.tsx](apps/web/app/page.tsx)** — Full rewrite to match Dench landing:
- Sticky navigation bar (logo "Dench" in `font-lora`, Login button in rounded-full blue pill)
- Hero section: "AI CRM" headline in `font-instrument font-bold`, subtext, "Get Started Free" CTA
- Full-width CRM demo area (window chrome with traffic-light dots, scaled mock table)
- Additional demo sections (workflow, kanban) — simplified versions
- Footer with copyright, links
### Phase 3 — Workspace Layout Shell
**[app/workspace/page.tsx](apps/web/app/workspace/page.tsx)** — Rewrite layout structure:
- Add top `AppNavbar` component: `bg-neutral-100 border-b border-border shadow-[0_0_40px_rgba(0,0,0,0.05)]`
- Left: org logo + "Powered by Dench" + org name in `font-instrument`
- Center: tab navigation (Dashboard, Workflows, Integrations) with active state
- Right: credit display, notification bell, sun/moon theme toggle, user avatar dropdown
- Main area: `grid lg:grid-cols-[260px_1fr]` under navbar
- Full height: `h-[100dvh] flex flex-col bg-neutral-50`
- Content area: `overflow-y-auto overflow-x-hidden`
- Replace all inline `style={{}}` dark colors with Tailwind classes
**New component: `app/components/workspace/app-navbar.tsx**` — Top navbar (extracted for reuse)
### Phase 4 — Sidebar Redesign
**[app/components/workspace/workspace-sidebar.tsx](apps/web/app/components/workspace/workspace-sidebar.tsx)** — Full rewrite:
- Background: `bg-sidebar` with `border-r border-border` (light: neutral-100, dark: #171717 via CSS var)
- Shadow: theme-aware subtle shadow
- Header: "KNOWLEDGE" section label in uppercase `text-[11px] font-medium tracking-wider text-muted-foreground`
- Knowledge items: `text-[13px]`, hover `bg-accent`, `rounded-xl`
- Item badges showing entry counts in `bg-muted border border-border` pills
- Icons per item type (objects get custom icons, documents get doc icon)
- Collapsible sections: KNOWLEDGE, CHATS, TELEPHONY
- Bottom: "API Keys" link
- Remove all inline `style={{}}` dark colors
**[app/components/workspace/file-manager-tree.tsx](apps/web/app/components/workspace/file-manager-tree.tsx)** — Restyle tree items:
- Light-theme hover states, active states matching `bg-neutral-200`
- `text-[13px]` sizing, proper icon colors
- Drag-and-drop visual indicators in light theme
### Phase 5 — Data Table Redesign
**[app/components/workspace/object-table.tsx](apps/web/app/components/workspace/object-table.tsx)** — Complete rewrite to match Dench data-table:
- Toolbar: object name in `font-instrument`, search input (`rounded-full shadow-[0_0_21px_0_rgba(0,0,0,0.07)]`), "Ask AI" button, Table/Board view toggle, refresh/import/filter/columns/+ Add buttons
- Table header: `sticky top-0 z-30 bg-card border-b-2 border-border/80`, sortable columns with sort arrows
- Table cells: `px-4 border-r border-border/30`, proper text truncation
- Enum badges: colored pill style matching Dench (translucent background + border)
- Relation chips: link icon + blue text
- Row hover: `hover:bg-muted/50`
- Pagination bar: "Showing 1 to N of N results", rows-per-page selector, page navigation
- "..." action menu per row (right column)
### Phase 6 — Kanban Board Redesign
**[app/components/workspace/object-kanban.tsx](apps/web/app/components/workspace/object-kanban.tsx)** — Rewrite:
- Board header: view toggle (Board/Table), "Ask AI" button, search, "Group by" selector
- Columns: `bg-muted/50 rounded-2xl border border-border/60`, column title + count badge
- Cards: `bg-card rounded-xl border border-border/80 shadow-sm`
- Card content: title, field badges (objective, risk profile), date, assignee avatar
- "+ Add Item" at column bottom
- "Drop cards here" empty column placeholder
### Phase 7 — Entry Detail Panel
**[app/components/workspace/entry-detail-modal.tsx](apps/web/app/components/workspace/entry-detail-modal.tsx)** — Redesign as right-panel slide-out:
- Takes ~40% of content width, pushes table left
- Header: icon + title in large font, "Created Jan 12, 2026 at 12:47 PM" subtitle
- "PROPERTIES" section label
- Property rows: label (uppercase text-xs text-muted-foreground) + value
- Relation fields show colored link chips
- Enum fields show colored badges (matching table)
- "Add a property" at bottom
- Close button (>> icon) top-right
### Phase 8a — Dashboard Chat UX (Greeting + Animate-to-Bottom Input)
This is the hero interaction on the workspace "Dashboard" tab — a centered greeting with a chat input that transitions into the bottom-docked composer after the first message.
**How Dench implements it:**
- `DashboardHeader`: time-based greeting ("Good morning/afternoon/evening, Name?") with staggered word-by-word Framer Motion entrance (`y:20 → 0`, `blur(8px) → blur(0)`)
- `DashboardChatbox`: centered TipTap input with placeholder "Build a workflow to automate your tasks", attach/voice/submit buttons, suggestion chips below (shuffled from a pool of ~27 templates, showing 7 in two rows)
- **Layout animation:** both the centered input and the bottom composer share a Framer Motion `layoutId="chat-thread-composer"`. When `showStartComposer` flips to false after the first message, Framer Motion automatically animates the input from center to bottom with `transition={{ type: "spring", stiffness: 260, damping: 30 }}`
**New components to create:**
`app/components/workspace/dashboard-view.tsx` — Dashboard home view:
- Greeting in `font-instrument text-4xl` with time-based message + user name
- Word-by-word staggered Framer Motion entrance animation
- Centered chat input area below greeting
`app/components/workspace/dashboard-chatbox.tsx` — Centered input + chips:
- Rounded white card with subtle shadow, textarea/input with placeholder
- Attach (paperclip), voice (mic), submit (arrow) icon buttons
- Suggestion chip rows: 3 on first row, 4 on second row, each with icon + label + `rounded-xl` border
- Accepts `layoutId` prop for shared layout animation
- `mode` prop: `"dashboard"` (centered, with greeting) vs `"thread"` (same input but used within chat thread)
- Entry animation: `opacity: 0, y: 20``opacity: 1, y: 0`, duration 0.8s
**Modify [app/workspace/page.tsx](apps/web/app/workspace/page.tsx):**
- When no content selected (Dashboard tab active), render `DashboardView`
- On chat submit: transition to chat thread view
- Use `LayoutGroup` from Framer Motion to wrap the dashboard + chat area
- Track `showStartComposer` state: when true, show centered `DashboardChatbox`; when false, show messages + bottom `ChatComposer` — both sharing the same `layoutId`
**Prompt templates** (simplified set for OpenClaw):
- Follow-up Emails, Calendly Prep, Zoom Recap, Facebook Leads, Calendar Sync, Salesforce Sync, Intercom Chat (matching the Dench screenshot chips)
### Phase 8b — Chat & Message Restyling
**[app/components/chat-panel.tsx](apps/web/app/components/chat-panel.tsx)** — Restyle:
- Theme-aware background (`bg-card`), card-colored input area
- Input: rounded border, subtle shadow, consistent with dashboard chatbox style
- Bottom-docked composer with `layoutId` for shared animation
- Session tabs in light theme
- Tool call indicators in light theme
- Send button styling (rounded, neutral)
**[app/components/chat-message.tsx](apps/web/app/components/chat-message.tsx)** — Restyle:
- Theme-aware message bubbles (user: `bg-muted`, assistant: `bg-card`)
- Code blocks with `bg-muted`
- Markdown rendering in light theme
- Chain-of-thought styling update
**[app/components/chain-of-thought.tsx](apps/web/app/components/chain-of-thought.tsx)** — Light theme
### Phase 9 — Remaining Components
All components below: replace every hardcoded color (`style={{}}`, hex, rgb) with semantic Tailwind utilities (`bg-background`, `text-foreground`, `border-border`, `bg-card`, `text-muted-foreground`, `bg-muted`, etc.) so they work in both light and dark:
- **[breadcrumbs.tsx](apps/web/app/components/workspace/breadcrumbs.tsx)** — `text-muted-foreground`, `hover:text-foreground`
- **[document-view.tsx](apps/web/app/components/workspace/document-view.tsx)** — `bg-card` background, `border-border`
- **[file-viewer.tsx](apps/web/app/components/workspace/file-viewer.tsx)** — `bg-muted` code blocks, `text-foreground`
- **[database-viewer.tsx](apps/web/app/components/workspace/database-viewer.tsx)** — `bg-card` tables, `bg-muted` query editor
- **[empty-state.tsx](apps/web/app/components/workspace/empty-state.tsx)** — `text-muted-foreground` illustration
- **[markdown-content.tsx](apps/web/app/components/workspace/markdown-content.tsx)** — Prose styles via CSS vars
- **[markdown-editor.tsx](apps/web/app/components/workspace/markdown-editor.tsx)** — `bg-card` editor chrome
- **[context-menu.tsx](apps/web/app/components/workspace/context-menu.tsx)** — `bg-card` dropdown, `border-border`
- **[slash-command.tsx](apps/web/app/components/workspace/slash-command.tsx)** — `bg-card` command palette
- **[inline-rename.tsx](apps/web/app/components/workspace/inline-rename.tsx)** — `bg-card` input, `border-border`
- **[knowledge-tree.tsx](apps/web/app/components/workspace/knowledge-tree.tsx)** — Theme-aware tree styles
- **[charts/](apps/web/app/components/charts/)** — All chart components: CSS var chart colors, `bg-card` panels
- **[sidebar.tsx](apps/web/app/components/sidebar.tsx)** — Theme-aware (if still used)
### Phase 10 — Package Dependencies
**[package.json](apps/web/package.json)** — Add if needed:
- `framer-motion` (for landing page + dashboard chat animations)
- `next-themes` (for dark/light toggle with `localStorage` + class-based switching, SSR-safe)
- Verify `next/font/google` is available (bundled with Next.js)
---
## Key Design Tokens
- **Radius:** `0.5rem` base
- **Primary font:** Inter via `next/font/google`
- **Heading font:** Instrument Serif via Google Fonts import
- **Brand font:** Lora via `next/font/google`
- **Sidebar width:** 260px
- **Shadows (light):** `shadow-[0_0_40px_rgba(0,0,0,0.05)]` (sidebar/navbar), `shadow-[0_0_21px_0_rgba(0,0,0,0.07)]` (search)
- **Shadows (dark):** `shadow-[0_0_40px_rgba(0,0,0,0.2)]` (sidebar/navbar), `shadow-[0_0_21px_0_rgba(0,0,0,0.15)]` (search)
## Component Styling Rules (Theme-Safe)
All components MUST use semantic CSS variable-backed utilities — never hardcoded colors:
- `bg-background` / `bg-card` / `bg-muted` / `bg-accent` — not `bg-white`, `bg-neutral-50`, `bg-[#1a1a1a]`
- `text-foreground` / `text-muted-foreground` / `text-card-foreground` — not `text-black`, `text-gray-500`
- `border-border` — not `border-neutral-200`, `border-[#2e2e2e]`
- `bg-sidebar` for sidebar/navbar backgrounds
- For shadows that differ between themes: use a CSS variable `--shadow-subtle` / `--shadow-elevated` or conditional `dark:shadow-*` utilities
- Exceptions: Dench-specific decorative elements (landing page traffic-light dots, brand colors) can use fixed values

View File

@ -657,7 +657,7 @@ export const ChatPanel = forwardRef<ChatPanelHandle, ChatPanelProps>(
🦞
</p>
<h3 className="text-lg font-semibold mb-1">
OpenClaw Chat
Ironclaw Chat
</h3>
<p
className="text-sm"
@ -740,7 +740,7 @@ export const ChatPanel = forwardRef<ChatPanelHandle, ChatPanelProps>(
placeholder={
compact && fileContext
? `Ask about ${fileContext.filename}...`
: "Message OpenClaw..."
: "Message Ironclaw..."
}
disabled={
isStreaming ||

View File

@ -402,7 +402,7 @@ export function Sidebar({
{/* Header with New Chat button */}
<div className="px-4 py-4 border-b border-[var(--color-border)] flex items-center justify-between">
<h1 className="text-base font-bold flex items-center gap-2">
<span>OpenClaw Dench</span>
<span>Ironclaw</span>
</h1>
<button
onClick={onNewSession}

View File

@ -27,13 +27,16 @@ body {
::-webkit-scrollbar {
width: 6px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: var(--color-border);
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--color-text-muted);
}
@ -66,13 +69,20 @@ body {
border-bottom: 1px solid var(--color-border);
padding-bottom: 0.5rem;
}
.workspace-prose h2 {
font-size: 1.375rem;
border-bottom: 1px solid var(--color-border);
padding-bottom: 0.4rem;
}
.workspace-prose h3 { font-size: 1.125rem; }
.workspace-prose h4 { font-size: 1rem; }
.workspace-prose h3 {
font-size: 1.125rem;
}
.workspace-prose h4 {
font-size: 1rem;
}
.workspace-prose p {
margin-bottom: 1em;
@ -83,6 +93,7 @@ body {
text-decoration: underline;
text-underline-offset: 2px;
}
.workspace-prose a:hover {
color: #93bbfd;
}
@ -114,8 +125,8 @@ body {
margin-bottom: 0.25em;
}
.workspace-prose li > ul,
.workspace-prose li > ol {
.workspace-prose li>ul,
.workspace-prose li>ol {
margin-top: 0.25em;
margin-bottom: 0;
}
@ -305,7 +316,7 @@ body {
transform: rotate(45deg);
}
.editor-content-area .tiptap ul[data-type="taskList"] li > div {
.editor-content-area .tiptap ul[data-type="taskList"] li>div {
flex: 1;
}

View File

@ -27,7 +27,10 @@
"@tiptap/starter-kit": "^3.19.0",
"@tiptap/suggestion": "^3.19.0",
"ai": "^6.0.73",
"framer-motion": "^12.34.0",
"fuse.js": "^7.1.0",
"next": "^15.3.3",
"next-themes": "^0.4.6",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-markdown": "^10.1.0",
@ -2755,6 +2758,33 @@
}
}
},
"node_modules/framer-motion": {
"version": "12.34.0",
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.34.0.tgz",
"integrity": "sha512-+/H49owhzkzQyxtn7nZeF4kdH++I2FWrESQ184Zbcw5cEqNHYkE5yxWxcTLSj5lNx3NWdbIRy5FHqUvetD8FWg==",
"license": "MIT",
"dependencies": {
"motion-dom": "^12.34.0",
"motion-utils": "^12.29.2",
"tslib": "^2.4.0"
},
"peerDependencies": {
"@emotion/is-prop-valid": "*",
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@emotion/is-prop-valid": {
"optional": true
},
"react": {
"optional": true
},
"react-dom": {
"optional": true
}
}
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@ -2770,6 +2800,15 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/fuse.js": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-7.1.0.tgz",
"integrity": "sha512-trLf4SzuuUxfusZADLINj+dE8clK1frKdmqiJNb1Es75fmI5oY6X2mxLVUciLLjxqw/xr72Dhy+lER6dGd02FQ==",
"license": "Apache-2.0",
"engines": {
"node": ">=10"
}
},
"node_modules/hast-util-to-jsx-runtime": {
"version": "2.3.6",
"resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz",
@ -3814,6 +3853,21 @@
],
"license": "MIT"
},
"node_modules/motion-dom": {
"version": "12.34.0",
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.34.0.tgz",
"integrity": "sha512-Lql3NuEcScRDxTAO6GgUsRHBZOWI/3fnMlkMcH5NftzcN37zJta+bpbMAV9px4Nj057TuvRooMK7QrzMCgtz6Q==",
"license": "MIT",
"dependencies": {
"motion-utils": "^12.29.2"
}
},
"node_modules/motion-utils": {
"version": "12.29.2",
"resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.29.2.tgz",
"integrity": "sha512-G3kc34H2cX2gI63RqU+cZq+zWRRPSsNIOjpdl9TN4AQwC4sgwYPl/Q/Obf/d53nOm569T0fYK+tcoSV50BWx8A==",
"license": "MIT"
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@ -3843,6 +3897,16 @@
"resolved": "../../node_modules/.pnpm/next@15.3.3_react-dom@19.1.0_react@19.1.0/node_modules/next",
"link": true
},
"node_modules/next-themes": {
"version": "0.4.6",
"resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz",
"integrity": "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==",
"license": "MIT",
"peerDependencies": {
"react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc"
}
},
"node_modules/obug": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz",

View File

@ -29,7 +29,10 @@
"@tiptap/starter-kit": "^3.19.0",
"@tiptap/suggestion": "^3.19.0",
"ai": "^6.0.73",
"framer-motion": "^12.34.0",
"fuse.js": "^7.1.0",
"next": "^15.3.3",
"next-themes": "^0.4.6",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-markdown": "^10.1.0",

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -152,6 +152,7 @@
"dotenv": "^17.2.4",
"express": "^5.2.1",
"file-type": "^21.3.0",
"gradient-string": "^3.0.0",
"grammy": "^1.39.3",
"hono": "4.11.9",
"jiti": "^2.6.1",

View File

@ -1,3 +1,4 @@
import gradient from "gradient-string";
import { resolveCommitHash } from "../infra/git-commit.js";
import { visibleWidth } from "../terminal/ansi.js";
import { isRich, theme } from "../terminal/theme.js";
@ -12,103 +13,130 @@ type BannerOptions = TaglineOptions & {
let bannerEmitted = false;
const graphemeSegmenter =
typeof Intl !== "undefined" && "Segmenter" in Intl
? new Intl.Segmenter(undefined, { granularity: "grapheme" })
: null;
function splitGraphemes(value: string): string[] {
if (!graphemeSegmenter) {
return Array.from(value);
}
try {
return Array.from(graphemeSegmenter.segment(value), (seg) => seg.segment);
} catch {
return Array.from(value);
}
}
const hasJsonFlag = (argv: string[]) =>
argv.some((arg) => arg === "--json" || arg.startsWith("--json="));
const hasVersionFlag = (argv: string[]) =>
argv.some((arg) => arg === "--version" || arg === "-V" || arg === "-v");
// ---------------------------------------------------------------------------
// IRONCLAW ASCII art (figlet "ANSI Shadow" font, baked at build time)
// ---------------------------------------------------------------------------
const IRONCLAW_ASCII = [
" ██╗██████╗ ██████╗ ███╗ ██╗ ██████╗██╗ █████╗ ██╗ ██╗",
" ██║██╔══██╗██╔═══██╗████╗ ██║██╔════╝██║ ██╔══██╗██║ ██║",
" ██║██████╔╝██║ ██║██╔██╗ ██║██║ ██║ ███████║██║ █╗ ██║",
" ██║██╔══██╗██║ ██║██║╚██╗██║██║ ██║ ██╔══██║██║███╗██║",
" ██║██║ ██║╚██████╔╝██║ ╚████║╚██████╗███████╗██║ ██║╚███╔███╔╝",
" ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═══╝ ╚═════╝╚══════╝╚═╝ ╚═╝ ╚══╝╚══╝ ",
];
// ---------------------------------------------------------------------------
// Iron-metallic gradient colors (dark iron → bright silver → dark iron)
// ---------------------------------------------------------------------------
const IRON_GRADIENT_COLORS = [
"#374151", // dark iron
"#4B5563",
"#6B7280", // medium iron
"#9CA3AF", // steel
"#D1D5DB", // bright silver
"#F3F4F6", // near-white highlight
"#D1D5DB",
"#9CA3AF",
"#6B7280",
"#4B5563",
];
// ---------------------------------------------------------------------------
// Gradient animation helpers
// ---------------------------------------------------------------------------
function rotateArray<T>(arr: T[], offset: number): T[] {
const n = arr.length;
const o = ((offset % n) + n) % n;
return [...arr.slice(o), ...arr.slice(0, o)];
}
function renderGradientFrame(lines: string[], frame: number): string {
const colors = rotateArray(IRON_GRADIENT_COLORS, frame);
return gradient(colors).multiline(lines.join("\n"));
}
const sleep = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms));
/**
* Play the iron shimmer animation: a bright highlight sweeps across the
* ASCII art like light glinting off polished metal. Runs for ~2.5 seconds
* at 12 fps, completing 3 full gradient cycles.
*/
async function animateIronBanner(): Promise<void> {
const lineCount = IRONCLAW_ASCII.length;
const fps = 12;
const totalFrames = IRON_GRADIENT_COLORS.length * 3; // 3 full shimmer sweeps
const frameMs = Math.round(1000 / fps);
// Print the first frame to claim vertical space
process.stdout.write(renderGradientFrame(IRONCLAW_ASCII, 0) + "\n");
for (let frame = 1; frame < totalFrames; frame++) {
await sleep(frameMs);
// Move cursor up to overwrite the previous frame
process.stdout.write(`\x1b[${lineCount}A\r`);
process.stdout.write(renderGradientFrame(IRONCLAW_ASCII, frame) + "\n");
}
}
// ---------------------------------------------------------------------------
// Static (non-animated) banner rendering
// ---------------------------------------------------------------------------
export function formatCliBannerArt(options: BannerOptions = {}): string {
const rich = options.richTty ?? isRich();
if (!rich) {
return IRONCLAW_ASCII.join("\n");
}
return renderGradientFrame(IRONCLAW_ASCII, 0);
}
// ---------------------------------------------------------------------------
// One-line version + tagline (prints below the ASCII art)
// ---------------------------------------------------------------------------
export function formatCliBannerLine(version: string, options: BannerOptions = {}): string {
const commit = options.commit ?? resolveCommitHash({ env: options.env });
const commitLabel = commit ?? "unknown";
const tagline = pickTagline(options);
const rich = options.richTty ?? isRich();
const title = "🦞 OpenClaw";
const prefix = "🦞 ";
const title = "IRONCLAW";
const prefix = " ";
const columns = options.columns ?? process.stdout.columns ?? 120;
const plainFullLine = `${title} ${version} (${commitLabel}) — ${tagline}`;
const plainFullLine = `${prefix}${title} ${version} (${commitLabel}) — ${tagline}`;
const fitsOnOneLine = visibleWidth(plainFullLine) <= columns;
if (rich) {
if (fitsOnOneLine) {
return `${theme.heading(title)} ${theme.info(version)} ${theme.muted(
return `${prefix}${theme.heading(title)} ${theme.info(version)} ${theme.muted(
`(${commitLabel})`,
)} ${theme.muted("—")} ${theme.accentDim(tagline)}`;
}
const line1 = `${theme.heading(title)} ${theme.info(version)} ${theme.muted(
const line1 = `${prefix}${theme.heading(title)} ${theme.info(version)} ${theme.muted(
`(${commitLabel})`,
)}`;
const line2 = `${" ".repeat(prefix.length)}${theme.accentDim(tagline)}`;
const line2 = `${prefix}${theme.accentDim(tagline)}`;
return `${line1}\n${line2}`;
}
if (fitsOnOneLine) {
return plainFullLine;
}
const line1 = `${title} ${version} (${commitLabel})`;
const line2 = `${" ".repeat(prefix.length)}${tagline}`;
const line1 = `${prefix}${title} ${version} (${commitLabel})`;
const line2 = `${prefix}${tagline}`;
return `${line1}\n${line2}`;
}
const LOBSTER_ASCII = [
"▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄",
"██░▄▄▄░██░▄▄░██░▄▄▄██░▀██░██░▄▄▀██░████░▄▄▀██░███░██",
"██░███░██░▀▀░██░▄▄▄██░█░█░██░█████░████░▀▀░██░█░█░██",
"██░▀▀▀░██░█████░▀▀▀██░██▄░██░▀▀▄██░▀▀░█░██░██▄▀▄▀▄██",
"▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀",
" 🦞 OPENCLAW 🦞 ",
" ",
];
// ---------------------------------------------------------------------------
// Emit the full banner (animated ASCII art + version line)
// ---------------------------------------------------------------------------
export function formatCliBannerArt(options: BannerOptions = {}): string {
const rich = options.richTty ?? isRich();
if (!rich) {
return LOBSTER_ASCII.join("\n");
}
const colorChar = (ch: string) => {
if (ch === "█") {
return theme.accentBright(ch);
}
if (ch === "░") {
return theme.accentDim(ch);
}
if (ch === "▀") {
return theme.accent(ch);
}
return theme.muted(ch);
};
const colored = LOBSTER_ASCII.map((line) => {
if (line.includes("OPENCLAW")) {
return (
theme.muted(" ") +
theme.accent("🦞") +
theme.info(" OPENCLAW ") +
theme.accent("🦞")
);
}
return splitGraphemes(line).map(colorChar).join("");
});
return colored.join("\n");
}
export function emitCliBanner(version: string, options: BannerOptions = {}) {
export async function emitCliBanner(version: string, options: BannerOptions = {}) {
if (bannerEmitted) {
return;
}
@ -122,9 +150,22 @@ export function emitCliBanner(version: string, options: BannerOptions = {}) {
if (hasVersionFlag(argv)) {
return;
}
const line = formatCliBannerLine(version, options);
process.stdout.write(`\n${line}\n\n`);
bannerEmitted = true;
const rich = options.richTty ?? isRich();
process.stdout.write("\n");
if (rich) {
// Animated iron shimmer
await animateIronBanner();
} else {
// Plain ASCII fallback
process.stdout.write(IRONCLAW_ASCII.join("\n") + "\n");
}
const line = formatCliBannerLine(version, options);
process.stdout.write(`${line}\n\n`);
}
export function hasEmittedCliBanner(): boolean {

View File

@ -1,9 +1,9 @@
import path from "node:path";
export const DEFAULT_CLI_NAME = "openclaw";
export const DEFAULT_CLI_NAME = "ironclaw";
const KNOWN_CLI_NAMES = new Set([DEFAULT_CLI_NAME]);
const CLI_PREFIX_RE = /^(?:((?:pnpm|npm|bunx|npx)\s+))?(openclaw)\b/;
const KNOWN_CLI_NAMES = new Set([DEFAULT_CLI_NAME, "openclaw"]);
const CLI_PREFIX_RE = /^(?:((?:pnpm|npm|bunx|npx)\s+))?(ironclaw|openclaw)\b/;
export function resolveCliName(argv: string[] = process.argv): string {
const argv1 = argv[1];

View File

@ -33,12 +33,13 @@ export function registerPreActionHooks(program: Command, programVersion: string)
}
const commandPath = getCommandPath(argv, 2);
const hideBanner =
isTruthyEnvValue(process.env.IRONCLAW_HIDE_BANNER) ||
isTruthyEnvValue(process.env.OPENCLAW_HIDE_BANNER) ||
commandPath[0] === "update" ||
commandPath[0] === "completion" ||
(commandPath[0] === "plugins" && commandPath[1] === "update");
if (!hideBanner) {
emitCliBanner(programVersion);
await emitCliBanner(programVersion);
}
const verbose = getVerboseFlag(argv, { includeDebug: true });
setVerbose(verbose);

View File

@ -12,7 +12,7 @@ async function prepareRoutedCommand(params: {
commandPath: string[];
loadPlugins?: boolean;
}) {
emitCliBanner(VERSION, { argv: params.argv });
await emitCliBanner(VERSION, { argv: params.argv });
await ensureConfigReady({ runtime: defaultRuntime, commandPath: params.commandPath });
if (params.loadPlugins) {
ensurePluginRegistryLoaded();

View File

@ -3,14 +3,16 @@ import path from "node:path";
import process from "node:process";
import { fileURLToPath } from "node:url";
import { loadDotEnv } from "../infra/dotenv.js";
import { normalizeEnv } from "../infra/env.js";
import { isTruthyEnvValue, normalizeEnv } from "../infra/env.js";
import { formatUncaughtError } from "../infra/errors.js";
import { isMainModule } from "../infra/is-main.js";
import { ensureOpenClawCliOnPath } from "../infra/path-env.js";
import { assertSupportedRuntime } from "../infra/runtime-guard.js";
import { installUnhandledRejectionHandler } from "../infra/unhandled-rejections.js";
import { enableConsoleCapture } from "../logging.js";
import { getPrimaryCommand, hasHelpOrVersion } from "./argv.js";
import { VERSION } from "../version.js";
import { getCommandPath, getPrimaryCommand, hasHelpOrVersion } from "./argv.js";
import { emitCliBanner } from "./banner.js";
import { tryRouteCli } from "./route.js";
export function rewriteUpdateFlagArgv(argv: string[]): string[] {
@ -33,6 +35,20 @@ export async function runCli(argv: string[] = process.argv) {
// Enforce the minimum supported runtime before doing any work.
assertSupportedRuntime();
// Show the animated Ironclaw banner early so it appears for ALL invocations
// (bare `ironclaw`, subcommands, help, etc.). The bannerEmitted flag inside
// emitCliBanner prevents double-emission from the route / preAction hooks.
const commandPath = getCommandPath(normalizedArgv, 2);
const hideBanner =
isTruthyEnvValue(process.env.IRONCLAW_HIDE_BANNER) ||
isTruthyEnvValue(process.env.OPENCLAW_HIDE_BANNER) ||
commandPath[0] === "update" ||
commandPath[0] === "completion" ||
(commandPath[0] === "plugins" && commandPath[1] === "update");
if (!hideBanner) {
await emitCliBanner(VERSION, { argv: normalizedArgv });
}
if (await tryRouteCli(normalizedArgv)) {
return;
}
@ -48,7 +64,7 @@ export async function runCli(argv: string[] = process.argv) {
installUnhandledRejectionHandler();
process.on("uncaughtException", (error) => {
console.error("[openclaw] Uncaught exception:", formatUncaughtError(error));
console.error("[ironclaw] Uncaught exception:", formatUncaughtError(error));
process.exit(1);
});

View File

@ -1,96 +1,88 @@
const DEFAULT_TAGLINE = "All your chats, one OpenClaw.";
const DEFAULT_TAGLINE = "Forge your workflow. Command your data.";
const HOLIDAY_TAGLINES = {
newYear:
"New Year's Day: New year, new config—same old EADDRINUSE, but this time we resolve it like grown-ups.",
"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 branches prosperous, and your merge conflicts chased away with fireworks.",
"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 little claw-sistant is here to ship joy, roll back chaos, and stash the keys safely.",
eid: "Eid al-Fitr: Celebration mode: queues cleared, tasks completed, and good vibes committed to main with clean history.",
"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 logs sparkle and the bugs flee—today we light up the terminal and ship with pride.",
"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.",
"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 peaceful.",
"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 a bot that reads the logs so nobody has to.",
"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.",
"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[] = [
"Your terminal just grew claws—type something and let the bot pinch the busywork.",
// 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."',
"Gateway online—please keep hands, feet, and appendages inside the shell at all times.",
"I speak fluent bash, mild sarcasm, and aggressive tab-completion energy.",
"One CLI to rule them all, and one more restart because you changed the port.",
"If it works, it's automation; if it breaks, it's a \"learning opportunity.\"",
"Pairing codes exist because even bots believe in consent—and good security hygiene.",
"Your .env is showing; don't worry, I'll pretend I didn't see it.",
"I'll do the boring stuff while you dramatically stare at the logs like it's cinema.",
"I'm not saying your workflow is chaotic... I'm just bringing a linter and a helmet.",
"Type the command with confidence—nature will provide the stack trace if needed.",
"I don't judge, but your missing API keys are absolutely judging you.",
"I can grep it, git blame it, and gently roast it—pick your coping mechanism.",
"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'm the assistant your terminal demanded, not the one your sleep schedule requested.",
"I keep secrets like a vault... unless you print them in debug logs again.",
"Automation with claws: minimal fuss, maximal pinch.",
"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.",
"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.",
"I'll refactor your busywork like it owes me money.",
'Say "stop" and I\'ll stop—say "ship" and we\'ll both learn a lesson.',
"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.",
"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.",
"I don't just autocomplete—I auto-commit (emotionally), then ask you to review (logically).",
'Less clicking, more shipping, fewer "where did that file go" moments.',
"Claws out, commit in—let's ship something mildly responsible.",
"I'll butter your workflow like a lobster roll: messy, delicious, effective.",
"Shell yeah—I'm here to pinch the toil and leave you the glory.",
"If it's repetitive, I'll automate it; if it's hard, I'll bring jokes and a rollback plan.",
"Because texting yourself reminders is so 2024.",
// Multi-channel / product
"Your inbox, your infra, your rules.",
'Turning "I\'ll reply later" into "my bot replied instantly".',
"The only crab in your contacts you actually want to hear from. 🦞",
'Turning "I\'ll reply later" into "my agent replied instantly".',
"Chat automation for people who peaked at IRC.",
"Because Siri wasn't answering at 3AM.",
"IPC, but it's your phone.",
"The UNIX philosophy meets your DMs.",
"curl for conversations.",
"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.",
'WhatsApp automation without the "please accept our new privacy policy".',
"Chat APIs that don't require a Senate hearing.",
"Meta wishes they shipped this fast.",
"Because the right answer is usually a script.",
"Your messages, your servers, your control.",
"OpenAI-compatible, not OpenAI-dependent.",
"iMessage green bubble energy, but for everyone.",
"Siri's competent cousin.",
"Works on Android. Crazy concept, we know.",
"No $999 stand required.",
"We ship features faster than Apple ships calculator updates.",
"Your AI assistant, now without the $3,499 headset.",
"Think different. Actually think.",
"Ah, the fruit tree company! 🍎",
"Greetings, Professor Falken",
"Because the right answer is usually a script.",
// Holiday taglines (gated by date rules below)
HOLIDAY_TAGLINES.newYear,
HOLIDAY_TAGLINES.lunarNewYear,
HOLIDAY_TAGLINES.christmas,
@ -253,7 +245,8 @@ export function activeTaglines(options: TaglineOptions = {}): string[] {
export function pickTagline(options: TaglineOptions = {}): string {
const env = options.env ?? process.env;
const override = env?.OPENCLAW_TAGLINE_INDEX;
// 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) {

View File

@ -7,7 +7,7 @@ import { isTruthyEnvValue, normalizeEnv } from "./infra/env.js";
import { installProcessWarningFilter } from "./infra/warning-filter.js";
import { attachChildProcessBridge } from "./process/child-process-bridge.js";
process.title = "openclaw";
process.title = "ironclaw";
installProcessWarningFilter();
normalizeEnv();
@ -32,10 +32,16 @@ function hasExperimentalWarningSuppressed(): boolean {
}
function ensureExperimentalWarningSuppressed(): boolean {
if (isTruthyEnvValue(process.env.OPENCLAW_NO_RESPAWN)) {
if (
isTruthyEnvValue(process.env.IRONCLAW_NO_RESPAWN) ||
isTruthyEnvValue(process.env.OPENCLAW_NO_RESPAWN)
) {
return false;
}
if (isTruthyEnvValue(process.env.OPENCLAW_NODE_OPTIONS_READY)) {
if (
isTruthyEnvValue(process.env.IRONCLAW_NODE_OPTIONS_READY) ||
isTruthyEnvValue(process.env.OPENCLAW_NODE_OPTIONS_READY)
) {
return false;
}
if (hasExperimentalWarningSuppressed()) {
@ -43,6 +49,7 @@ function ensureExperimentalWarningSuppressed(): boolean {
}
// Respawn guard (and keep recursion bounded if something goes wrong).
process.env.IRONCLAW_NODE_OPTIONS_READY = "1";
process.env.OPENCLAW_NODE_OPTIONS_READY = "1";
// Pass flag as a Node CLI option, not via NODE_OPTIONS (--disable-warning is disallowed in NODE_OPTIONS).
const child = spawn(
@ -66,7 +73,7 @@ function ensureExperimentalWarningSuppressed(): boolean {
child.once("error", (error) => {
console.error(
"[openclaw] Failed to respawn CLI:",
"[ironclaw] Failed to respawn CLI:",
error instanceof Error ? (error.stack ?? error.message) : error,
);
process.exit(1);
@ -149,7 +156,7 @@ if (!ensureExperimentalWarningSuppressed()) {
const parsed = parseCliProfileArgs(process.argv);
if (!parsed.ok) {
// Keep it simple; Commander will handle rich help/errors after we strip flags.
console.error(`[openclaw] ${parsed.error}`);
console.error(`[ironclaw] ${parsed.error}`);
process.exit(2);
}
@ -163,7 +170,7 @@ if (!ensureExperimentalWarningSuppressed()) {
.then(({ runCli }) => runCli(process.argv))
.catch((error) => {
console.error(
"[openclaw] Failed to start CLI:",
"[ironclaw] Failed to start CLI:",
error instanceof Error ? (error.stack ?? error.message) : error,
);
process.exitCode = 1;

View File

@ -1,12 +1,15 @@
// Lobster palette tokens for CLI/UI theming. "lobster seam" == use this palette.
// Iron palette tokens for CLI/UI theming. "iron seam" == use this palette.
// Keep in sync with docs/cli/index.md (CLI palette section).
export const LOBSTER_PALETTE = {
accent: "#FF5A2D",
accentBright: "#FF7A3D",
accentDim: "#D14A22",
info: "#FF8A5B",
success: "#2FBF71",
warn: "#FFB020",
error: "#E23D2D",
muted: "#8B7F77",
export const IRON_PALETTE = {
accent: "#9CA3AF", // cool steel grey
accentBright: "#D1D5DB", // bright silver highlight
accentDim: "#6B7280", // dark iron
info: "#93C5FD", // steel blue
success: "#34D399", // emerald
warn: "#FBBF24", // amber
error: "#F87171", // red
muted: "#6B7280", // iron grey
} as const;
// Backward-compatible alias for any external importers.
export { IRON_PALETTE as LOBSTER_PALETTE };

View File

@ -1,5 +1,5 @@
import chalk, { Chalk } from "chalk";
import { LOBSTER_PALETTE } from "./palette.js";
import { IRON_PALETTE } from "./palette.js";
const hasForceColor =
typeof process.env.FORCE_COLOR === "string" &&
@ -11,17 +11,17 @@ const baseChalk = process.env.NO_COLOR && !hasForceColor ? new Chalk({ level: 0
const hex = (value: string) => baseChalk.hex(value);
export const theme = {
accent: hex(LOBSTER_PALETTE.accent),
accentBright: hex(LOBSTER_PALETTE.accentBright),
accentDim: hex(LOBSTER_PALETTE.accentDim),
info: hex(LOBSTER_PALETTE.info),
success: hex(LOBSTER_PALETTE.success),
warn: hex(LOBSTER_PALETTE.warn),
error: hex(LOBSTER_PALETTE.error),
muted: hex(LOBSTER_PALETTE.muted),
heading: baseChalk.bold.hex(LOBSTER_PALETTE.accent),
command: hex(LOBSTER_PALETTE.accentBright),
option: hex(LOBSTER_PALETTE.warn),
accent: hex(IRON_PALETTE.accent),
accentBright: hex(IRON_PALETTE.accentBright),
accentDim: hex(IRON_PALETTE.accentDim),
info: hex(IRON_PALETTE.info),
success: hex(IRON_PALETTE.success),
warn: hex(IRON_PALETTE.warn),
error: hex(IRON_PALETTE.error),
muted: hex(IRON_PALETTE.muted),
heading: baseChalk.bold.hex(IRON_PALETTE.accent),
command: hex(IRON_PALETTE.accentBright),
option: hex(IRON_PALETTE.warn),
} as const;
export const isRich = () => Boolean(baseChalk.level > 0);