use std::io::Read;
use std::path::{Path, PathBuf};
use serde_json::Value;
const TOKENSAVE_RESEARCH_BLOCK_REASON: &str = "STOP: Use tokensave MCP tools \
(tokensave_context, tokensave_search, tokensave_callees, tokensave_callers, \
tokensave_impact, tokensave_files, tokensave_affected) instead of agents for \
code research. Tokensave is faster and more precise for symbol relationships, \
call paths, and code structure. Only use agents for code exploration if you \
have already tried tokensave and it cannot answer the question.";
pub fn hook_pre_tool_use() {
let tool_input = std::env::var("TOOL_INPUT").unwrap_or_default();
let decision = evaluate_hook_decision(&tool_input);
if !decision.is_empty() {
println!("{decision}");
}
}
pub fn evaluate_hook_decision(tool_input: &str) -> String {
let block_msg = serde_json::json!({
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": TOKENSAVE_RESEARCH_BLOCK_REASON
}
});
let parsed: serde_json::Value =
serde_json::from_str(tool_input).unwrap_or_else(|_| serde_json::json!({}));
if parsed.get("subagent_type").and_then(|v| v.as_str()) == Some("Explore") {
return block_msg.to_string();
}
if let Some(prompt) = parsed.get("prompt").and_then(|v| v.as_str()) {
if is_code_research_prompt(prompt) {
return block_msg.to_string();
}
}
String::new()
}
fn is_code_research_prompt(prompt: &str) -> bool {
let lower = prompt.to_ascii_lowercase();
let exploration_patterns = [
"explore",
"codebase structure",
"codebase architecture",
"codebase overview",
"source files contents",
"read every",
"full contents",
"entire codebase",
"architecture and structure",
"call graph",
"call path",
"call chain",
"symbol relat",
"symbol lookup",
"who calls",
"callers of",
"callees of",
];
exploration_patterns.iter().any(|pat| lower.contains(pat))
}
pub fn hook_kiro_pre_tool_use() -> i32 {
let event = read_stdin_to_string();
if let Some(reason) = evaluate_kiro_pre_tool_use(&event) {
eprintln!("{reason}");
2
} else {
0
}
}
pub fn evaluate_kiro_pre_tool_use(event_json: &str) -> Option<&'static str> {
let parsed: Value = serde_json::from_str(event_json).ok()?;
let tool_name = parsed.get("tool_name").and_then(Value::as_str)?;
if !is_kiro_delegation_tool(tool_name) {
return None;
}
if kiro_event_has_research_text(parsed.get("tool_input").unwrap_or(&Value::Null)) {
Some(TOKENSAVE_RESEARCH_BLOCK_REASON)
} else {
None
}
}
fn is_kiro_delegation_tool(tool_name: &str) -> bool {
matches!(tool_name, "delegate" | "subagent" | "use_subagent")
}
fn kiro_event_has_research_text(value: &Value) -> bool {
let mut text = Vec::new();
collect_kiro_task_strings(value, &mut text);
if text.is_empty() {
collect_strings(value, &mut text);
}
text.iter().any(|s| is_code_research_prompt(s))
}
fn collect_kiro_task_strings<'a>(value: &'a Value, out: &mut Vec<&'a str>) {
match value {
Value::Object(map) => {
for (key, child) in map {
let key = key.to_ascii_lowercase();
if key.contains("prompt")
|| key.contains("task")
|| key.contains("query")
|| key.contains("instruction")
|| key.contains("message")
|| key.contains("description")
{
collect_strings(child, out);
} else {
collect_kiro_task_strings(child, out);
}
}
}
Value::Array(items) => {
for item in items {
collect_kiro_task_strings(item, out);
}
}
Value::String(s) => out.push(s),
_ => {}
}
}
fn collect_strings<'a>(value: &'a Value, out: &mut Vec<&'a str>) {
match value {
Value::String(s) => out.push(s),
Value::Array(items) => {
for item in items {
collect_strings(item, out);
}
}
Value::Object(map) => {
for child in map.values() {
collect_strings(child, out);
}
}
_ => {}
}
}
pub async fn hook_prompt_submit() {
let project_path = crate::config::resolve_path(None);
if let Ok(cg) = crate::tokensave::TokenSave::open(&project_path).await {
let _ = cg.reset_local_counter().await;
}
}
pub async fn hook_kiro_prompt_submit() -> i32 {
let event = read_stdin_to_string();
reset_counter_for_kiro_event(&event).await;
0
}
pub async fn hook_kiro_post_tool_use() -> i32 {
let event = read_stdin_to_string();
match sync_for_kiro_event(&event).await {
Ok(()) => 0,
Err(e) => {
eprintln!("tokensave sync failed: {e}");
1
}
}
}
async fn reset_counter_for_kiro_event(event_json: &str) {
let Some(project_root) = kiro_project_root(event_json) else {
return;
};
if let Ok(cg) = crate::tokensave::TokenSave::open(&project_root).await {
let _ = cg.reset_local_counter().await;
}
}
async fn sync_for_kiro_event(event_json: &str) -> crate::errors::Result<()> {
let Some(project_root) = kiro_project_root(event_json) else {
return Ok(());
};
let cg = crate::tokensave::TokenSave::open(&project_root).await?;
match cg.sync().await {
Ok(_) | Err(crate::errors::TokenSaveError::SyncLock { .. }) => Ok(()),
Err(e) => Err(e),
}
}
fn kiro_project_root(event_json: &str) -> Option<PathBuf> {
let cwd = kiro_event_cwd(event_json).or_else(|| std::env::current_dir().ok())?;
crate::config::discover_project_root(&cwd)
}
fn kiro_event_cwd(event_json: &str) -> Option<PathBuf> {
let parsed: Value = serde_json::from_str(event_json).ok()?;
let cwd = parsed.get("cwd").and_then(Value::as_str)?;
let path = Path::new(cwd);
if path.as_os_str().is_empty() {
None
} else {
Some(path.to_path_buf())
}
}
fn read_stdin_to_string() -> String {
let mut input = String::new();
let _ = std::io::stdin().read_to_string(&mut input);
input
}
pub async fn hook_stop() {
let Some(gdb) = crate::global_db::GlobalDb::open().await else {
return;
};
let stats = crate::accounting::parser::ingest(&gdb).await;
if stats.turns_inserted == 0 {
return;
}
let project_path = crate::config::resolve_path(None);
let tokens_saved = if let Ok(cg) = crate::tokensave::TokenSave::open(&project_path).await {
cg.get_tokens_saved().await.unwrap_or(0)
} else {
0
};
let efficiency = if tokens_saved + stats.tokens_consumed > 0 {
(tokens_saved as f64 / (tokens_saved + stats.tokens_consumed) as f64) * 100.0
} else {
0.0
};
let saved_str = crate::display::format_token_count(tokens_saved);
if stats.cost_usd >= 0.001 {
eprintln!(
"\x1b[36mSession: ${:.2} spent | {saved_str} saved | {efficiency:.0}% efficiency\x1b[0m",
stats.cost_usd
);
}
}