From a9de547bd238566f4c7f90427f151b4d6e7074a1 Mon Sep 17 00:00:00 2001 From: Zacaria <> Date: Thu, 12 Mar 2026 07:41:21 +0100 Subject: [PATCH] feat(init): add codex cli init support Signed-off-by: Zacaria <> --- hooks/rtk-awareness-codex.md | 32 ++++ src/init.rs | 337 ++++++++++++++++++++++++++++++++++- src/main.rs | 17 +- 3 files changed, 372 insertions(+), 14 deletions(-) create mode 100644 hooks/rtk-awareness-codex.md diff --git a/hooks/rtk-awareness-codex.md b/hooks/rtk-awareness-codex.md new file mode 100644 index 000000000..7ae285e1a --- /dev/null +++ b/hooks/rtk-awareness-codex.md @@ -0,0 +1,32 @@ +# RTK - Rust Token Killer (Codex CLI) + +**Usage**: Token-optimized CLI proxy for shell commands. + +## Rule + +Always prefix shell commands with `rtk`. + +Examples: + +```bash +rtk git status +rtk cargo test +rtk npm run build +rtk pytest -q +``` + +## Meta Commands + +```bash +rtk gain # Token savings analytics +rtk gain --history # Recent command savings history +rtk proxy # Run raw command without filtering +``` + +## Verification + +```bash +rtk --version +rtk gain +which rtk +``` diff --git a/src/init.rs b/src/init.rs index 2fbd11fb3..81bef7f7c 100644 --- a/src/init.rs +++ b/src/init.rs @@ -11,6 +11,7 @@ const REWRITE_HOOK: &str = include_str!("../hooks/rtk-rewrite.sh"); // Embedded slim RTK awareness instructions const RTK_SLIM: &str = include_str!("../hooks/rtk-awareness.md"); +const RTK_SLIM_CODEX: &str = include_str!("../hooks/rtk-awareness-codex.md"); /// Control flow for settings.json patching #[derive(Debug, Clone, Copy, PartialEq)] @@ -170,14 +171,21 @@ pub fn run( global: bool, claude_md: bool, hook_only: bool, + codex: bool, patch_mode: PatchMode, verbose: u8, ) -> Result<()> { - // Mode selection - match (claude_md, hook_only) { - (true, _) => run_claude_md_mode(global, verbose), - (false, true) => run_hook_only_mode(global, patch_mode, verbose), - (false, false) => run_default_mode(global, patch_mode, verbose), + match (codex, claude_md, hook_only) { + (true, true, _) => anyhow::bail!("--codex cannot be combined with --claude-md"), + (true, _, true) => anyhow::bail!("--codex cannot be combined with --hook-only"), + (true, _, false) => match patch_mode { + PatchMode::Ask => run_codex_mode(global, verbose), + PatchMode::Auto => anyhow::bail!("--codex cannot be combined with --auto-patch"), + PatchMode::Skip => anyhow::bail!("--codex cannot be combined with --no-patch"), + }, + (false, true, _) => run_claude_md_mode(global, verbose), + (false, false, true) => run_hook_only_mode(global, patch_mode, verbose), + (false, false, false) => run_default_mode(global, patch_mode, verbose), } } @@ -410,8 +418,16 @@ fn remove_hook_from_settings(verbose: u8) -> Result { Ok(removed) } -/// Full uninstall: remove hook, RTK.md, @RTK.md reference, settings.json entry -pub fn uninstall(global: bool, verbose: u8) -> Result<()> { +/// Full uninstall for Claude or Codex artifacts. +pub fn uninstall(global: bool, codex: bool, verbose: u8) -> Result<()> { + if codex { + return uninstall_codex(global, verbose); + } + + uninstall_claude(global, verbose) +} + +fn uninstall_claude(global: bool, verbose: u8) -> Result<()> { if !global { anyhow::bail!("Uninstall only works with --global flag. For local projects, manually remove RTK from CLAUDE.md"); } @@ -482,6 +498,49 @@ pub fn uninstall(global: bool, verbose: u8) -> Result<()> { Ok(()) } +fn uninstall_codex(global: bool, verbose: u8) -> Result<()> { + if !global { + anyhow::bail!( + "Uninstall only works with --global flag. For local projects, manually remove RTK from AGENTS.md" + ); + } + + let codex_dir = resolve_codex_dir()?; + let removed = uninstall_codex_at(&codex_dir, verbose)?; + + if removed.is_empty() { + println!("RTK was not installed for Codex CLI (nothing to remove)"); + } else { + println!("RTK uninstalled for Codex CLI:"); + for item in removed { + println!(" - {}", item); + } + } + + Ok(()) +} + +fn uninstall_codex_at(codex_dir: &Path, verbose: u8) -> Result> { + let mut removed = Vec::new(); + + let rtk_md_path = codex_dir.join("RTK.md"); + if rtk_md_path.exists() { + fs::remove_file(&rtk_md_path) + .with_context(|| format!("Failed to remove RTK.md: {}", rtk_md_path.display()))?; + if verbose > 0 { + eprintln!("Removed RTK.md: {}", rtk_md_path.display()); + } + removed.push(format!("RTK.md: {}", rtk_md_path.display())); + } + + let agents_md_path = codex_dir.join("AGENTS.md"); + if remove_rtk_reference_from_agents(&agents_md_path, verbose)? { + removed.push("AGENTS.md: removed @RTK.md reference".to_string()); + } + + Ok(removed) +} + /// Orchestrator: patch settings.json with RTK hook /// Handles reading, checking, prompting, merging, backing up, and atomic writing fn patch_settings_json(hook_path: &Path, mode: PatchMode, verbose: u8) -> Result { @@ -849,6 +908,51 @@ fn run_claude_md_mode(global: bool, verbose: u8) -> Result<()> { Ok(()) } +/// Codex mode: slim RTK.md + @RTK.md reference in AGENTS.md +fn run_codex_mode(global: bool, verbose: u8) -> Result<()> { + let (agents_md_path, rtk_md_path) = if global { + let codex_dir = resolve_codex_dir()?; + (codex_dir.join("AGENTS.md"), codex_dir.join("RTK.md")) + } else { + (PathBuf::from("AGENTS.md"), PathBuf::from("RTK.md")) + }; + + if global { + if let Some(parent) = agents_md_path.parent() { + fs::create_dir_all(parent).with_context(|| { + format!( + "Failed to create Codex config directory: {}", + parent.display() + ) + })?; + } + } + + write_if_changed(&rtk_md_path, RTK_SLIM_CODEX, "RTK.md", verbose)?; + let added_ref = patch_agents_md(&agents_md_path, verbose)?; + + println!("\nRTK configured for Codex CLI.\n"); + println!(" RTK.md: {}", rtk_md_path.display()); + if added_ref { + println!(" AGENTS.md: @RTK.md reference added"); + } else { + println!(" AGENTS.md: @RTK.md reference already present"); + } + if global { + println!( + "\n Codex global instructions path: {}", + agents_md_path.display() + ); + } else { + println!( + "\n Codex project instructions path: {}", + agents_md_path.display() + ); + } + + Ok(()) +} + // --- upsert_rtk_block: idempotent RTK block management --- #[derive(Debug, Clone, Copy, PartialEq)] @@ -961,6 +1065,83 @@ fn patch_claude_md(path: &Path, verbose: u8) -> Result { Ok(migrated) } +/// Patch AGENTS.md: add @RTK.md, migrate old inline block if present +fn patch_agents_md(path: &Path, verbose: u8) -> Result { + let mut content = if path.exists() { + fs::read_to_string(path) + .with_context(|| format!("Failed to read AGENTS.md: {}", path.display()))? + } else { + String::new() + }; + + let mut migrated = false; + if content.contains("\nold\n\n", + ) + .unwrap(); + + let added = patch_agents_md(&agents_md, 0).unwrap(); + + assert!(added); + let content = fs::read_to_string(&agents_md).unwrap(); + assert!(!content.contains("old")); + assert_eq!(content.matches("@RTK.md").count(), 1); + } + + #[test] + fn test_uninstall_codex_at_is_idempotent() { + let temp = TempDir::new().unwrap(); + let codex_dir = temp.path(); + let agents_md = codex_dir.join("AGENTS.md"); + let rtk_md = codex_dir.join("RTK.md"); + + fs::write(&agents_md, "# Team rules\n\n@RTK.md\n").unwrap(); + fs::write(&rtk_md, "codex config").unwrap(); + + let removed_first = uninstall_codex_at(codex_dir, 0).unwrap(); + let removed_second = uninstall_codex_at(codex_dir, 0).unwrap(); + + assert_eq!(removed_first.len(), 2); + assert!(removed_second.is_empty()); + assert!(!rtk_md.exists()); + + let content = fs::read_to_string(&agents_md).unwrap(); + assert!(!content.contains("@RTK.md")); + assert!(content.contains("# Team rules")); + } + #[test] fn test_local_init_unchanged() { // Local init should use claude-md mode diff --git a/src/main.rs b/src/main.rs index dce31dc3b..bfc82a126 100644 --- a/src/main.rs +++ b/src/main.rs @@ -303,9 +303,9 @@ enum Commands { extra_args: Vec, }, - /// Initialize rtk instructions in CLAUDE.md + /// Initialize rtk instructions for assistant CLI usage Init { - /// Add to global ~/.claude/CLAUDE.md instead of local + /// Add to global assistant config directory instead of local project file #[arg(short, long)] global: bool, @@ -329,9 +329,13 @@ enum Commands { #[arg(long = "no-patch", group = "patch")] no_patch: bool, - /// Remove all RTK artifacts (hook, RTK.md, CLAUDE.md reference, settings.json entry) + /// Remove RTK artifacts for the selected assistant mode #[arg(long)] uninstall: bool, + + /// Target Codex CLI (uses AGENTS.md + RTK.md, no Claude hook patching) + #[arg(long)] + codex: bool, }, /// Download with compact output (strips progress bars) @@ -1367,11 +1371,12 @@ fn main() -> Result<()> { auto_patch, no_patch, uninstall, + codex, } => { if show { - init::show_config()?; + init::show_config(codex)?; } else if uninstall { - init::uninstall(global, cli.verbose)?; + init::uninstall(global, codex, cli.verbose)?; } else { let patch_mode = if auto_patch { init::PatchMode::Auto @@ -1380,7 +1385,7 @@ fn main() -> Result<()> { } else { init::PatchMode::Ask }; - init::run(global, claude_md, hook_only, patch_mode, cli.verbose)?; + init::run(global, claude_md, hook_only, codex, patch_mode, cli.verbose)?; } }