diff --git a/rust/crates/rusty-claude-cli/src/main.rs b/rust/crates/rusty-claude-cli/src/main.rs index 4f8362a..194456f 100644 --- a/rust/crates/rusty-claude-cli/src/main.rs +++ b/rust/crates/rusty-claude-cli/src/main.rs @@ -30,12 +30,11 @@ use plugins::{PluginManager, PluginManagerConfig}; use render::{MarkdownStreamState, Spinner, TerminalRenderer}; use runtime::{ clear_oauth_credentials, generate_pkce_pair, generate_state, load_system_prompt, - parse_oauth_callback_request_target, resolve_sandbox_status, save_oauth_credentials, - ApiClient, ApiRequest, AssistantEvent, CompactionConfig, ConfigLoader, ConfigSource, - ContentBlock, ConversationMessage, ConversationRuntime, MessageRole, PromptCacheEvent, - OAuthAuthorizationRequest, OAuthConfig, - OAuthTokenExchangeRequest, PermissionMode, PermissionPolicy, ProjectContext, RuntimeError, - Session, TokenUsage, ToolError, ToolExecutor, UsageTracker, + parse_oauth_callback_request_target, resolve_sandbox_status, save_oauth_credentials, ApiClient, + ApiRequest, AssistantEvent, CompactionConfig, ConfigLoader, ConfigSource, ContentBlock, + ConversationMessage, ConversationRuntime, MessageRole, OAuthAuthorizationRequest, OAuthConfig, + OAuthTokenExchangeRequest, PermissionMode, PermissionPolicy, ProjectContext, PromptCacheEvent, + RuntimeError, Session, TokenUsage, ToolError, ToolExecutor, UsageTracker, }; use serde_json::json; use tools::GlobalToolRegistry; @@ -56,16 +55,38 @@ const GIT_SHA: Option<&str> = option_env!("GIT_SHA"); const INTERNAL_PROGRESS_HEARTBEAT_INTERVAL: Duration = Duration::from_secs(3); const PRIMARY_SESSION_EXTENSION: &str = "jsonl"; const LEGACY_SESSION_EXTENSION: &str = "json"; +const LATEST_SESSION_REFERENCE: &str = "latest"; +const SESSION_REFERENCE_ALIASES: &[&str] = &[LATEST_SESSION_REFERENCE, "last", "recent"]; +const CLI_OPTION_SUGGESTIONS: &[&str] = &[ + "--help", + "-h", + "--version", + "-V", + "--model", + "--output-format", + "--permission-mode", + "--dangerously-skip-permissions", + "--allowedTools", + "--allowed-tools", + "--resume", + "--print", + "-p", +]; type AllowedToolSet = BTreeSet; fn main() { if let Err(error) = run() { - eprintln!( - "error: {error} + let message = error.to_string(); + if message.contains("`claw --help`") { + eprintln!("error: {message}"); + } else { + eprintln!( + "error: {message} Run `claw --help` for usage." - ); + ); + } std::process::exit(1); } } @@ -165,6 +186,7 @@ fn parse_args(args: &[String]) -> Result { let mut model = DEFAULT_MODEL.to_string(); let mut output_format = CliOutputFormat::Text; let mut permission_mode = default_permission_mode(); + let mut wants_help = false; let mut wants_version = false; let mut allowed_tool_values = Vec::new(); let mut rest = Vec::new(); @@ -172,6 +194,10 @@ fn parse_args(args: &[String]) -> Result { while index < args.len() { match args[index].as_str() { + "--help" | "-h" if rest.is_empty() => { + wants_help = true; + index += 1; + } "--version" | "-V" => { wants_version = true; index += 1; @@ -232,6 +258,15 @@ fn parse_args(args: &[String]) -> Result { output_format = CliOutputFormat::Text; index += 1; } + "--resume" if rest.is_empty() => { + rest.push("--resume".to_string()); + index += 1; + } + flag if rest.is_empty() && flag.starts_with("--resume=") => { + rest.push("--resume".to_string()); + rest.push(flag[9..].to_string()); + index += 1; + } "--allowedTools" | "--allowed-tools" => { let value = args .get(index + 1) @@ -247,6 +282,9 @@ fn parse_args(args: &[String]) -> Result { allowed_tool_values.push(flag[16..].to_string()); index += 1; } + other if rest.is_empty() && other.starts_with('-') => { + return Err(format_unknown_option(other)) + } other => { rest.push(other.to_string()); index += 1; @@ -254,6 +292,10 @@ fn parse_args(args: &[String]) -> Result { } } + if wants_help { + return Ok(CliAction::Help); + } + if wants_version { return Ok(CliAction::Version); } @@ -267,9 +309,6 @@ fn parse_args(args: &[String]) -> Result { permission_mode, }); } - if matches!(rest.first().map(String::as_str), Some("--help" | "-h")) { - return Ok(CliAction::Help); - } if rest.first().map(String::as_str) == Some("--resume") { return parse_resume_args(&rest[1..]); } @@ -323,17 +362,127 @@ fn parse_direct_slash_cli_action(rest: &[String]) -> Result { Some(SlashCommand::Help) => Ok(CliAction::Help), Some(SlashCommand::Agents { args }) => Ok(CliAction::Agents { args }), Some(SlashCommand::Skills { args }) => Ok(CliAction::Skills { args }), + Some(SlashCommand::Unknown(name)) => Err(format_unknown_direct_slash_command(&name)), Some(command) => Err(format!( "unsupported direct slash command outside the REPL: {command_name}", - command_name = match command { - SlashCommand::Unknown(name) => format!("/{name}"), - _ => rest[0].clone(), + command_name = { + let _ = command; + rest[0].clone() } )), None => Err(format!("unknown subcommand: {}", rest[0])), } } +fn format_unknown_option(option: &str) -> String { + let mut message = format!("unknown option: {option}"); + if let Some(suggestion) = suggest_closest_term(option, CLI_OPTION_SUGGESTIONS) { + message.push_str("\nDid you mean "); + message.push_str(suggestion); + message.push('?'); + } + message.push_str("\nRun `claw --help` for usage."); + message +} + +fn format_unknown_direct_slash_command(name: &str) -> String { + let mut message = format!("unknown slash command outside the REPL: /{name}"); + if let Some(suggestions) = render_suggestion_line("Did you mean", &suggest_slash_commands(name)) + { + message.push('\n'); + message.push_str(&suggestions); + } + message.push_str("\nRun `claw --help` for CLI usage, or start `claw` and use /help."); + message +} + +fn format_unknown_slash_command(name: &str) -> String { + let mut message = format!("Unknown slash command: /{name}"); + if let Some(suggestions) = render_suggestion_line("Did you mean", &suggest_slash_commands(name)) + { + message.push('\n'); + message.push_str(&suggestions); + } + message.push_str("\n Help /help lists available slash commands"); + message +} + +fn render_suggestion_line(label: &str, suggestions: &[String]) -> Option { + (!suggestions.is_empty()).then(|| format!(" {label:<16} {}", suggestions.join(", "),)) +} + +fn suggest_slash_commands(input: &str) -> Vec { + let mut candidates = slash_command_specs() + .iter() + .flat_map(|spec| { + std::iter::once(spec.name) + .chain(spec.aliases.iter().copied()) + .map(|name| format!("/{name}")) + .collect::>() + }) + .collect::>(); + candidates.sort(); + candidates.dedup(); + let candidate_refs = candidates.iter().map(String::as_str).collect::>(); + ranked_suggestions(input.trim_start_matches('/'), &candidate_refs) + .into_iter() + .map(str::to_string) + .collect() +} + +fn suggest_closest_term<'a>(input: &str, candidates: &'a [&'a str]) -> Option<&'a str> { + ranked_suggestions(input, candidates).into_iter().next() +} + +fn ranked_suggestions<'a>(input: &str, candidates: &'a [&'a str]) -> Vec<&'a str> { + let normalized_input = input.trim_start_matches('/').to_ascii_lowercase(); + let mut ranked = candidates + .iter() + .filter_map(|candidate| { + let normalized_candidate = candidate.trim_start_matches('/').to_ascii_lowercase(); + let distance = levenshtein_distance(&normalized_input, &normalized_candidate); + let prefix_bonus = usize::from( + !(normalized_candidate.starts_with(&normalized_input) + || normalized_input.starts_with(&normalized_candidate)), + ); + let score = distance + prefix_bonus; + (score <= 4).then_some((score, *candidate)) + }) + .collect::>(); + ranked.sort_by(|left, right| left.cmp(right).then_with(|| left.1.cmp(right.1))); + ranked + .into_iter() + .map(|(_, candidate)| candidate) + .take(3) + .collect() +} + +fn levenshtein_distance(left: &str, right: &str) -> usize { + if left.is_empty() { + return right.chars().count(); + } + if right.is_empty() { + return left.chars().count(); + } + + let right_chars = right.chars().collect::>(); + let mut previous = (0..=right_chars.len()).collect::>(); + let mut current = vec![0; right_chars.len() + 1]; + + for (left_index, left_char) in left.chars().enumerate() { + current[0] = left_index + 1; + for (right_index, right_char) in right_chars.iter().enumerate() { + let substitution_cost = usize::from(left_char != *right_char); + current[right_index + 1] = (previous[right_index + 1] + 1) + .min(current[right_index] + 1) + .min(previous[right_index] + substitution_cost); + } + previous.clone_from(¤t); + } + + previous[right_chars.len()] +} + fn resolve_model_alias(model: &str) -> &str { match model { "opus" => "claude-opus-4-6", @@ -421,11 +570,13 @@ fn parse_system_prompt_args(args: &[String]) -> Result { } fn parse_resume_args(args: &[String]) -> Result { - let session_path = args - .first() - .ok_or_else(|| "missing session path for --resume".to_string()) - .map(PathBuf::from)?; - let commands = args[1..].to_vec(); + let (session_path, commands) = match args.first() { + None => (PathBuf::from(LATEST_SESSION_REFERENCE), Vec::new()), + Some(first) if first.trim_start().starts_with('/') => { + (PathBuf::from(LATEST_SESSION_REFERENCE), args.to_vec()) + } + Some(first) => (PathBuf::from(first), args[1..].to_vec()), + }; if commands .iter() .any(|command| !command.trim_start().starts_with('/')) @@ -783,6 +934,15 @@ fn format_resume_report(session_path: &str, message_count: usize, turns: u32) -> ) } +fn render_resume_usage() -> String { + format!( + "Resume + Usage /resume + Auto-save .claw/sessions/.{PRIMARY_SESSION_EXTENSION} + Tip use /session list to inspect saved sessions" + ) +} + fn format_compact_report(removed: usize, resulting_messages: usize, skipped: bool) -> String { if skipped { format!( @@ -1014,6 +1174,7 @@ fn run_resume_command( message: Some(handle_skills_slash_command(args.as_deref(), &cwd)?), }) } + SlashCommand::Unknown(name) => Err(format_unknown_slash_command(name).into()), SlashCommand::Bughunter { .. } | SlashCommand::Commit | SlashCommand::Pr { .. } @@ -1025,8 +1186,7 @@ fn run_resume_command( | SlashCommand::Model { .. } | SlashCommand::Permissions { .. } | SlashCommand::Session { .. } - | SlashCommand::Plugins { .. } - | SlashCommand::Unknown(_) => Err("unsupported resumed slash command".into()), + | SlashCommand::Plugins { .. } => Err("unsupported resumed slash command".into()), } } @@ -1080,7 +1240,7 @@ struct SessionHandle { struct ManagedSessionSummary { id: String, path: PathBuf, - modified_epoch_secs: u64, + modified_epoch_millis: u128, message_count: usize, parent_session_id: Option, branch_name: Option, @@ -1188,6 +1348,10 @@ impl LiveCli { |_| "".to_string(), |path| path.display().to_string(), ); + let session_path = self.session.path.strip_prefix(Path::new(&cwd)).map_or_else( + |_| self.session.path.display().to_string(), + |path| path.display().to_string(), + ); format!( "\x1b[38;5;196m\ ██████╗██╗ █████╗ ██╗ ██╗\n\ @@ -1199,12 +1363,14 @@ impl LiveCli { \x1b[2mModel\x1b[0m {}\n\ \x1b[2mPermissions\x1b[0m {}\n\ \x1b[2mDirectory\x1b[0m {}\n\ - \x1b[2mSession\x1b[0m {}\n\n\ - Type \x1b[1m/help\x1b[0m for commands · \x1b[2mShift+Enter\x1b[0m for newline", + \x1b[2mSession\x1b[0m {}\n\ + \x1b[2mAuto-save\x1b[0m {}\n\n\ + Type \x1b[1m/help\x1b[0m for commands · \x1b[2m/resume latest\x1b[0m jumps back to the newest session", self.model, self.permission_mode.as_str(), cwd, self.session.id, + session_path, ) } @@ -1416,7 +1582,7 @@ impl LiveCli { false } SlashCommand::Unknown(name) => { - eprintln!("unknown slash command: /{name}"); + eprintln!("{}", format_unknown_slash_command(&name)); false } }) @@ -1592,7 +1758,7 @@ impl LiveCli { session_path: Option, ) -> Result> { let Some(session_ref) = session_path else { - println!("Usage: /resume "); + println!("{}", render_resume_usage()); return Ok(false); }; @@ -2002,12 +2168,23 @@ fn create_managed_session_handle( } fn resolve_session_reference(reference: &str) -> Result> { + if SESSION_REFERENCE_ALIASES + .iter() + .any(|alias| reference.eq_ignore_ascii_case(alias)) + { + let latest = latest_managed_session()?; + return Ok(SessionHandle { + id: latest.id, + path: latest.path, + }); + } + let direct = PathBuf::from(reference); let looks_like_path = direct.extension().is_some() || direct.components().count() > 1; let path = if direct.exists() { direct } else if looks_like_path { - return Err(format!("session not found: {reference}").into()); + return Err(format_missing_session_reference(reference).into()); } else { resolve_managed_session_path(reference)? }; @@ -2031,7 +2208,7 @@ fn resolve_managed_session_path(session_id: &str) -> Result bool { @@ -2051,53 +2228,79 @@ fn list_managed_sessions() -> Result, Box Result> { + list_managed_sessions()? + .into_iter() + .next() + .ok_or_else(|| format_no_managed_sessions().into()) +} + +fn format_missing_session_reference(reference: &str) -> String { + format!( + "session not found: {reference}\nHint: managed sessions live in .claw/sessions/. Try `{LATEST_SESSION_REFERENCE}` for the most recent session or `/session list` in the REPL." + ) +} + +fn format_no_managed_sessions() -> String { + format!( + "no managed sessions found in .claw/sessions/\nStart `claw` to create a session, then rerun with `--resume {LATEST_SESSION_REFERENCE}`." + ) +} + fn render_session_list(active_session_id: &str) -> Result> { let sessions = list_managed_sessions()?; let mut lines = vec![ @@ -2129,7 +2332,7 @@ fn render_session_list(active_session_id: &str) -> Result Result String { + let now = std::time::SystemTime::now() + .duration_since(UNIX_EPOCH) + .ok() + .map_or(modified_epoch_millis, |duration| duration.as_millis()); + let delta_seconds = now + .saturating_sub(modified_epoch_millis) + .checked_div(1_000) + .unwrap_or_default(); + match delta_seconds { + 0..=4 => "just-now".to_string(), + 5..=59 => format!("{delta_seconds}s-ago"), + 60..=3_599 => format!("{}m-ago", delta_seconds / 60), + 3_600..=86_399 => format!("{}h-ago", delta_seconds / 3_600), + _ => format!("{}d-ago", delta_seconds / 86_400), + } +} + fn render_repl_help() -> String { [ "REPL".to_string(), @@ -2146,6 +2367,9 @@ fn render_repl_help() -> String { " Tab Complete slash commands".to_string(), " Ctrl-C Clear input (or exit on empty prompt)".to_string(), " Shift+Enter/Ctrl+J Insert a newline".to_string(), + " Auto-save .claw/sessions/.jsonl".to_string(), + " Resume latest /resume latest".to_string(), + " Browse sessions /session list".to_string(), String::new(), render_slash_command_help(), ] @@ -3146,7 +3370,8 @@ fn build_runtime( allowed_tools: Option, permission_mode: PermissionMode, progress_reporter: Option, -) -> Result, Box> { +) -> Result, Box> +{ let (feature_config, tool_registry) = build_runtime_plugin_state()?; let mut runtime = ConversationRuntime::new_with_features( session, @@ -3286,7 +3511,6 @@ impl AnthropicRuntimeClient { progress_reporter, }) } - } fn resolve_cli_auth_source() -> Result> { @@ -4023,7 +4247,9 @@ fn push_prompt_cache_record(client: &AnthropicClient, events: &mut Vec Option { +fn prompt_cache_record_to_runtime_event( + record: api::PromptCacheRecord, +) -> Option { let cache_break = record.cache_break?; Some(PromptCacheEvent { unexpected: cache_break.unexpected, @@ -4146,6 +4372,7 @@ fn convert_messages(messages: &[ConversationMessage]) -> Vec { .collect() } +#[allow(clippy::too_many_lines)] fn print_help_to(out: &mut impl Write) -> io::Result<()> { writeln!(out, "claw v{VERSION}")?; writeln!(out)?; @@ -4167,7 +4394,7 @@ fn print_help_to(out: &mut impl Write) -> io::Result<()> { writeln!(out, " Shorthand non-interactive prompt mode")?; writeln!( out, - " claw --resume SESSION.jsonl [/status] [/compact] [...]" + " claw --resume [SESSION.jsonl|session-id|latest] [/status] [/compact] [...]" )?; writeln!( out, @@ -4217,6 +4444,20 @@ fn print_help_to(out: &mut impl Write) -> io::Result<()> { .collect::>() .join(", "); writeln!(out, "Resume-safe commands: {resume_commands}")?; + writeln!(out)?; + writeln!(out, "Session shortcuts:")?; + writeln!( + out, + " REPL turns auto-save to .claw/sessions/.{PRIMARY_SESSION_EXTENSION}" + )?; + writeln!( + out, + " Use `{LATEST_SESSION_REFERENCE}` with --resume, /resume, or /session switch to target the newest saved session" + )?; + writeln!( + out, + " Use /session list in the REPL to browse managed sessions" + )?; writeln!(out, "Examples:")?; writeln!(out, " claw --model claude-opus \"summarize this repo\"")?; writeln!( @@ -4227,9 +4468,10 @@ fn print_help_to(out: &mut impl Write) -> io::Result<()> { out, " claw --allowedTools read,glob \"summarize Cargo.toml\"" )?; + writeln!(out, " claw --resume {LATEST_SESSION_REFERENCE}")?; writeln!( out, - " claw --resume session.jsonl /status /diff /export notes.txt" + " claw --resume {LATEST_SESSION_REFERENCE} /status /diff /export notes.txt" )?; writeln!(out, " claw agents")?; writeln!(out, " claw /skills")?; @@ -4245,18 +4487,18 @@ fn print_help() { #[cfg(test)] mod tests { use super::{ - describe_tool_progress, filter_tool_specs, format_compact_report, format_cost_report, - format_internal_prompt_progress_line, format_model_report, format_model_switch_report, - format_permissions_report, + create_managed_session_handle, describe_tool_progress, filter_tool_specs, + format_compact_report, format_cost_report, format_internal_prompt_progress_line, + format_model_report, format_model_switch_report, format_permissions_report, format_permissions_switch_report, format_resume_report, format_status_report, - format_tool_call_start, format_tool_result, normalize_permission_mode, parse_args, - parse_git_status_branch, parse_git_status_metadata_for, permission_policy, - print_help_to, push_output_block, render_config_report, render_diff_report, - render_memory_report, render_repl_help, resolve_model_alias, response_to_events, + format_tool_call_start, format_tool_result, format_unknown_slash_command, + normalize_permission_mode, parse_args, parse_git_status_branch, + parse_git_status_metadata_for, permission_policy, print_help_to, push_output_block, + render_config_report, render_diff_report, render_memory_report, render_repl_help, + render_resume_usage, resolve_model_alias, resolve_session_reference, response_to_events, resume_supported_slash_commands, run_resume_command, status_context, CliAction, - CliOutputFormat, InternalPromptProgressEvent, - InternalPromptProgressState, SlashCommand, StatusUsage, DEFAULT_MODEL, - create_managed_session_handle, resolve_session_reference, + CliOutputFormat, InternalPromptProgressEvent, InternalPromptProgressState, SlashCommand, + StatusUsage, DEFAULT_MODEL, }; use api::{MessageResponse, OutputContentBlock, Usage}; use plugins::{PluginTool, PluginToolDefinition, PluginToolPermission}; @@ -4551,6 +4793,25 @@ mod tests { ); } + #[test] + fn parses_resume_flag_without_path_as_latest_session() { + assert_eq!( + parse_args(&["--resume".to_string()]).expect("args should parse"), + CliAction::ResumeSession { + session_path: PathBuf::from("latest"), + commands: vec![], + } + ); + assert_eq!( + parse_args(&["--resume".to_string(), "/status".to_string()]) + .expect("resume shortcut should parse"), + CliAction::ResumeSession { + session_path: PathBuf::from("latest"), + commands: vec!["/status".to_string()], + } + ); + } + #[test] fn parses_resume_flag_with_multiple_slash_commands() { let args = vec![ @@ -4573,6 +4834,14 @@ mod tests { ); } + #[test] + fn rejects_unknown_options_with_helpful_guidance() { + let error = parse_args(&["--resum".to_string()]).expect_err("unknown option should fail"); + assert!(error.contains("unknown option: --resum")); + assert!(error.contains("Did you mean --resume?")); + assert!(error.contains("claw --help")); + } + #[test] fn filtered_tool_specs_respect_allowlist() { let allowed = ["read_file", "grep_search"] @@ -4643,6 +4912,8 @@ mod tests { assert!(help.contains("/agents")); assert!(help.contains("/skills")); assert!(help.contains("/exit")); + assert!(help.contains("Auto-save .claw/sessions/.jsonl")); + assert!(help.contains("Resume latest /resume latest")); } #[test] @@ -5022,8 +5293,10 @@ mod tests { let mut help = Vec::new(); print_help_to(&mut help).expect("help should render"); let help = String::from_utf8(help).expect("help should be utf8"); - assert!(help.contains("claw --resume SESSION.jsonl")); - assert!(help.contains("claw --resume session.jsonl /status /diff /export notes.txt")); + assert!(help.contains("claw --resume [SESSION.jsonl|session-id|latest]")); + assert!(help.contains("Use `latest` with --resume, /resume, or /session switch")); + assert!(help.contains("claw --resume latest")); + assert!(help.contains("claw --resume latest /status /diff /export notes.txt")); } #[test] @@ -5051,14 +5324,68 @@ mod tests { let resolved = resolve_session_reference("legacy").expect("legacy session should resolve"); assert_eq!( - resolved.path.canonicalize().expect("resolved path should exist"), - legacy_path.canonicalize().expect("legacy path should exist") + resolved + .path + .canonicalize() + .expect("resolved path should exist"), + legacy_path + .canonicalize() + .expect("legacy path should exist") ); std::env::set_current_dir(previous).expect("restore cwd"); std::fs::remove_dir_all(workspace).expect("workspace should clean up"); } + #[test] + fn latest_session_alias_resolves_most_recent_managed_session() { + let _guard = cwd_lock().lock().expect("cwd lock"); + let workspace = temp_workspace("latest-session-alias"); + std::fs::create_dir_all(&workspace).expect("workspace should create"); + let previous = std::env::current_dir().expect("cwd"); + std::env::set_current_dir(&workspace).expect("switch cwd"); + + let older = create_managed_session_handle("session-older").expect("older handle"); + Session::new() + .with_persistence_path(older.path.clone()) + .save_to_path(&older.path) + .expect("older session should save"); + std::thread::sleep(Duration::from_millis(20)); + let newer = create_managed_session_handle("session-newer").expect("newer handle"); + Session::new() + .with_persistence_path(newer.path.clone()) + .save_to_path(&newer.path) + .expect("newer session should save"); + + let resolved = resolve_session_reference("latest").expect("latest session should resolve"); + assert_eq!( + resolved + .path + .canonicalize() + .expect("resolved path should exist"), + newer.path.canonicalize().expect("newer path should exist") + ); + + std::env::set_current_dir(previous).expect("restore cwd"); + std::fs::remove_dir_all(workspace).expect("workspace should clean up"); + } + + #[test] + fn unknown_slash_command_guidance_suggests_nearby_commands() { + let message = format_unknown_slash_command("stats"); + assert!(message.contains("Unknown slash command: /stats")); + assert!(message.contains("/status")); + assert!(message.contains("/help")); + } + + #[test] + fn resume_usage_mentions_latest_shortcut() { + let usage = render_resume_usage(); + assert!(usage.contains("/resume ")); + assert!(usage.contains(".claw/sessions/.jsonl")); + assert!(usage.contains("/session list")); + } + fn cwd_lock() -> &'static Mutex<()> { static LOCK: OnceLock> = OnceLock::new(); LOCK.get_or_init(|| Mutex::new(()))