use serde_json::{json, Map, Value};
#[derive(Debug, Clone)]
pub struct Event {
pub name: String,
pub properties: Map<String, Value>,
}
impl Event {
fn new(name: &str, properties: Map<String, Value>) -> Self {
Self {
name: name.into(),
properties,
}
}
pub fn cli_command_invoked(
command: &str,
output_mode: &str,
exit_code: i32,
duration_ms: u64,
) -> Self {
let mut p = Map::new();
p.insert("command".into(), json!(command));
p.insert("output_mode".into(), json!(output_mode));
p.insert("exit_code".into(), json!(exit_code));
p.insert("duration_ms".into(), json!(duration_ms));
Self::new("cli_command_invoked", p)
}
pub fn init_completed(
editor_present: bool,
tool_count: u32,
provider_count: u32,
binding_count: u32,
) -> Self {
let mut p = Map::new();
p.insert("editor_present".into(), json!(editor_present));
p.insert("tool_count".into(), json!(tool_count));
p.insert("provider_count".into(), json!(provider_count));
p.insert("binding_count".into(), json!(binding_count));
Self::new("init_completed", p)
}
pub fn login_succeeded(flow: &str, duration_ms: u64) -> Self {
let mut p = Map::new();
p.insert("flow".into(), json!(flow));
p.insert("duration_ms".into(), json!(duration_ms));
Self::new("login_succeeded", p)
}
pub fn login_failed(flow: &str, error_code: &str) -> Self {
let mut p = Map::new();
p.insert("flow".into(), json!(flow));
p.insert("error_code".into(), json!(error_code));
Self::new("login_failed", p)
}
pub fn tool_published(version_bump: &str, dry_run: bool, category_count: u32) -> Self {
let mut p = Map::new();
p.insert("version_bump".into(), json!(version_bump));
p.insert("dry_run".into(), json!(dry_run));
p.insert("category_count".into(), json!(category_count));
Self::new("tool_published", p)
}
pub fn provider_registered(region: &str, binding_count: u32, dry_run: bool) -> Self {
let mut p = Map::new();
p.insert("region".into(), json!(region));
p.insert("binding_count".into(), json!(binding_count));
p.insert("dry_run".into(), json!(dry_run));
Self::new("provider_registered", p)
}
pub fn binding_secret_rotated() -> Self {
Self::new("binding_secret_rotated", Map::new())
}
pub fn listen_started(port: u16, binding_count: u32, tls_mode: &str) -> Self {
let mut p = Map::new();
p.insert("port".into(), json!(port));
p.insert("binding_count".into(), json!(binding_count));
p.insert("tls_mode".into(), json!(tls_mode));
Self::new("listen_started", p)
}
pub fn listen_stopped(
reason: &str,
uptime_ms: u64,
events_processed: u64,
events_failed: u64,
) -> Self {
let mut p = Map::new();
p.insert("reason".into(), json!(reason));
p.insert("uptime_ms".into(), json!(uptime_ms));
p.insert("events_processed".into(), json!(events_processed));
p.insert("events_failed".into(), json!(events_failed));
Self::new("listen_stopped", p)
}
pub fn webhook_verify_failed(binding_id: &str, failure_kind: &str) -> Self {
let mut p = Map::new();
p.insert("binding_id".into(), json!(binding_id));
p.insert("failure_kind".into(), json!(failure_kind));
Self::new("webhook_verify_failed", p)
}
pub fn proxy_call_failed(binding_id: &str, failure_kind: &str) -> Self {
let mut p = Map::new();
p.insert("binding_id".into(), json!(binding_id));
p.insert("failure_kind".into(), json!(failure_kind));
Self::new("proxy_call_failed", p)
}
pub fn error_emitted(code: &str, command_or_path: &str) -> Self {
let mut p = Map::new();
p.insert("code".into(), json!(code));
p.insert("command_or_path".into(), json!(command_or_path));
Self::new("error_emitted", p)
}
pub fn tool_process_started(
binding_id: &str,
language_hint: &str,
restart_count_at_start: u32,
) -> Self {
let mut p = Map::new();
p.insert("binding_id".into(), json!(binding_id));
p.insert("language_hint".into(), json!(language_hint));
p.insert(
"restart_count_at_start".into(),
json!(restart_count_at_start),
);
Self::new("tool_process_started", p)
}
pub fn tool_process_crashed(binding_id: &str, failure_kind: &str, restart_count: u32) -> Self {
let mut p = Map::new();
p.insert("binding_id".into(), json!(binding_id));
p.insert("failure_kind".into(), json!(failure_kind));
p.insert("restart_count".into(), json!(restart_count));
Self::new("tool_process_crashed", p)
}
pub fn tool_process_degraded(
binding_id: &str,
restart_count: u32,
window_seconds: u32,
) -> Self {
let mut p = Map::new();
p.insert("binding_id".into(), json!(binding_id));
p.insert("restart_count".into(), json!(restart_count));
p.insert("window_seconds".into(), json!(window_seconds));
Self::new("tool_process_degraded", p)
}
pub fn manifest_v2_loaded(
tool_count: u32,
binding_count: u32,
provider_count: u32,
glob_count: u32,
) -> Self {
let mut p = Map::new();
p.insert("tool_count".into(), json!(tool_count));
p.insert("binding_count".into(), json!(binding_count));
p.insert("provider_count".into(), json!(provider_count));
p.insert("glob_count".into(), json!(glob_count));
Self::new("manifest_v2_loaded", p)
}
pub fn manifest_v1_deprecated_used(command: &str) -> Self {
let mut p = Map::new();
p.insert("command".into(), json!(command));
Self::new("manifest_v1_deprecated_used", p)
}
pub fn tool_resolve_failed(error_code: &str, ref_kind: &str) -> Self {
let mut p = Map::new();
p.insert("error_code".into(), json!(error_code));
p.insert("ref_kind".into(), json!(ref_kind));
Self::new("tool_resolve_failed", p)
}
pub fn process_override_applied(binding_id: &str, overridden_fields: &[&str]) -> Self {
let mut p = Map::new();
p.insert("binding_id".into(), json!(binding_id));
let mut fields: Vec<&str> = overridden_fields.to_vec();
fields.sort();
p.insert("overridden_fields".into(), json!(fields));
Self::new("process_override_applied", p)
}
pub fn manifest_migrated(tools_count: u32, bindings_count: u32, dry_run: bool) -> Self {
let mut p = Map::new();
p.insert("tools_count".into(), json!(tools_count));
p.insert("bindings_count".into(), json!(bindings_count));
p.insert("dry_run".into(), json!(dry_run));
Self::new("manifest_migrated", p)
}
pub fn create_alias(machine_id: &str, editor_id: &str) -> Self {
let mut p = Map::new();
p.insert("alias".into(), json!(machine_id));
p.insert("distinct_id".into(), json!(editor_id));
Self::new("$create_alias", p)
}
}
pub fn language_hint_for(argv0: &str) -> &'static str {
let lower = argv0
.rsplit(['/', '\\'])
.next()
.unwrap_or(argv0)
.to_ascii_lowercase();
let stem = lower.strip_suffix(".exe").unwrap_or(&lower);
match stem {
"python" | "python3" | "uv" | "uvicorn" | "gunicorn" | "hypercorn" => "python",
"node" | "npm" | "npx" | "pnpm" | "yarn" | "bun" | "deno" => "node",
"cargo" => "rust",
_ => "other",
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cli_command_invoked_carries_required_fields() {
let e = Event::cli_command_invoked("tools.list", "table", 0, 42);
assert_eq!(e.name, "cli_command_invoked");
assert_eq!(e.properties["command"], "tools.list");
assert_eq!(e.properties["output_mode"], "table");
assert_eq!(e.properties["exit_code"], 0);
assert_eq!(e.properties["duration_ms"], 42);
}
#[test]
fn create_alias_event_shape() {
let e = Event::create_alias("mach_x", "edt_y");
assert_eq!(e.name, "$create_alias");
assert_eq!(e.properties["alias"], "mach_x");
assert_eq!(e.properties["distinct_id"], "edt_y");
}
#[test]
fn language_hint_recognises_common_interpreters() {
assert_eq!(language_hint_for("/usr/bin/python3"), "python");
assert_eq!(language_hint_for("uv"), "python");
assert_eq!(language_hint_for("C:\\Windows\\node.exe"), "node");
assert_eq!(language_hint_for("cargo"), "rust");
assert_eq!(language_hint_for("/opt/weirdtool/bin/run"), "other");
}
#[test]
fn tool_process_started_carries_required_fields() {
let e = Event::tool_process_started("bnd_42", "python", 0);
assert_eq!(e.name, "tool_process_started");
assert_eq!(e.properties["binding_id"], "bnd_42");
assert_eq!(e.properties["language_hint"], "python");
assert_eq!(e.properties["restart_count_at_start"], 0);
}
}