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