diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index c58edfd2e..ee375f25e 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -297,8 +297,8 @@ SHARED utils.rs Helpers N/A ✓ ### Module Count Breakdown -- **Command Modules**: 34 (directly exposed to users) -- **Infrastructure Modules**: 20 (utils, filter, tracking, tee, config, init, gain, toml_filter, verify_cmd, etc.) +- **Command Modules**: 44 (directly exposed to users) +- **Infrastructure Modules**: 22 (utils, filter, tracking, tee, config, init, gain, toml_filter, verify_cmd, trust, etc.) - **Git Commands**: 7 operations (status, diff, log, add, commit, push, branch/checkout) - **JS/TS Tooling**: 8 modules (modern frontend/fullstack development) - **Python Tooling**: 3 modules (ruff, pytest, pip) 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 f6982ac9a..eaf104f8f 100644 --- a/src/init.rs +++ b/src/init.rs @@ -14,6 +14,7 @@ const OPENCODE_PLUGIN: &str = include_str!("../hooks/opencode-rtk.ts"); // 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"); /// Template written by `rtk init` when no filters.toml exists yet. const FILTERS_TEMPLATE: &str = r#"# Project-local RTK filters — commit this file with your repo. @@ -206,20 +207,41 @@ pub fn run( install_opencode: bool, claude_md: bool, hook_only: bool, + codex: bool, patch_mode: PatchMode, verbose: u8, ) -> Result<()> { - if install_opencode && !global { - anyhow::bail!("OpenCode plugin is global-only. Use: rtk init -g --opencode"); - } - - // Mode selection - match (install_claude, install_opencode, claude_md, hook_only) { - (false, true, _, _) => run_opencode_only_mode(verbose), - (true, opencode, true, _) => run_claude_md_mode(global, verbose, opencode), - (true, opencode, false, true) => run_hook_only_mode(global, patch_mode, verbose, opencode), - (true, opencode, false, false) => run_default_mode(global, patch_mode, verbose, opencode), - (false, false, _, _) => { + match ( + codex, + install_claude, + install_opencode, + global, + claude_md, + hook_only, + patch_mode, + ) { + (true, _, true, _, _, _, _) => anyhow::bail!("--codex cannot be combined with --opencode"), + (true, _, _, _, true, _, _) => anyhow::bail!("--codex cannot be combined with --claude-md"), + (true, _, _, _, _, true, _) => anyhow::bail!("--codex cannot be combined with --hook-only"), + (true, _, _, _, _, _, PatchMode::Auto) => { + anyhow::bail!("--codex cannot be combined with --auto-patch") + } + (true, _, _, _, _, _, PatchMode::Skip) => { + anyhow::bail!("--codex cannot be combined with --no-patch") + } + (true, _, _, _, _, _, PatchMode::Ask) => run_codex_mode(global, verbose), + (false, _, true, false, _, _, _) => { + anyhow::bail!("OpenCode plugin is global-only. Use: rtk init -g --opencode") + } + (false, false, true, _, _, _, _) => run_opencode_only_mode(verbose), + (false, true, opencode, _, true, _, _) => run_claude_md_mode(global, verbose, opencode), + (false, true, opencode, _, false, true, _) => { + run_hook_only_mode(global, patch_mode, verbose, opencode) + } + (false, true, opencode, _, false, false, _) => { + run_default_mode(global, patch_mode, verbose, opencode) + } + (false, false, false, _, _, _, _) => { anyhow::bail!("at least one of install_claude or install_opencode must be true") } } @@ -458,8 +480,11 @@ 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, gemini: bool, verbose: u8) -> Result<()> { +/// Full uninstall for Claude, Gemini, or Codex artifacts. +pub fn uninstall(global: bool, gemini: bool, codex: bool, verbose: u8) -> Result<()> { + if codex { + return uninstall_codex(global, verbose); + } if !global { anyhow::bail!("Uninstall only works with --global flag. For local projects, manually remove RTK from CLAUDE.md"); } @@ -552,6 +577,49 @@ pub fn uninstall(global: bool, gemini: 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( @@ -1037,6 +1105,51 @@ fn run_claude_md_mode(global: bool, verbose: u8, install_opencode: bool) -> Resu 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)] @@ -1149,6 +1262,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 81350deae..1429b1da8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -323,9 +323,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, @@ -357,9 +357,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) @@ -1632,11 +1636,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, gemini, cli.verbose)?; + init::uninstall(global, gemini, codex, cli.verbose)?; } else if gemini { let patch_mode = if auto_patch { init::PatchMode::Auto @@ -1663,6 +1668,7 @@ fn main() -> Result<()> { install_opencode, claude_md, hook_only, + codex, patch_mode, cli.verbose, )?;