diff --git a/CHANGELOG.md b/CHANGELOG.md index e4c8746c2f6..2185aa15503 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ Docs: https://docs.openclaw.ai - Hooks/message lifecycle: add internal hook events `message:transcribed` and `message:preprocessed`, plus richer outbound `message:sent` context (`isGroup`, `groupId`) for group-conversation correlation and post-transcription automations. (#9859) Thanks @Drickon. - Telegram/Streaming defaults: default `channels.telegram.streaming` to `partial` (from `off`) so new Telegram setups get live preview streaming out of the box, with runtime fallback to message-edit preview when native drafts are unavailable. - CLI/Config validation: add `openclaw config validate` (with `--json`) to validate config files before gateway startup, and include detailed invalid-key paths in startup invalid-config errors. (#31220) thanks @Sid-Qin. +- CLI/Banner taglines: add `cli.banner.taglineMode` (`random` | `default` | `off`) to control funny tagline behavior in startup output, with docs + FAQ guidance and regression tests for config override behavior. - Tools/Diffs: add PDF file output support and rendering quality customization controls (`fileQuality`, `fileScale`, `fileMaxWidth`) for generated diff artifacts, and document PDF as the preferred option when messaging channels compress images. (#31342) Thanks @gumadeiras. - README/Contributors: rank contributor avatars by composite score (commits + merged PRs + code LOC), excluding docs-only LOC to prevent bulk-generated files from inflating rankings. (#23970) Thanks @tyler6204. diff --git a/docs/gateway/configuration-reference.md b/docs/gateway/configuration-reference.md index 733ba4bad36..37e64f2f840 100644 --- a/docs/gateway/configuration-reference.md +++ b/docs/gateway/configuration-reference.md @@ -2731,6 +2731,26 @@ Notes: --- +## CLI + +```json5 +{ + cli: { + banner: { + taglineMode: "off", // random | default | off + }, + }, +} +``` + +- `cli.banner.taglineMode` controls banner tagline style: + - `"random"` (default): rotating funny/seasonal taglines. + - `"default"`: fixed neutral tagline (`All your chats, one OpenClaw.`). + - `"off"`: no tagline text (banner title/version still shown). +- To hide the entire banner (not just taglines), set env `OPENCLAW_HIDE_BANNER=1`. + +--- + ## Wizard Metadata written by CLI wizards (`onboard`, `configure`, `doctor`): diff --git a/docs/help/faq.md b/docs/help/faq.md index 2af5bd34efa..a1235dde549 100644 --- a/docs/help/faq.md +++ b/docs/help/faq.md @@ -101,6 +101,7 @@ Quick answers plus deeper troubleshooting for real-world setups (local dev, VPS, - [I set `gateway.bind: "lan"` (or `"tailnet"`) and now nothing listens / the UI says unauthorized](#i-set-gatewaybind-lan-or-tailnet-and-now-nothing-listens-the-ui-says-unauthorized) - [Why do I need a token on localhost now?](#why-do-i-need-a-token-on-localhost-now) - [Do I have to restart after changing config?](#do-i-have-to-restart-after-changing-config) + - [How do I disable funny CLI taglines?](#how-do-i-disable-funny-cli-taglines) - [How do I enable web search (and web fetch)?](#how-do-i-enable-web-search-and-web-fetch) - [config.apply wiped my config. How do I recover and avoid this?](#configapply-wiped-my-config-how-do-i-recover-and-avoid-this) - [How do I run a central Gateway with specialized workers across devices?](#how-do-i-run-a-central-gateway-with-specialized-workers-across-devices) @@ -1466,6 +1467,25 @@ The Gateway watches the config and supports hot-reload: - `gateway.reload.mode: "hybrid"` (default): hot-apply safe changes, restart for critical ones - `hot`, `restart`, `off` are also supported +### How do I disable funny CLI taglines + +Set `cli.banner.taglineMode` in config: + +```json5 +{ + cli: { + banner: { + taglineMode: "off", // random | default | off + }, + }, +} +``` + +- `off`: hides tagline text but keeps the banner title/version line. +- `default`: uses `All your chats, one OpenClaw.` every time. +- `random`: rotating funny/seasonal taglines (default behavior). +- If you want no banner at all, set env `OPENCLAW_HIDE_BANNER=1`. + ### How do I enable web search and web fetch `web_fetch` works without an API key. `web_search` requires a Brave Search API diff --git a/src/cli/banner.test.ts b/src/cli/banner.test.ts new file mode 100644 index 00000000000..4863bc04551 --- /dev/null +++ b/src/cli/banner.test.ts @@ -0,0 +1,60 @@ +import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; + +const loadConfigMock = vi.fn(); + +vi.mock("../config/config.js", () => ({ + loadConfig: loadConfigMock, +})); + +let formatCliBannerLine: typeof import("./banner.js").formatCliBannerLine; + +beforeAll(async () => { + ({ formatCliBannerLine } = await import("./banner.js")); +}); + +beforeEach(() => { + loadConfigMock.mockReset(); + loadConfigMock.mockReturnValue({}); +}); + +describe("formatCliBannerLine", () => { + it("hides tagline text when cli.banner.taglineMode is off", () => { + loadConfigMock.mockReturnValue({ + cli: { banner: { taglineMode: "off" } }, + }); + + const line = formatCliBannerLine("2026.3.3", { + commit: "abc1234", + richTty: false, + }); + + expect(line).toBe("🦞 OpenClaw 2026.3.3 (abc1234)"); + }); + + it("uses default tagline when cli.banner.taglineMode is default", () => { + loadConfigMock.mockReturnValue({ + cli: { banner: { taglineMode: "default" } }, + }); + + const line = formatCliBannerLine("2026.3.3", { + commit: "abc1234", + richTty: false, + }); + + expect(line).toBe("🦞 OpenClaw 2026.3.3 (abc1234) — All your chats, one OpenClaw."); + }); + + it("prefers explicit tagline mode over config", () => { + loadConfigMock.mockReturnValue({ + cli: { banner: { taglineMode: "off" } }, + }); + + const line = formatCliBannerLine("2026.3.3", { + commit: "abc1234", + richTty: false, + mode: "default", + }); + + expect(line).toBe("🦞 OpenClaw 2026.3.3 (abc1234) — All your chats, one OpenClaw."); + }); +}); diff --git a/src/cli/banner.ts b/src/cli/banner.ts index 2417566548b..4c9e4b7e488 100644 --- a/src/cli/banner.ts +++ b/src/cli/banner.ts @@ -1,8 +1,9 @@ +import { loadConfig } from "../config/config.js"; import { resolveCommitHash } from "../infra/git-commit.js"; import { visibleWidth } from "../terminal/ansi.js"; import { isRich, theme } from "../terminal/theme.js"; import { hasRootVersionAlias } from "./argv.js"; -import { pickTagline, type TaglineOptions } from "./tagline.js"; +import { pickTagline, type TaglineMode, type TaglineOptions } from "./tagline.js"; type BannerOptions = TaglineOptions & { argv?: string[]; @@ -35,18 +36,42 @@ const hasJsonFlag = (argv: string[]) => const hasVersionFlag = (argv: string[]) => argv.some((arg) => arg === "--version" || arg === "-V") || hasRootVersionAlias(argv); +function parseTaglineMode(value: unknown): TaglineMode | undefined { + if (value === "random" || value === "default" || value === "off") { + return value; + } + return undefined; +} + +function resolveTaglineMode(options: BannerOptions): TaglineMode | undefined { + const explicit = parseTaglineMode(options.mode); + if (explicit) { + return explicit; + } + try { + return parseTaglineMode(loadConfig().cli?.banner?.taglineMode); + } catch { + // Fall back to default random behavior when config is missing/invalid. + return undefined; + } +} + 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 tagline = pickTagline({ ...options, mode: resolveTaglineMode(options) }); const rich = options.richTty ?? isRich(); const title = "🦞 OpenClaw"; const prefix = "🦞 "; const columns = options.columns ?? process.stdout.columns ?? 120; - const plainFullLine = `${title} ${version} (${commitLabel}) — ${tagline}`; + const plainBaseLine = `${title} ${version} (${commitLabel})`; + const plainFullLine = tagline ? `${plainBaseLine} — ${tagline}` : plainBaseLine; const fitsOnOneLine = visibleWidth(plainFullLine) <= columns; if (rich) { if (fitsOnOneLine) { + if (!tagline) { + return `${theme.heading(title)} ${theme.info(version)} ${theme.muted(`(${commitLabel})`)}`; + } return `${theme.heading(title)} ${theme.info(version)} ${theme.muted( `(${commitLabel})`, )} ${theme.muted("—")} ${theme.accentDim(tagline)}`; @@ -54,13 +79,19 @@ export function formatCliBannerLine(version: string, options: BannerOptions = {} const line1 = `${theme.heading(title)} ${theme.info(version)} ${theme.muted( `(${commitLabel})`, )}`; + if (!tagline) { + return line1; + } const line2 = `${" ".repeat(prefix.length)}${theme.accentDim(tagline)}`; return `${line1}\n${line2}`; } if (fitsOnOneLine) { return plainFullLine; } - const line1 = `${title} ${version} (${commitLabel})`; + const line1 = plainBaseLine; + if (!tagline) { + return line1; + } const line2 = `${" ".repeat(prefix.length)}${tagline}`; return `${line1}\n${line2}`; } diff --git a/src/cli/tagline.test.ts b/src/cli/tagline.test.ts new file mode 100644 index 00000000000..b81f33c620c --- /dev/null +++ b/src/cli/tagline.test.ts @@ -0,0 +1,21 @@ +import { describe, expect, it } from "vitest"; +import { DEFAULT_TAGLINE, pickTagline } from "./tagline.js"; + +describe("pickTagline", () => { + it("returns empty string when mode is off", () => { + expect(pickTagline({ mode: "off" })).toBe(""); + }); + + it("returns default tagline when mode is default", () => { + expect(pickTagline({ mode: "default" })).toBe(DEFAULT_TAGLINE); + }); + + it("keeps OPENCLAW_TAGLINE_INDEX behavior in random mode", () => { + const value = pickTagline({ + mode: "random", + env: { OPENCLAW_TAGLINE_INDEX: "0" } as NodeJS.ProcessEnv, + }); + expect(value.length).toBeGreaterThan(0); + expect(value).not.toBe(DEFAULT_TAGLINE); + }); +}); diff --git a/src/cli/tagline.ts b/src/cli/tagline.ts index 2ecf8bf829a..9df2bf303a5 100644 --- a/src/cli/tagline.ts +++ b/src/cli/tagline.ts @@ -1,4 +1,5 @@ const DEFAULT_TAGLINE = "All your chats, one OpenClaw."; +export type TaglineMode = "random" | "default" | "off"; const HOLIDAY_TAGLINES = { newYear: @@ -248,6 +249,7 @@ export interface TaglineOptions { env?: NodeJS.ProcessEnv; random?: () => number; now?: () => Date; + mode?: TaglineMode; } export function activeTaglines(options: TaglineOptions = {}): string[] { @@ -260,6 +262,12 @@ export function activeTaglines(options: TaglineOptions = {}): string[] { } export function pickTagline(options: TaglineOptions = {}): string { + if (options.mode === "off") { + return ""; + } + if (options.mode === "default") { + return DEFAULT_TAGLINE; + } const env = options.env ?? process.env; const override = env?.OPENCLAW_TAGLINE_INDEX; if (override !== undefined) { diff --git a/src/config/schema.help.quality.test.ts b/src/config/schema.help.quality.test.ts index ec83273eb8a..a05d1f6417f 100644 --- a/src/config/schema.help.quality.test.ts +++ b/src/config/schema.help.quality.test.ts @@ -9,6 +9,7 @@ const ROOT_SECTIONS = [ "wizard", "diagnostics", "logging", + "cli", "update", "browser", "ui", @@ -421,6 +422,7 @@ const ENUM_EXPECTATIONS: Record = { ], "logging.consoleStyle": ['"pretty"', '"compact"', '"json"'], "logging.redactSensitive": ['"off"', '"tools"'], + "cli.banner.taglineMode": ['"random"', '"default"', '"off"'], "update.channel": ['"stable"', '"beta"', '"dev"'], "agents.defaults.compaction.mode": ['"default"', '"safeguard"'], "agents.defaults.compaction.identifierPolicy": ['"strict"', '"off"', '"custom"'], diff --git a/src/config/schema.help.ts b/src/config/schema.help.ts index b36b74e8bfe..f64d90aedd0 100644 --- a/src/config/schema.help.ts +++ b/src/config/schema.help.ts @@ -46,6 +46,11 @@ export const FIELD_HELP: Record = { 'Sensitive redaction mode: "off" disables built-in masking, while "tools" redacts sensitive tool/config payload fields. Keep "tools" in shared logs unless you have isolated secure log sinks.', "logging.redactPatterns": "Additional custom redact regex patterns applied to log output before emission/storage. Use this to mask org-specific tokens and identifiers not covered by built-in redaction rules.", + cli: "CLI presentation controls for local command output behavior such as banner and tagline style. Use this section to keep startup output aligned with operator preference without changing runtime behavior.", + "cli.banner": + "CLI startup banner controls for title/version line and tagline style behavior. Keep banner enabled for fast version/context checks, then tune tagline mode to your preferred noise level.", + "cli.banner.taglineMode": + 'Controls tagline style in the CLI startup banner: "random" (default) picks from the rotating tagline pool, "default" always shows the neutral default tagline, and "off" hides tagline text while keeping the banner version line.', update: "Update-channel and startup-check behavior for keeping OpenClaw runtime versions current. Use conservative channels in production and more experimental channels only in controlled environments.", "update.channel": 'Update channel for git + npm installs ("stable", "beta", or "dev").', diff --git a/src/config/schema.hints.ts b/src/config/schema.hints.ts index 3d212836f70..64d1acde778 100644 --- a/src/config/schema.hints.ts +++ b/src/config/schema.hints.ts @@ -13,6 +13,7 @@ export type { ConfigUiHint, ConfigUiHints } from "../shared/config-ui-hints-type const GROUP_LABELS: Record = { wizard: "Wizard", update: "Update", + cli: "CLI", diagnostics: "Diagnostics", logging: "Logging", gateway: "Gateway", @@ -41,6 +42,7 @@ const GROUP_LABELS: Record = { const GROUP_ORDER: Record = { wizard: 20, update: 25, + cli: 26, diagnostics: 27, gateway: 30, nodeHost: 35, diff --git a/src/config/schema.labels.ts b/src/config/schema.labels.ts index 4edd050f3f6..ee1b09e322c 100644 --- a/src/config/schema.labels.ts +++ b/src/config/schema.labels.ts @@ -26,6 +26,9 @@ export const FIELD_LABELS: Record = { "logging.consoleStyle": "Console Log Style", "logging.redactSensitive": "Sensitive Data Redaction Mode", "logging.redactPatterns": "Custom Redaction Patterns", + cli: "CLI", + "cli.banner": "CLI Banner", + "cli.banner.taglineMode": "CLI Banner Tagline Mode", update: "Updates", "update.channel": "Update Channel", "update.checkOnStart": "Update Check on Start", diff --git a/src/config/types.cli.ts b/src/config/types.cli.ts new file mode 100644 index 00000000000..0690bd75b30 --- /dev/null +++ b/src/config/types.cli.ts @@ -0,0 +1,13 @@ +export type CliBannerTaglineMode = "random" | "default" | "off"; + +export type CliConfig = { + banner?: { + /** + * Controls CLI banner tagline behavior. + * - "random": pick from tagline pool (default) + * - "default": always use DEFAULT_TAGLINE + * - "off": hide tagline text + */ + taglineMode?: CliBannerTaglineMode; + }; +}; diff --git a/src/config/types.openclaw.ts b/src/config/types.openclaw.ts index f3374083de8..e83b105dff3 100644 --- a/src/config/types.openclaw.ts +++ b/src/config/types.openclaw.ts @@ -5,6 +5,7 @@ import type { AuthConfig } from "./types.auth.js"; import type { DiagnosticsConfig, LoggingConfig, SessionConfig, WebConfig } from "./types.base.js"; import type { BrowserConfig } from "./types.browser.js"; import type { ChannelsConfig } from "./types.channels.js"; +import type { CliConfig } from "./types.cli.js"; import type { CronConfig } from "./types.cron.js"; import type { CanvasHostConfig, @@ -61,6 +62,7 @@ export type OpenClawConfig = { }; diagnostics?: DiagnosticsConfig; logging?: LoggingConfig; + cli?: CliConfig; update?: { /** Update channel for git + npm installs ("stable", "beta", or "dev"). */ channel?: "stable" | "beta" | "dev"; diff --git a/src/config/types.ts b/src/config/types.ts index 50ee48c9b54..52e45b32aaf 100644 --- a/src/config/types.ts +++ b/src/config/types.ts @@ -8,6 +8,7 @@ export * from "./types.auth.js"; export * from "./types.base.js"; export * from "./types.browser.js"; export * from "./types.channels.js"; +export * from "./types.cli.js"; export * from "./types.openclaw.js"; export * from "./types.cron.js"; export * from "./types.discord.js"; diff --git a/src/config/zod-schema.ts b/src/config/zod-schema.ts index 5b2cf7d075c..6eb9f5bb5aa 100644 --- a/src/config/zod-schema.ts +++ b/src/config/zod-schema.ts @@ -222,6 +222,19 @@ export const OpenClawSchema = z }) .strict() .optional(), + cli: z + .object({ + banner: z + .object({ + taglineMode: z + .union([z.literal("random"), z.literal("default"), z.literal("off")]) + .optional(), + }) + .strict() + .optional(), + }) + .strict() + .optional(), update: z .object({ channel: z.union([z.literal("stable"), z.literal("beta"), z.literal("dev")]).optional(),