From 6d0cb1c935639f93aa80304fef585882586a6dee Mon Sep 17 00:00:00 2001 From: Rayo Date: Wed, 28 Jan 2026 13:21:18 +0000 Subject: [PATCH] feat(onboard): add timezone prompt to onboarding wizard Adds a userTimezone prompt to the onboarding wizard that allows users to set their preferred timezone for cron schedules during initial setup. Features: - Interactive mode: prompts for timezone with validation and auto-detection - QuickStart mode: uses existing config or system timezone silently - Non-interactive mode: supports --user-timezone CLI flag - Both 'clawdbot onboard' and 'clawdbot setup --wizard' commands The timezone is stored in agents.defaults.userTimezone and will be used as the default for cron job schedules (requires PR #3329 for cron support). Helper functions added: - isValidTimezone(): validates IANA timezone strings - detectSystemTimezone(): detects the system's local timezone --- src/cli/program/register.onboard.ts | 5 +++ src/cli/program/register.setup.ts | 5 +++ src/commands/onboard-helpers.ts | 41 +++++++++++++++++++ src/commands/onboard-non-interactive/local.ts | 20 +++++++++ src/commands/onboard-types.ts | 2 + src/wizard/onboarding.ts | 39 ++++++++++++++++++ 6 files changed, 112 insertions(+) diff --git a/src/cli/program/register.onboard.ts b/src/cli/program/register.onboard.ts index 8f31635f0..6188dbd9c 100644 --- a/src/cli/program/register.onboard.ts +++ b/src/cli/program/register.onboard.ts @@ -41,6 +41,10 @@ export function registerOnboardCommand(program: Command) { `\n${theme.muted("Docs:")} ${formatDocsLink("/cli/onboard", "docs.molt.bot/cli/onboard")}\n`, ) .option("--workspace ", "Agent workspace directory (default: ~/clawd)") + .option( + "--user-timezone ", + "User timezone for cron schedules (e.g., America/New_York, Africa/Lagos)", + ) .option("--reset", "Reset config + credentials + sessions + workspace before running wizard") .option("--non-interactive", "Run without prompts", false) .option( @@ -105,6 +109,7 @@ export function registerOnboardCommand(program: Command) { await onboardCommand( { workspace: opts.workspace as string | undefined, + userTimezone: opts.userTimezone as string | undefined, nonInteractive: Boolean(opts.nonInteractive), acceptRisk: Boolean(opts.acceptRisk), flow: opts.flow as "quickstart" | "advanced" | "manual" | undefined, diff --git a/src/cli/program/register.setup.ts b/src/cli/program/register.setup.ts index 53c33a670..358faaa6e 100644 --- a/src/cli/program/register.setup.ts +++ b/src/cli/program/register.setup.ts @@ -20,6 +20,10 @@ export function registerSetupCommand(program: Command) { "--workspace ", "Agent workspace directory (default: ~/clawd; stored as agents.defaults.workspace)", ) + .option( + "--user-timezone ", + "User timezone for cron schedules (e.g., America/New_York, Africa/Lagos)", + ) .option("--wizard", "Run the interactive onboarding wizard", false) .option("--non-interactive", "Run the wizard without prompts", false) .option("--mode ", "Wizard mode: local|remote") @@ -38,6 +42,7 @@ export function registerSetupCommand(program: Command) { await onboardCommand( { workspace: opts.workspace as string | undefined, + userTimezone: opts.userTimezone as string | undefined, nonInteractive: Boolean(opts.nonInteractive), mode: opts.mode as "local" | "remote" | undefined, remoteUrl: opts.remoteUrl as string | undefined, diff --git a/src/commands/onboard-helpers.ts b/src/commands/onboard-helpers.ts index 165365bb6..7ccc97b65 100644 --- a/src/commands/onboard-helpers.ts +++ b/src/commands/onboard-helpers.ts @@ -424,3 +424,44 @@ function isValidIPv4(host: string): boolean { return !Number.isNaN(n) && n >= 0 && n <= 255 && part === String(n); }); } + +/** + * Validate an IANA timezone string. + * Uses Intl.supportedValuesOf('timeZone') when available (Node 18.6+), + * falls back to testing via DateTimeFormat. + */ +export function isValidTimezone(tz: string): boolean { + if (!tz || typeof tz !== "string") return false; + const trimmed = tz.trim(); + if (!trimmed) return false; + + // Try Intl.supportedValuesOf if available (Node 18.6+) + if (typeof Intl.supportedValuesOf === "function") { + try { + const supported = Intl.supportedValuesOf("timeZone"); + return supported.includes(trimmed); + } catch { + // Fall through to DateTimeFormat check + } + } + + // Fallback: try to create a DateTimeFormat with the timezone + try { + new Intl.DateTimeFormat("en-US", { timeZone: trimmed }); + return true; + } catch { + return false; + } +} + +/** + * Detect the system's local timezone. + * Returns the IANA timezone string (e.g., "America/New_York"). + */ +export function detectSystemTimezone(): string { + try { + return Intl.DateTimeFormat().resolvedOptions().timeZone; + } catch { + return "UTC"; + } +} diff --git a/src/commands/onboard-non-interactive/local.ts b/src/commands/onboard-non-interactive/local.ts index d642cc1da..206cc6c4d 100644 --- a/src/commands/onboard-non-interactive/local.ts +++ b/src/commands/onboard-non-interactive/local.ts @@ -8,7 +8,9 @@ import { healthCommand } from "../health.js"; import { applyWizardMetadata, DEFAULT_WORKSPACE, + detectSystemTimezone, ensureWorkspaceAndSessions, + isValidTimezone, resolveControlUiLinks, waitForGatewayReachable, } from "../onboard-helpers.js"; @@ -35,6 +37,23 @@ export async function runNonInteractiveOnboardingLocal(params: { defaultWorkspaceDir: DEFAULT_WORKSPACE, }); + // Resolve user timezone + const systemTimezone = detectSystemTimezone(); + const existingTimezone = baseConfig.agents?.defaults?.userTimezone; + let userTimezone: string | undefined; + if (opts.userTimezone !== undefined) { + const trimmed = opts.userTimezone.trim(); + if (trimmed && isValidTimezone(trimmed)) { + userTimezone = trimmed; + } else if (trimmed) { + runtime.log(`Invalid timezone "${trimmed}". Using system default: ${systemTimezone}`); + userTimezone = systemTimezone; + } + } else { + // Use existing or system timezone + userTimezone = existingTimezone ?? systemTimezone; + } + let nextConfig: MoltbotConfig = { ...baseConfig, agents: { @@ -42,6 +61,7 @@ export async function runNonInteractiveOnboardingLocal(params: { defaults: { ...baseConfig.agents?.defaults, workspace: workspaceDir, + ...(userTimezone ? { userTimezone } : {}), }, }, gateway: { diff --git a/src/commands/onboard-types.ts b/src/commands/onboard-types.ts index aa1d9afe0..6fdf54369 100644 --- a/src/commands/onboard-types.ts +++ b/src/commands/onboard-types.ts @@ -46,6 +46,8 @@ export type OnboardOptions = { /** "manual" is an alias for "advanced". */ flow?: "quickstart" | "advanced" | "manual"; workspace?: string; + /** User timezone for cron schedules (e.g., "America/New_York", "Europe/London"). */ + userTimezone?: string; nonInteractive?: boolean; /** Required for non-interactive onboarding; skips the interactive risk prompt when true. */ acceptRisk?: boolean; diff --git a/src/wizard/onboarding.ts b/src/wizard/onboarding.ts index 75543ca19..bc6ae5f47 100644 --- a/src/wizard/onboarding.ts +++ b/src/wizard/onboarding.ts @@ -11,8 +11,10 @@ import { setupChannels } from "../commands/onboard-channels.js"; import { applyWizardMetadata, DEFAULT_WORKSPACE, + detectSystemTimezone, ensureWorkspaceAndSessions, handleReset, + isValidTimezone, printWizardHeader, probeGatewayReachable, summarizeExistingConfig, @@ -335,6 +337,42 @@ export async function runOnboardingWizard( const workspaceDir = resolveUserPath(workspaceInput.trim() || DEFAULT_WORKSPACE); + // Timezone prompt + const systemTimezone = detectSystemTimezone(); + const existingTimezone = baseConfig.agents?.defaults?.userTimezone; + const timezoneDefault = existingTimezone ?? systemTimezone; + + let userTimezone: string | undefined; + if (opts.userTimezone !== undefined) { + // CLI option provided + userTimezone = opts.userTimezone.trim() || undefined; + if (userTimezone && !isValidTimezone(userTimezone)) { + await prompter.note( + `Invalid timezone "${userTimezone}". Using system default: ${systemTimezone}`, + "Timezone", + ); + userTimezone = systemTimezone; + } + } else if (flow === "quickstart") { + // QuickStart: use existing or system timezone silently + userTimezone = existingTimezone ?? systemTimezone; + } else { + // Manual flow: prompt for timezone + const timezoneInput = await prompter.text({ + message: "Timezone (for cron schedules)", + initialValue: timezoneDefault, + placeholder: "e.g., America/New_York, Europe/London, Africa/Lagos", + validate: (value) => { + const trimmed = value.trim(); + if (!trimmed) return; // Allow empty (will use system default) + if (!isValidTimezone(trimmed)) { + return `Invalid timezone. Examples: America/New_York, Europe/London, Africa/Lagos`; + } + }, + }); + userTimezone = timezoneInput.trim() || systemTimezone; + } + let nextConfig: MoltbotConfig = { ...baseConfig, agents: { @@ -342,6 +380,7 @@ export async function runOnboardingWizard( defaults: { ...baseConfig.agents?.defaults, workspace: workspaceDir, + ...(userTimezone ? { userTimezone } : {}), }, }, gateway: {