mod cache_cmd;
mod gain;
use std::path::Path;
use clap::{Parser, Subcommand};
use tokf::config;
use tokf::config::types::FilterConfig;
use tokf::filter;
use tokf::hook;
use tokf::rewrite;
use tokf::runner;
use tokf::skill;
use tokf::tracking;
#[derive(Parser)]
#[command(
name = "tokf",
about = "Token filter — compress command output for LLM context"
)]
#[allow(clippy::struct_excessive_bools)] struct Cli {
#[arg(long, global = true)]
timing: bool,
#[arg(long, global = true)]
no_filter: bool,
#[arg(short, long, global = true)]
verbose: bool,
#[arg(long, global = true)]
no_cache: bool,
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
Run {
#[arg(trailing_var_arg = true, required = true)]
command_args: Vec<String>,
},
Check {
filter_path: String,
},
Test {
filter_path: String,
fixture_path: String,
#[arg(long, default_value_t = 0)]
exit_code: i32,
},
Ls,
Rewrite {
command: String,
},
Which {
command: String,
},
Show {
filter: String,
},
Hook {
#[command(subcommand)]
action: HookAction,
},
Skill {
#[command(subcommand)]
action: SkillAction,
},
Cache {
#[command(subcommand)]
action: cache_cmd::CacheAction,
},
Gain {
#[arg(long)]
daily: bool,
#[arg(long, name = "by-filter")]
by_filter: bool,
#[arg(long)]
json: bool,
},
}
#[derive(Subcommand)]
enum SkillAction {
Install {
#[arg(long)]
global: bool,
},
}
#[derive(Subcommand)]
enum HookAction {
Handle,
Install {
#[arg(long)]
global: bool,
},
}
fn find_filter(
command_args: &[String],
verbose: bool,
no_cache: bool,
) -> anyhow::Result<(Option<FilterConfig>, usize)> {
let search_dirs = config::default_search_dirs();
let resolved = if no_cache {
config::discover_all_filters(&search_dirs)?
} else {
config::cache::discover_with_cache(&search_dirs)?
};
let words: Vec<&str> = command_args.iter().map(String::as_str).collect();
for filter in &resolved {
if let Some(consumed) = filter.matches(&words) {
if verbose {
eprintln!(
"[tokf] matched {} (command: \"{}\") in {}",
filter.relative_path.display(),
filter.config.command.first(),
filter
.source_path
.parent()
.map_or("?", |p| p.to_str().unwrap_or("?")),
);
}
return Ok((Some(filter.config.clone()), consumed));
}
}
if verbose {
eprintln!(
"[tokf] no filter found for '{}', passing through",
words.join(" ")
);
}
Ok((None, 0))
}
fn run_command(
filter_cfg: Option<&FilterConfig>,
words_consumed: usize,
command_args: &[String],
remaining_args: &[String],
) -> anyhow::Result<runner::CommandResult> {
if let Some(cfg) = filter_cfg
&& let Some(run_cmd) = &cfg.run
{
runner::execute_shell(run_cmd, remaining_args)
} else if words_consumed > 0 {
let cmd_str = command_args[..words_consumed].join(" ");
runner::execute(&cmd_str, remaining_args)
} else {
runner::execute(&command_args[0], remaining_args)
}
}
#[allow(clippy::too_many_arguments)]
fn record_run(
command_args: &[String],
filter_name: Option<&str>,
input_bytes: usize,
output_bytes: usize,
filter_time_ms: u128,
exit_code: i32,
) {
let Some(path) = tracking::db_path() else {
eprintln!("[tokf] tracking: cannot determine DB path");
return;
};
let conn = match tracking::open_db(&path) {
Ok(c) => c,
Err(e) => {
eprintln!("[tokf] tracking error (db open): {e:#}");
return;
}
};
let command = command_args.join(" ");
let event = tracking::build_event(
&command,
filter_name,
input_bytes,
output_bytes,
filter_time_ms,
exit_code,
);
if let Err(e) = tracking::record_event(&conn, &event) {
eprintln!("[tokf] tracking error (record): {e:#}");
}
}
fn cmd_run(command_args: &[String], cli: &Cli) -> anyhow::Result<i32> {
let (filter_cfg, words_consumed) = if cli.no_filter {
(None, 0)
} else {
find_filter(command_args, cli.verbose, cli.no_cache)?
};
let remaining_args: Vec<String> = if words_consumed > 0 {
command_args[words_consumed..].to_vec()
} else if command_args.len() > 1 {
command_args[1..].to_vec()
} else {
vec![]
};
let cmd_result = run_command(
filter_cfg.as_ref(),
words_consumed,
command_args,
&remaining_args,
)?;
let Some(cfg) = filter_cfg else {
let bytes = cmd_result.combined.len();
if !cmd_result.combined.is_empty() {
println!("{}", cmd_result.combined);
}
record_run(command_args, None, bytes, bytes, 0, cmd_result.exit_code);
return Ok(cmd_result.exit_code);
};
let input_bytes = cmd_result.combined.len();
let start = std::time::Instant::now();
let filtered = filter::apply(&cfg, &cmd_result, &remaining_args);
let elapsed = start.elapsed();
if cli.timing {
eprintln!("[tokf] filter took {:.1}ms", elapsed.as_secs_f64() * 1000.0);
}
let output_bytes = filtered.output.len();
if !filtered.output.is_empty() {
println!("{}", filtered.output);
}
let filter_name = cfg.command.first();
record_run(
command_args,
Some(filter_name),
input_bytes,
output_bytes,
elapsed.as_millis(),
cmd_result.exit_code,
);
Ok(cmd_result.exit_code)
}
fn cmd_check(filter_path: &Path) -> i32 {
match config::try_load_filter(filter_path) {
Ok(Some(cfg)) => {
eprintln!(
"[tokf] {} is valid (command: \"{}\")",
filter_path.display(),
cfg.command.first()
);
0
}
Ok(None) => {
eprintln!("[tokf] file not found: {}", filter_path.display());
1
}
Err(e) => {
eprintln!("[tokf] error: {e:#}");
1
}
}
}
fn cmd_test(
filter_path: &Path,
fixture_path: &Path,
exit_code: i32,
cli: &Cli,
) -> anyhow::Result<i32> {
let cfg = config::try_load_filter(filter_path)?
.ok_or_else(|| anyhow::anyhow!("filter not found: {}", filter_path.display()))?;
let fixture = std::fs::read_to_string(fixture_path)
.map_err(|e| anyhow::anyhow!("failed to read fixture: {}: {e}", fixture_path.display()))?;
let combined = fixture.trim_end().to_string();
let cmd_result = runner::CommandResult {
stdout: String::new(),
stderr: String::new(),
exit_code,
combined,
};
let start = std::time::Instant::now();
let filtered = filter::apply(&cfg, &cmd_result, &[]);
let elapsed = start.elapsed();
if cli.timing {
eprintln!("[tokf] filter took {:.1}ms", elapsed.as_secs_f64() * 1000.0);
}
if !filtered.output.is_empty() {
println!("{}", filtered.output);
}
Ok(0)
}
fn cmd_ls(verbose: bool) -> i32 {
let search_dirs = config::default_search_dirs();
let Ok(filters) = config::cache::discover_with_cache(&search_dirs) else {
eprintln!("[tokf] error: failed to discover filters");
return 1;
};
for filter in &filters {
let display_name = filter
.relative_path
.with_extension("")
.display()
.to_string();
println!(
"{display_name} \u{2192} {}",
filter.config.command.first()
);
if verbose {
eprintln!(
"[tokf] source: {} [{}]",
filter.source_path.display(),
filter.priority_label()
);
let patterns = filter.config.command.patterns();
if patterns.len() > 1 {
for p in patterns {
eprintln!("[tokf] pattern: \"{p}\"");
}
}
}
}
0
}
fn cmd_which(command: &str, verbose: bool) -> i32 {
let search_dirs = config::default_search_dirs();
let Ok(filters) = config::cache::discover_with_cache(&search_dirs) else {
eprintln!("[tokf] error: failed to discover filters");
return 1;
};
let words: Vec<&str> = command.split_whitespace().collect();
for filter in &filters {
if filter.matches(&words).is_some() {
let display_name = filter
.relative_path
.with_extension("")
.display()
.to_string();
println!(
"{} [{}] command: \"{}\"",
display_name,
filter.priority_label(),
filter.config.command.first()
);
if verbose {
eprintln!("[tokf] source: {}", filter.source_path.display());
}
return 0;
}
}
eprintln!("[tokf] no filter found for \"{command}\"");
1
}
fn main() {
let cli = Cli::parse();
let exit_code = match &cli.command {
Commands::Run { command_args } => cmd_run(command_args, &cli).unwrap_or_else(|e| {
eprintln!("[tokf] error: {e:#}");
1
}),
Commands::Check { filter_path } => cmd_check(Path::new(filter_path)),
Commands::Test {
filter_path,
fixture_path,
exit_code,
} => cmd_test(
Path::new(filter_path),
Path::new(fixture_path),
*exit_code,
&cli,
)
.unwrap_or_else(|e| {
eprintln!("[tokf] error: {e:#}");
1
}),
Commands::Ls => cmd_ls(cli.verbose),
Commands::Rewrite { command } => cmd_rewrite(command),
Commands::Which { command } => cmd_which(command, cli.verbose),
Commands::Show { filter } => cmd_show(filter),
Commands::Hook { action } => match action {
HookAction::Handle => cmd_hook_handle(),
HookAction::Install { global } => cmd_hook_install(*global),
},
Commands::Skill { action } => match action {
SkillAction::Install { global } => cmd_skill_install(*global),
},
Commands::Cache { action } => cache_cmd::run_cache_action(action),
Commands::Gain {
daily,
by_filter,
json,
} => gain::cmd_gain(*daily, *by_filter, *json),
};
std::process::exit(exit_code);
}
fn cmd_show(filter: &str) -> i32 {
let filter_name = filter.strip_suffix(".toml").unwrap_or(filter);
let search_dirs = config::default_search_dirs();
let Ok(filters) = config::cache::discover_with_cache(&search_dirs) else {
eprintln!("[tokf] error: failed to discover filters");
return 1;
};
let found = filters
.iter()
.find(|f| f.relative_path.with_extension("").to_string_lossy() == filter_name);
let Some(resolved) = found else {
eprintln!("[tokf] filter not found: {filter}");
return 1;
};
let content = if resolved.priority == u8::MAX {
if let Some(c) = config::get_embedded_filter(&resolved.relative_path) {
c.to_string()
} else {
eprintln!("[tokf] error: embedded filter not readable");
return 1;
}
} else {
match std::fs::read_to_string(&resolved.source_path) {
Ok(c) => c,
Err(e) => {
eprintln!("[tokf] error reading filter: {e}");
return 1;
}
}
};
print!("{content}");
0
}
fn cmd_rewrite(command: &str) -> i32 {
let result = rewrite::rewrite(command);
println!("{result}");
0
}
fn cmd_skill_install(global: bool) -> i32 {
match skill::install(global) {
Ok(()) => 0,
Err(e) => {
eprintln!("[tokf] error: {e:#}");
1
}
}
}
fn cmd_hook_handle() -> i32 {
hook::handle();
0
}
fn cmd_hook_install(global: bool) -> i32 {
match hook::install(global) {
Ok(()) => 0,
Err(e) => {
eprintln!("[tokf] error: {e:#}");
1
}
}
}