use rusqlite::Connection;
use serde_json::json;
use crate::contexts::activity_log;
use crate::schema::Issue;
pub const ACTOR_ENV: &str = "CLIBAN_ACTOR";
pub const SESSION_ENV: &str = "CLAUDE_CODE_SESSION_ID";
pub fn actor() -> Option<String> {
let explicit = std::env::var(ACTOR_ENV)
.ok()
.map(|v| v.trim().to_string())
.filter(|v| !v.is_empty());
explicit.or_else(|| {
std::env::var(SESSION_ENV)
.ok()
.map(|v| v.trim().to_string())
.filter(|v| !v.is_empty())
.map(|id| format!("session:{}", id.chars().take(8).collect::<String>()))
})
}
pub fn record(conn: &Connection, issue: &Issue, kind: &str, message: &str) {
let extra = match actor() {
Some(a) => json!({ "actor": a }),
None => json!({}),
};
let _ = activity_log::append(conn, issue, kind, message, &extra);
}
pub fn record_move(conn: &Connection, issue: &Issue, from: &str, to: &str, note: Option<&str>) {
let mut message = format!("{from} → {to}");
if let Some(note) = note.map(str::trim).filter(|n| !n.is_empty()) {
message.push_str(": ");
message.push_str(note);
}
record(conn, issue, "status", &message);
}
#[derive(Default)]
pub struct EditSummary {
parts: Vec<String>,
}
impl EditSummary {
pub fn field(&mut self, name: &str, before: &str, after: &str) {
if before == after {
return;
}
self.parts.push(format!(
"{name}: {} → {}",
dash_if_blank(before),
dash_if_blank(after)
));
}
pub fn note(&mut self, text: impl Into<String>) {
self.parts.push(text.into());
}
pub fn is_empty(&self) -> bool {
self.parts.is_empty()
}
pub fn message(&self) -> String {
self.parts.join(", ")
}
}
fn dash_if_blank(s: &str) -> &str {
if s.is_empty() {
"-"
} else {
s
}
}
pub fn log_dedupe_key(ts: chrono::DateTime<chrono::Utc>, message: &str) -> (i64, String) {
(ts.timestamp() / 60, message.trim().to_string())
}
pub fn actor_of(extra: &str) -> Option<String> {
serde_json::from_str::<serde_json::Value>(extra)
.ok()?
.get("actor")?
.as_str()
.map(str::to_string)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn actor_round_trips_through_the_extra_blob() {
assert_eq!(actor_of(r#"{"actor":"claude"}"#).as_deref(), Some("claude"));
assert_eq!(actor_of("{}"), None);
assert_eq!(actor_of("not json"), None);
assert_eq!(actor_of(r#"{"actor":7}"#), None);
}
}