Skip to main content

virtuoso_cli/
history.rs

1use chrono::Utc;
2use serde::{Deserialize, Serialize};
3use std::io::Write;
4use std::path::PathBuf;
5use std::sync::{Mutex, OnceLock};
6
7static CMD_LOCK: OnceLock<Mutex<()>> = OnceLock::new();
8
9fn cmd_lock() -> std::sync::MutexGuard<'static, ()> {
10    CMD_LOCK
11        .get_or_init(|| Mutex::new(()))
12        .lock()
13        .expect("cmd history lock poisoned")
14}
15
16#[derive(Debug, Serialize, Deserialize, Clone)]
17pub struct SkillEntry {
18    pub ts: String,
19    pub skill: String,
20    pub ok: bool,
21    pub output: String,
22}
23
24#[derive(Debug, Serialize, Deserialize, Clone)]
25pub struct CmdEntry {
26    pub ts: String,
27    pub session: Option<String>,
28    pub cmd: Vec<String>,
29    pub exit_code: i32,
30}
31
32pub fn history_dir() -> PathBuf {
33    dirs::home_dir()
34        .unwrap_or_else(|| PathBuf::from("/tmp"))
35        .join(".cache/virtuoso_bridge/history")
36}
37
38fn write_jsonl_line(path: &std::path::Path, line: &str) {
39    if let Ok(mut f) = std::fs::OpenOptions::new().create(true).append(true).open(path) {
40        let _ = writeln!(f, "{line}");
41    }
42}
43
44pub fn append_skill(session_id: &str, skill: &str, ok: bool, output: &str) {
45    let dir = history_dir();
46    let _ = std::fs::create_dir_all(&dir);
47    let entry = SkillEntry {
48        ts: Utc::now().to_rfc3339(),
49        skill: skill.to_string(),
50        ok,
51        output: output.chars().take(512).collect(),
52    };
53    if let Ok(line) = serde_json::to_string(&entry) {
54        write_jsonl_line(&dir.join(format!("{session_id}.jsonl")), &line);
55    }
56}
57
58pub fn append_cmd(args: &[String], session: Option<&str>, exit_code: i32) {
59    let _guard = cmd_lock();
60    let dir = history_dir();
61    let _ = std::fs::create_dir_all(&dir);
62    let entry = CmdEntry {
63        ts: Utc::now().to_rfc3339(),
64        session: session.map(String::from),
65        cmd: args.to_vec(),
66        exit_code,
67    };
68    if let Ok(line) = serde_json::to_string(&entry) {
69        write_jsonl_line(&dir.join("cmd.jsonl"), &line);
70    }
71}
72
73fn tail<T>(mut v: Vec<T>, limit: usize) -> Vec<T> {
74    if limit > 0 && v.len() > limit {
75        v.drain(..v.len() - limit);
76    }
77    v
78}
79
80pub fn load_skill(session_id: &str, limit: usize) -> Vec<SkillEntry> {
81    let path = history_dir().join(format!("{session_id}.jsonl"));
82    let entries: Vec<SkillEntry> = std::fs::read_to_string(path)
83        .unwrap_or_default()
84        .lines()
85        .filter_map(|line| serde_json::from_str(line).ok())
86        .collect();
87    tail(entries, limit)
88}
89
90pub fn load_cmd(session_filter: Option<&str>, limit: usize) -> Vec<CmdEntry> {
91    let _guard = cmd_lock();
92    let path = history_dir().join("cmd.jsonl");
93    let entries: Vec<CmdEntry> = std::fs::read_to_string(path)
94        .unwrap_or_default()
95        .lines()
96        .filter_map(|line| serde_json::from_str(line).ok())
97        .filter(|e: &CmdEntry| {
98            session_filter.map_or(true, |id| e.session.as_deref() == Some(id))
99        })
100        .collect();
101    tail(entries, limit)
102}