//! Environment variable helpers and process isolation for terminal execution. //! //! The implementations live in the lightweight [`kigi_tty_utils`] crate so //! every workspace crate can use them without depending on `kigi-tools`; this //! module re-exports the public API. pub use kigi_tty_utils::{detach_from_tty, pager_env}; /// Parse a positive whole number in plain digits (no sign, no separators, /// no scientific notation). `None` for zero or anything non-canonical. pub fn parse_positive(value: &str) -> Option { value.parse::().ok().filter(|&parsed| parsed > 0) } /// Parse a positive whole-number env value into a count. A set-but-invalid /// value warns and reads as unset, so the caller's default applies. pub fn parse_positive_env(var: &str, value: Option) -> Option { let value = value?; let parsed = parse_positive(&value).and_then(|parsed| usize::try_from(parsed).ok()); if parsed.is_none() { tracing::warn!( var, %value, "env value is not a positive whole number in plain digits; using the default" ); } parsed } /// Env var set on agent-spawned terminal processes so host tools (e.g. `x ban`) /// can distinguish agent invocations from human interactive shells. /// Note: the CLI also uses `KIGI_AGENT` as an /// optional agent-definition selector for launching `kigi` itself; child terminal /// processes only need the sentinel value `"1"`. pub const KIGI_AGENT_ENV: &str = "KIGI_AGENT"; /// Sentinel value for [`KIGI_AGENT_ENV`] on agent tool terminals. pub const KIGI_AGENT_ENV_VALUE: &str = "1"; /// Force `KIGI_AGENT=1` on an agent terminal child so request/login env cannot /// clear the agent marker. pub fn apply_kigi_agent_marker(cmd: &mut tokio::process::Command) { cmd.env(KIGI_AGENT_ENV, KIGI_AGENT_ENV_VALUE); } /// Expand the four plugin-path tokens (`${CLAUDE_PLUGIN_ROOT}` / `${KIGI_PLUGIN_ROOT}` /// and `${CLAUDE_PLUGIN_DATA}` / `${KIGI_PLUGIN_DATA}`) in `s`. Each pair is expanded /// only when its value is provided. Single source of truth for plugin agent bodies, /// plugin skill/command bodies, and plugin MCP/hook config substitution. pub fn substitute_plugin_tokens( s: &str, plugin_root: Option<&str>, plugin_data: Option<&str>, ) -> String { let mut out = s.to_string(); if let Some(root) = plugin_root { out = out .replace("${CLAUDE_PLUGIN_ROOT}", root) .replace("${KIGI_PLUGIN_ROOT}", root); } if let Some(data) = plugin_data { out = out .replace("${CLAUDE_PLUGIN_DATA}", data) .replace("${KIGI_PLUGIN_DATA}", data); } out } #[cfg(test)] mod tests { use super::{KIGI_AGENT_ENV, KIGI_AGENT_ENV_VALUE, substitute_plugin_tokens}; const ALL_TOKENS: &str = "${CLAUDE_PLUGIN_ROOT}/a ${KIGI_PLUGIN_ROOT}/b ${CLAUDE_PLUGIN_DATA}/c ${KIGI_PLUGIN_DATA}/d"; #[test] fn expands_all_four_tokens_when_both_provided() { let out = substitute_plugin_tokens(ALL_TOKENS, Some("/root"), Some("/data")); assert_eq!(out, "/root/a /root/b /data/c /data/d"); } #[test] fn leaves_tokens_literal_when_both_none() { let out = substitute_plugin_tokens(ALL_TOKENS, None, None); assert_eq!(out, ALL_TOKENS); } #[test] fn expands_only_root_when_data_none() { let out = substitute_plugin_tokens(ALL_TOKENS, Some("/root"), None); assert_eq!( out, "/root/a /root/b ${CLAUDE_PLUGIN_DATA}/c ${KIGI_PLUGIN_DATA}/d" ); } #[test] fn agent_marker_constants_match_cursor_parity() { assert_eq!(KIGI_AGENT_ENV, "KIGI_AGENT"); assert_eq!(KIGI_AGENT_ENV_VALUE, "1"); } }