hematite/agent/
shell_history.rs1const MAX_COMMANDS: usize = 20;
12const MIN_CMD_LEN: usize = 4;
13
14pub fn load_shell_history_block() -> Option<String> {
15 let commands = read_history()?;
16 if commands.is_empty() {
17 return None;
18 }
19 let mut block = String::from("## Recent Shell History (last session commands)\n");
20 for cmd in &commands {
21 block.push_str(&format!(" $ {}\n", cmd));
22 }
23 block.push_str("Use this context to understand what the user was recently working on.\n");
24 Some(block)
25}
26
27fn read_history() -> Option<Vec<String>> {
28 let path = history_path()?;
29 let raw = std::fs::read_to_string(&path).ok()?;
30
31 let mut seen = LinkedHashSet::new();
32 let mut cmds: Vec<String> = Vec::new();
33
34 for line in raw.lines().rev() {
35 let cmd = line.trim();
36 if cmd.len() < MIN_CMD_LEN {
37 continue;
38 }
39 if matches!(
41 cmd,
42 "ls" | "pwd" | "cd" | "clear" | "exit" | "cls" | "history"
43 ) {
44 continue;
45 }
46 if cmd.starts_with('#') || cmd.starts_with("PS ") {
48 continue;
49 }
50 if seen.contains(cmd) {
51 continue;
52 }
53 seen.insert(cmd.to_string());
54 cmds.push(cmd.to_string());
55 if cmds.len() >= MAX_COMMANDS {
56 break;
57 }
58 }
59
60 cmds.reverse(); if cmds.is_empty() {
62 None
63 } else {
64 Some(cmds)
65 }
66}
67
68fn history_path() -> Option<std::path::PathBuf> {
69 #[cfg(target_os = "windows")]
70 {
71 let appdata = std::env::var("APPDATA").ok()?;
73 let p = std::path::PathBuf::from(appdata)
74 .join("Microsoft")
75 .join("Windows")
76 .join("PowerShell")
77 .join("PSReadLine")
78 .join("ConsoleHost_history.txt");
79 if p.exists() {
80 Some(p)
81 } else {
82 None
83 }
84 }
85 #[cfg(not(target_os = "windows"))]
86 {
87 let home = std::env::var("HOME").ok()?;
88 let zsh = std::path::PathBuf::from(&home).join(".zsh_history");
90 if zsh.exists() {
91 return Some(zsh);
92 }
93 let bash = std::path::PathBuf::from(&home).join(".bash_history");
94 if bash.exists() {
95 return Some(bash);
96 }
97 None
98 }
99}
100
101struct LinkedHashSet(Vec<String>);
103
104impl LinkedHashSet {
105 fn new() -> Self {
106 Self(Vec::new())
107 }
108 fn contains(&self, s: &str) -> bool {
109 self.0.iter().any(|x| x == s)
110 }
111 fn insert(&mut self, s: String) {
112 self.0.push(s);
113 }
114}