aboutsummaryrefslogtreecommitdiffstats
path: root/crates/atuin-client
diff options
context:
space:
mode:
authorEllie Huxtable <ellie@atuin.sh>2026-02-25 19:10:58 -0800
committerGitHub <noreply@github.com>2026-02-25 19:10:58 -0800
commit9fe7d10fcf73570767ba7b4eabaa95f65958821b (patch)
treeed9705225db8e5bbe2f3c689ba5d1c9908b75eb1 /crates/atuin-client
parentfeat: Generate commands or ask questions with `atuin ai` (#3199) (diff)
downloadatuin-9fe7d10fcf73570767ba7b4eabaa95f65958821b.zip
feat: Add history author/intent metadata and v1 record version (#3205)
<!-- Thank you for making a PR! Bug fixes are always welcome, but if you're adding a new feature or changing an existing one, we'd really appreciate if you open an issue, post on the forum, or drop in on Discord --> ## Checks - [x] I am happy for maintainers to push small adjustments to this PR, to speed up the review cycle - [x] I have checked that there are no existing pull requests for the same thing Adds `author` and `intent` to client history records and DB persistence, including migration/backfill and CLI/daemon propagation. Introduces V2 record-store history version `v1` while retaining read compatibility for legacy `v0` records. Adds `--author` and `--intent` flags to `atuin history start`, plus `{author}` and `{intent}` format keys for listing/history output. Updates shell-integration docs for `ATUIN_HISTORY_AUTHOR` and `ATUIN_HISTORY_INTENT`, and updates related tests/fixtures. Validated with `cargo test -p atuin-client --lib`, `cargo test -p atuin-daemon --tests`, `cargo test -p atuin search::inspector`, and `cargo fmt --check`.
Diffstat (limited to 'crates/atuin-client')
-rw-r--r--crates/atuin-client/migrations/20260224000100_history_author_intent.sql2
-rw-r--r--crates/atuin-client/src/database.rs23
-rw-r--r--crates/atuin-client/src/encryption.rs10
-rw-r--r--crates/atuin-client/src/history.rs237
-rw-r--r--crates/atuin-client/src/history/builder.rs22
-rw-r--r--crates/atuin-client/src/history/store.rs21
6 files changed, 265 insertions, 50 deletions
diff --git a/crates/atuin-client/migrations/20260224000100_history_author_intent.sql b/crates/atuin-client/migrations/20260224000100_history_author_intent.sql
new file mode 100644
index 00000000..2bed17e9
--- /dev/null
+++ b/crates/atuin-client/migrations/20260224000100_history_author_intent.sql
@@ -0,0 +1,2 @@
+alter table history add column author text;
+alter table history add column intent text;
diff --git a/crates/atuin-client/src/database.rs b/crates/atuin-client/src/database.rs
index 7aa095f7..5f292bec 100644
--- a/crates/atuin-client/src/database.rs
+++ b/crates/atuin-client/src/database.rs
@@ -200,8 +200,8 @@ impl Sqlite {
async fn save_raw(tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>, h: &History) -> Result<()> {
sqlx::query(
- "insert or ignore into history(id, timestamp, duration, exit, command, cwd, session, hostname, deleted_at)
- values(?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)",
+ "insert or ignore into history(id, timestamp, duration, exit, command, cwd, session, hostname, author, intent, deleted_at)
+ values(?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11)",
)
.bind(h.id.0.as_str())
.bind(h.timestamp.unix_timestamp_nanos() as i64)
@@ -211,6 +211,8 @@ impl Sqlite {
.bind(h.cwd.as_str())
.bind(h.session.as_str())
.bind(h.hostname.as_str())
+ .bind(h.author.as_str())
+ .bind(h.intent.as_deref())
.bind(h.deleted_at.map(|t|t.unix_timestamp_nanos() as i64))
.execute(&mut **tx)
.await?;
@@ -232,6 +234,13 @@ impl Sqlite {
fn query_history(row: SqliteRow) -> History {
let deleted_at: Option<i64> = row.get("deleted_at");
+ let hostname: String = row.get("hostname");
+ let author: Option<String> = row.try_get("author").ok().flatten();
+ let author = author
+ .filter(|author| !author.trim().is_empty())
+ .unwrap_or_else(|| History::author_from_hostname(hostname.as_str()));
+ let intent: Option<String> = row.try_get("intent").ok().flatten();
+ let intent = intent.filter(|intent| !intent.trim().is_empty());
History::from_db()
.id(row.get("id"))
@@ -244,7 +253,9 @@ impl Sqlite {
.command(row.get("command"))
.cwd(row.get("cwd"))
.session(row.get("session"))
- .hostname(row.get("hostname"))
+ .hostname(hostname)
+ .author(author)
+ .intent(intent)
.deleted_at(
deleted_at.and_then(|t| OffsetDateTime::from_unix_timestamp_nanos(t as i128).ok()),
)
@@ -295,7 +306,7 @@ impl Database for Sqlite {
sqlx::query(
"update history
- set timestamp = ?2, duration = ?3, exit = ?4, command = ?5, cwd = ?6, session = ?7, hostname = ?8, deleted_at = ?9
+ set timestamp = ?2, duration = ?3, exit = ?4, command = ?5, cwd = ?6, session = ?7, hostname = ?8, author = ?9, intent = ?10, deleted_at = ?11
where id = ?1",
)
.bind(h.id.0.as_str())
@@ -306,6 +317,8 @@ impl Database for Sqlite {
.bind(h.cwd.as_str())
.bind(h.session.as_str())
.bind(h.hostname.as_str())
+ .bind(h.author.as_str())
+ .bind(h.intent.as_deref())
.bind(h.deleted_at.map(|t|t.unix_timestamp_nanos() as i64))
.execute(&self.pool)
.await?;
@@ -612,6 +625,8 @@ impl Database for Sqlite {
"exit",
"command",
"deleted_at",
+ "null as author",
+ "null as intent",
"group_concat(cwd, ':') as cwd",
"group_concat(session) as session",
"group_concat(hostname, ',') as hostname",
diff --git a/crates/atuin-client/src/encryption.rs b/crates/atuin-client/src/encryption.rs
index a56dbd09..f2032482 100644
--- a/crates/atuin-client/src/encryption.rs
+++ b/crates/atuin-client/src/encryption.rs
@@ -253,6 +253,8 @@ fn decode(bytes: &[u8]) -> Result<History> {
cwd: cwd.to_owned(),
session: session.to_owned(),
hostname: hostname.to_owned(),
+ author: History::author_from_hostname(hostname),
+ intent: None,
deleted_at: deleted_at
.map(|t| OffsetDateTime::parse(t, &Rfc3339))
.transpose()?,
@@ -287,6 +289,8 @@ mod test {
.duration(1)
.session("beep boop".into())
.hostname("booop".into())
+ .author("booop".into())
+ .intent(None)
.deleted_at(None)
.build()
.into();
@@ -331,6 +335,8 @@ mod test {
cwd: "/Users/conrad.ludgate/Documents/code/atuin".to_owned(),
session: "b97d9a306f274473a203d2eba41f9457".to_owned(),
hostname: "fvfg936c0kpf:conrad.ludgate".to_owned(),
+ author: "conrad.ludgate".to_owned(),
+ intent: None,
deleted_at: None,
};
@@ -352,6 +358,8 @@ mod test {
cwd: "/Users/conrad.ludgate/Documents/code/atuin".to_owned(),
session: "b97d9a306f274473a203d2eba41f9457".to_owned(),
hostname: "fvfg936c0kpf:conrad.ludgate".to_owned(),
+ author: "conrad.ludgate".to_owned(),
+ intent: None,
deleted_at: Some(datetime!(2023-05-28 18:35:40.633872 +00:00)),
};
@@ -383,6 +391,8 @@ mod test {
cwd: "/Users/conrad.ludgate/Documents/code/atuin".to_owned(),
session: "b97d9a306f274473a203d2eba41f9457".to_owned(),
hostname: "fvfg936c0kpf:conrad.ludgate".to_owned(),
+ author: "conrad.ludgate".to_owned(),
+ intent: None,
deleted_at: None,
};
diff --git a/crates/atuin-client/src/history.rs b/crates/atuin-client/src/history.rs
index d9d5a203..a5adc233 100644
--- a/crates/atuin-client/src/history.rs
+++ b/crates/atuin-client/src/history.rs
@@ -1,4 +1,5 @@
use core::fmt::Formatter;
+use rmp::decode::DecodeStringError;
use rmp::decode::ValueReadError;
use rmp::{Marker, decode::Bytes};
use std::env;
@@ -17,8 +18,14 @@ use time::OffsetDateTime;
mod builder;
pub mod store;
-const HISTORY_VERSION: &str = "v0";
+pub(crate) const HISTORY_VERSION_V0: &str = "v0";
+pub(crate) const HISTORY_VERSION_V1: &str = "v1";
+const HISTORY_RECORD_VERSION_V0: u16 = 0;
+const HISTORY_RECORD_VERSION_V1: u16 = 1;
+pub(crate) const HISTORY_VERSION: &str = HISTORY_VERSION_V1;
pub const HISTORY_TAG: &str = "history";
+const HISTORY_AUTHOR_ENV: &str = "ATUIN_HISTORY_AUTHOR";
+const HISTORY_INTENT_ENV: &str = "ATUIN_HISTORY_INTENT";
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
pub struct HistoryId(pub String);
@@ -46,8 +53,8 @@ impl From<String> for HistoryId {
//
// ## Implementation Notes
//
-// New fields must should be added to `encryption::{encode, decode}` in a backwards
-// compatible way. (eg sensible defaults and updating the nfields parameter)
+// New fields must be added to `History::{serialize,deserialize}` in a backwards
+// compatible way (sensible defaults and careful `nfields` handling).
#[derive(Debug, Clone, PartialEq, Eq, sqlx::FromRow)]
pub struct History {
/// A client-generated ID, used to identify the entry when syncing.
@@ -68,6 +75,10 @@ pub struct History {
pub session: String,
/// The hostname of the machine the command was run on.
pub hostname: String,
+ /// Who wrote this command (human user or automation/agent identity).
+ pub author: String,
+ /// Optional rationale for why the command was executed.
+ pub intent: Option<String>,
/// Timestamp, which is set when the entry is deleted, allowing a soft delete.
pub deleted_at: Option<OffsetDateTime>,
}
@@ -93,6 +104,23 @@ pub struct HistoryStats {
}
impl History {
+ pub(crate) fn author_from_hostname(hostname: &str) -> String {
+ hostname
+ .split_once(':')
+ .map_or_else(|| hostname.to_owned(), |(_, user)| user.to_owned())
+ }
+
+ fn normalize_optional_field(field: Option<String>) -> Option<String> {
+ field.and_then(|value| {
+ let trimmed = value.trim();
+ if trimmed.is_empty() {
+ None
+ } else {
+ Some(trimmed.to_owned())
+ }
+ })
+ }
+
#[allow(clippy::too_many_arguments)]
fn new(
timestamp: OffsetDateTime,
@@ -102,12 +130,19 @@ impl History {
duration: i64,
session: Option<String>,
hostname: Option<String>,
+ author: Option<String>,
+ intent: Option<String>,
deleted_at: Option<OffsetDateTime>,
) -> Self {
let session = session
.or_else(|| env::var("ATUIN_SESSION").ok())
.unwrap_or_else(|| uuid_v7().as_simple().to_string());
let hostname = hostname.unwrap_or_else(get_host_user);
+ let author = Self::normalize_optional_field(author)
+ .or_else(|| Self::normalize_optional_field(env::var(HISTORY_AUTHOR_ENV).ok()))
+ .unwrap_or_else(|| Self::author_from_hostname(hostname.as_str()));
+ let intent = Self::normalize_optional_field(intent)
+ .or_else(|| Self::normalize_optional_field(env::var(HISTORY_INTENT_ENV).ok()));
Self {
id: uuid_v7().as_simple().to_string().into(),
@@ -118,6 +153,8 @@ impl History {
duration,
session,
hostname,
+ author,
+ intent,
deleted_at,
}
}
@@ -131,9 +168,9 @@ impl History {
let mut output = vec![];
// write the version
- encode::write_u16(&mut output, 0)?;
- // INFO: ensure this is updated when adding new fields
- encode::write_array_len(&mut output, 9)?;
+ encode::write_u16(&mut output, HISTORY_RECORD_VERSION_V1)?;
+ let include_intent = self.intent.is_some();
+ encode::write_array_len(&mut output, 10 + u32::from(include_intent))?;
encode::write_str(&mut output, &self.id.0)?;
encode::write_u64(&mut output, self.timestamp.unix_timestamp_nanos() as u64)?;
@@ -149,9 +186,33 @@ impl History {
None => encode::write_nil(&mut output)?,
}
+ encode::write_str(&mut output, self.author.as_str())?;
+ if let Some(intent) = &self.intent {
+ encode::write_str(&mut output, intent.as_str())?;
+ }
+
Ok(DecryptedData(output))
}
+ fn read_optional_string(bytes: &[u8]) -> Result<(Option<String>, &[u8])> {
+ use rmp::decode;
+
+ fn error_report<E: std::fmt::Debug>(err: E) -> eyre::Report {
+ eyre!("{err:?}")
+ }
+
+ match decode::read_str_from_slice(bytes) {
+ Ok((value, bytes)) => Ok((Some(value.to_owned()), bytes)),
+ Err(DecodeStringError::TypeMismatch(Marker::Null)) => {
+ let mut cursor = Bytes::new(bytes);
+ decode::read_nil(&mut cursor).map_err(error_report)?;
+
+ Ok((None, cursor.remaining_slice()))
+ }
+ Err(err) => Err(error_report(err)),
+ }
+ }
+
fn deserialize_v0(bytes: &[u8]) -> Result<History> {
use rmp::decode;
@@ -163,7 +224,7 @@ impl History {
let version = decode::read_u16(&mut bytes).map_err(error_report)?;
- if version != 0 {
+ if version != HISTORY_RECORD_VERSION_V0 {
bail!("expected decoding v0 record, found v{version}");
}
@@ -187,7 +248,6 @@ impl History {
let (session, bytes) = decode::read_str_from_slice(bytes).map_err(error_report)?;
let (hostname, bytes) = decode::read_str_from_slice(bytes).map_err(error_report)?;
- // if we have more fields, try and get the deleted_at
let mut bytes = Bytes::new(bytes);
let (deleted_at, bytes) = match decode::read_u64(&mut bytes) {
@@ -196,6 +256,76 @@ impl History {
Err(ValueReadError::TypeMismatch(Marker::Null)) => (None, bytes.remaining_slice()),
Err(err) => return Err(error_report(err)),
};
+ if !bytes.is_empty() {
+ bail!("trailing bytes in encoded history. malformed")
+ }
+
+ Ok(History {
+ id: id.to_owned().into(),
+ timestamp: OffsetDateTime::from_unix_timestamp_nanos(timestamp as i128)?,
+ duration,
+ exit,
+ command: command.to_owned(),
+ cwd: cwd.to_owned(),
+ session: session.to_owned(),
+ hostname: hostname.to_owned(),
+ author: Self::author_from_hostname(hostname),
+ intent: None,
+ deleted_at: deleted_at
+ .map(|t| OffsetDateTime::from_unix_timestamp_nanos(t as i128))
+ .transpose()?,
+ })
+ }
+
+ fn deserialize_v1(bytes: &[u8]) -> Result<History> {
+ use rmp::decode;
+
+ fn error_report<E: std::fmt::Debug>(err: E) -> eyre::Report {
+ eyre!("{err:?}")
+ }
+
+ let mut bytes = Bytes::new(bytes);
+
+ let version = decode::read_u16(&mut bytes).map_err(error_report)?;
+
+ if version != HISTORY_RECORD_VERSION_V1 {
+ bail!("expected decoding v1 record, found v{version}");
+ }
+
+ let nfields = decode::read_array_len(&mut bytes).map_err(error_report)?;
+
+ if !(10..=11).contains(&nfields) {
+ bail!("cannot decrypt history from a different version of Atuin");
+ }
+
+ let bytes = bytes.remaining_slice();
+ let (id, bytes) = decode::read_str_from_slice(bytes).map_err(error_report)?;
+
+ let mut bytes = Bytes::new(bytes);
+ let timestamp = decode::read_u64(&mut bytes).map_err(error_report)?;
+ let duration = decode::read_int(&mut bytes).map_err(error_report)?;
+ let exit = decode::read_int(&mut bytes).map_err(error_report)?;
+
+ let bytes = bytes.remaining_slice();
+ let (command, bytes) = decode::read_str_from_slice(bytes).map_err(error_report)?;
+ let (cwd, bytes) = decode::read_str_from_slice(bytes).map_err(error_report)?;
+ let (session, bytes) = decode::read_str_from_slice(bytes).map_err(error_report)?;
+ let (hostname, bytes) = decode::read_str_from_slice(bytes).map_err(error_report)?;
+
+ let mut bytes = Bytes::new(bytes);
+
+ let (deleted_at, bytes) = match decode::read_u64(&mut bytes) {
+ Ok(unix) => (Some(unix), bytes.remaining_slice()),
+ // we accept null here
+ Err(ValueReadError::TypeMismatch(Marker::Null)) => (None, bytes.remaining_slice()),
+ Err(err) => return Err(error_report(err)),
+ };
+ let (author, bytes) = Self::read_optional_string(bytes)?;
+ let (intent, bytes) = if nfields > 10 {
+ Self::read_optional_string(bytes)?
+ } else {
+ (None, bytes)
+ };
if !bytes.is_empty() {
bail!("trailing bytes in encoded history. malformed")
@@ -210,6 +340,8 @@ impl History {
cwd: cwd.to_owned(),
session: session.to_owned(),
hostname: hostname.to_owned(),
+ author: author.unwrap_or_else(|| Self::author_from_hostname(hostname)),
+ intent,
deleted_at: deleted_at
.map(|t| OffsetDateTime::from_unix_timestamp_nanos(t as i128))
.transpose()?,
@@ -218,7 +350,8 @@ impl History {
pub fn deserialize(bytes: &[u8], version: &str) -> Result<History> {
match version {
- HISTORY_VERSION => Self::deserialize_v0(bytes),
+ HISTORY_VERSION_V0 => Self::deserialize_v0(bytes),
+ HISTORY_VERSION_V1 => Self::deserialize_v1(bytes),
_ => bail!("unknown version {version:?}"),
}
@@ -361,6 +494,8 @@ impl History {
/// .duration(100)
/// .session("somesession".to_string())
/// .hostname("localhost".to_string())
+ /// .author("user".to_string())
+ /// .intent(None)
/// .deleted_at(None)
/// .build()
/// .into();
@@ -469,18 +604,6 @@ mod tests {
#[test]
fn test_serialize_deserialize() {
- let bytes = [
- 205, 0, 0, 153, 217, 32, 54, 54, 100, 49, 54, 99, 98, 101, 101, 55, 99, 100, 52, 55,
- 53, 51, 56, 101, 53, 99, 53, 98, 56, 98, 52, 52, 101, 57, 48, 48, 54, 101, 207, 23, 99,
- 98, 117, 24, 210, 246, 128, 206, 2, 238, 210, 240, 0, 170, 103, 105, 116, 32, 115, 116,
- 97, 116, 117, 115, 217, 42, 47, 85, 115, 101, 114, 115, 47, 99, 111, 110, 114, 97, 100,
- 46, 108, 117, 100, 103, 97, 116, 101, 47, 68, 111, 99, 117, 109, 101, 110, 116, 115,
- 47, 99, 111, 100, 101, 47, 97, 116, 117, 105, 110, 217, 32, 98, 57, 55, 100, 57, 97,
- 51, 48, 54, 102, 50, 55, 52, 52, 55, 51, 97, 50, 48, 51, 100, 50, 101, 98, 97, 52, 49,
- 102, 57, 52, 53, 55, 187, 102, 118, 102, 103, 57, 51, 54, 99, 48, 107, 112, 102, 58,
- 99, 111, 110, 114, 97, 100, 46, 108, 117, 100, 103, 97, 116, 101, 192,
- ];
-
let history = History {
id: "66d16cbee7cd47538e5c5b8b44e9006e".to_owned().into(),
timestamp: datetime!(2023-05-28 18:35:40.633872 +00:00),
@@ -490,20 +613,21 @@ mod tests {
cwd: "/Users/conrad.ludgate/Documents/code/atuin".to_owned(),
session: "b97d9a306f274473a203d2eba41f9457".to_owned(),
hostname: "fvfg936c0kpf:conrad.ludgate".to_owned(),
+ author: "conrad.ludgate".to_owned(),
+ intent: None,
deleted_at: None,
};
let serialized = history.serialize().expect("failed to serialize history");
- assert_eq!(serialized.0, bytes);
+ assert_eq!(
+ &serialized.0[0..3],
+ [205, 0, 1],
+ "should encode as history v1"
+ );
let deserialized = History::deserialize(&serialized.0, HISTORY_VERSION)
.expect("failed to deserialize history");
assert_eq!(history, deserialized);
-
- // test the snapshot too
- let deserialized =
- History::deserialize(&bytes, HISTORY_VERSION).expect("failed to deserialize history");
- assert_eq!(history, deserialized);
}
#[test]
@@ -517,6 +641,8 @@ mod tests {
cwd: "/Users/conrad.ludgate/Documents/code/atuin".to_owned(),
session: "b97d9a306f274473a203d2eba41f9457".to_owned(),
hostname: "fvfg936c0kpf:conrad.ludgate".to_owned(),
+ author: "conrad.ludgate".to_owned(),
+ intent: None,
deleted_at: Some(datetime!(2023-11-19 20:18 +00:00)),
};
@@ -529,6 +655,29 @@ mod tests {
}
#[test]
+ fn test_serialize_deserialize_with_author_and_intent() {
+ let history = History {
+ id: "66d16cbee7cd47538e5c5b8b44e9006e".to_owned().into(),
+ timestamp: datetime!(2023-05-28 18:35:40.633872 +00:00),
+ duration: 49206000,
+ exit: 0,
+ command: "git status".to_owned(),
+ cwd: "/Users/conrad.ludgate/Documents/code/atuin".to_owned(),
+ session: "b97d9a306f274473a203d2eba41f9457".to_owned(),
+ hostname: "fvfg936c0kpf:conrad.ludgate".to_owned(),
+ author: "claude".to_owned(),
+ intent: Some("check repository status".to_owned()),
+ deleted_at: None,
+ };
+
+ let serialized = history.serialize().expect("failed to serialize history");
+ let deserialized = History::deserialize(&serialized.0, HISTORY_VERSION)
+ .expect("failed to deserialize history");
+
+ assert_eq!(history, deserialized);
+ }
+
+ #[test]
fn test_serialize_deserialize_version() {
// v0
let bytes_v0 = [
@@ -543,23 +692,31 @@ mod tests {
99, 111, 110, 114, 97, 100, 46, 108, 117, 100, 103, 97, 116, 101, 192,
];
- // some other version
- let bytes_v1 = [
- 205, 1, 0, 153, 217, 32, 54, 54, 100, 49, 54, 99, 98, 101, 101, 55, 99, 100, 52, 55,
- 53, 51, 56, 101, 53, 99, 53, 98, 56, 98, 52, 52, 101, 57, 48, 48, 54, 101, 207, 23, 99,
- 98, 117, 24, 210, 246, 128, 206, 2, 238, 210, 240, 0, 170, 103, 105, 116, 32, 115, 116,
- 97, 116, 117, 115, 217, 42, 47, 85, 115, 101, 114, 115, 47, 99, 111, 110, 114, 97, 100,
- 46, 108, 117, 100, 103, 97, 116, 101, 47, 68, 111, 99, 117, 109, 101, 110, 116, 115,
- 47, 99, 111, 100, 101, 47, 97, 116, 117, 105, 110, 217, 32, 98, 57, 55, 100, 57, 97,
- 51, 48, 54, 102, 50, 55, 52, 52, 55, 51, 97, 50, 48, 51, 100, 50, 101, 98, 97, 52, 49,
- 102, 57, 52, 53, 55, 187, 102, 118, 102, 103, 57, 51, 54, 99, 48, 107, 112, 102, 58,
- 99, 111, 110, 114, 97, 100, 46, 108, 117, 100, 103, 97, 116, 101, 192,
- ];
+ let deserialized = History::deserialize(&bytes_v0, "v0");
+ assert!(deserialized.is_ok());
let deserialized = History::deserialize(&bytes_v0, HISTORY_VERSION);
+ assert!(deserialized.is_err());
+
+ let current = History {
+ id: "66d16cbee7cd47538e5c5b8b44e9006e".to_owned().into(),
+ timestamp: datetime!(2023-05-28 18:35:40.633872 +00:00),
+ duration: 49206000,
+ exit: 0,
+ command: "git status".to_owned(),
+ cwd: "/Users/conrad.ludgate/Documents/code/atuin".to_owned(),
+ session: "b97d9a306f274473a203d2eba41f9457".to_owned(),
+ hostname: "fvfg936c0kpf:conrad.ludgate".to_owned(),
+ author: "conrad.ludgate".to_owned(),
+ intent: None,
+ deleted_at: None,
+ };
+
+ let bytes_v1 = current.serialize().expect("failed to serialize history");
+ let deserialized = History::deserialize(&bytes_v1.0, HISTORY_VERSION);
assert!(deserialized.is_ok());
- let deserialized = History::deserialize(&bytes_v1, HISTORY_VERSION);
+ let deserialized = History::deserialize(&bytes_v1.0, "v0");
assert!(deserialized.is_err());
}
}
diff --git a/crates/atuin-client/src/history/builder.rs b/crates/atuin-client/src/history/builder.rs
index 2b28339f..72a505fd 100644
--- a/crates/atuin-client/src/history/builder.rs
+++ b/crates/atuin-client/src/history/builder.rs
@@ -20,6 +20,10 @@ pub struct HistoryImported {
session: Option<String>,
#[builder(default, setter(strip_option, into))]
hostname: Option<String>,
+ #[builder(default, setter(strip_option, into))]
+ author: Option<String>,
+ #[builder(default, setter(strip_option, into))]
+ intent: Option<String>,
}
impl From<HistoryImported> for History {
@@ -32,6 +36,8 @@ impl From<HistoryImported> for History {
imported.duration,
imported.session,
imported.hostname,
+ imported.author,
+ imported.intent,
None,
)
}
@@ -49,6 +55,10 @@ pub struct HistoryCaptured {
command: String,
#[builder(setter(into))]
cwd: String,
+ #[builder(default, setter(strip_option, into))]
+ author: Option<String>,
+ #[builder(default, setter(strip_option, into))]
+ intent: Option<String>,
}
impl From<HistoryCaptured> for History {
@@ -61,6 +71,8 @@ impl From<HistoryCaptured> for History {
-1,
None,
None,
+ captured.author,
+ captured.intent,
None,
)
}
@@ -79,6 +91,8 @@ pub struct HistoryFromDb {
duration: i64,
session: String,
hostname: String,
+ author: String,
+ intent: Option<String>,
deleted_at: Option<time::OffsetDateTime>,
}
@@ -93,6 +107,8 @@ impl From<HistoryFromDb> for History {
duration: from_db.duration,
session: from_db.session,
hostname: from_db.hostname,
+ author: from_db.author,
+ intent: from_db.intent,
deleted_at: from_db.deleted_at,
}
}
@@ -114,6 +130,10 @@ pub struct HistoryDaemonCapture {
session: String,
#[builder(setter(into))]
hostname: String,
+ #[builder(default, setter(strip_option, into))]
+ author: Option<String>,
+ #[builder(default, setter(strip_option, into))]
+ intent: Option<String>,
}
impl From<HistoryDaemonCapture> for History {
@@ -126,6 +146,8 @@ impl From<HistoryDaemonCapture> for History {
-1,
Some(captured.session),
Some(captured.hostname),
+ captured.author,
+ captured.intent,
None,
)
}
diff --git a/crates/atuin-client/src/history/store.rs b/crates/atuin-client/src/history/store.rs
index 041d90ce..d166564f 100644
--- a/crates/atuin-client/src/history/store.rs
+++ b/crates/atuin-client/src/history/store.rs
@@ -10,7 +10,7 @@ use crate::{
};
use atuin_common::record::{DecryptedData, Host, HostId, Record, RecordId, RecordIdx};
-use super::{HISTORY_TAG, HISTORY_VERSION, History, HistoryId};
+use super::{HISTORY_TAG, HISTORY_VERSION, HISTORY_VERSION_V0, History, HistoryId};
#[derive(Debug, Clone)]
pub struct HistoryStore {
@@ -196,10 +196,11 @@ impl HistoryStore {
for record in records.into_iter() {
let hist = match record.version.as_str() {
- HISTORY_VERSION => {
+ HISTORY_VERSION_V0 | HISTORY_VERSION => {
+ let version = record.version.clone();
let decrypted = record.decrypt::<PASETO_V4>(&self.encryption_key)?;
- HistoryRecord::deserialize(&decrypted.data, HISTORY_VERSION)
+ HistoryRecord::deserialize(&decrypted.data, version.as_str())
}
version => bail!("unknown history version {version:?}"),
}?;
@@ -257,8 +258,14 @@ impl HistoryStore {
continue;
}
+ let version = record.version.clone();
let decrypted = record.decrypt::<PASETO_V4>(&self.encryption_key)?;
- let record = HistoryRecord::deserialize(&decrypted.data, HISTORY_VERSION)?;
+ let record = match version.as_str() {
+ HISTORY_VERSION_V0 | HISTORY_VERSION => {
+ HistoryRecord::deserialize(&decrypted.data, version.as_str())?
+ }
+ version => bail!("unknown history version {version:?}"),
+ };
match record {
HistoryRecord::Create(h) => {
@@ -350,14 +357,14 @@ mod tests {
#[test]
fn test_serialize_deserialize_create() {
let bytes = [
- 204, 0, 196, 141, 205, 0, 0, 153, 217, 32, 48, 49, 56, 99, 100, 52, 102, 101, 56, 49,
+ 204, 0, 196, 147, 205, 0, 1, 154, 217, 32, 48, 49, 56, 99, 100, 52, 102, 101, 56, 49,
55, 53, 55, 99, 100, 50, 97, 101, 101, 54, 53, 99, 100, 55, 56, 54, 49, 102, 57, 99,
56, 49, 207, 23, 166, 251, 212, 181, 82, 0, 0, 100, 0, 162, 108, 115, 217, 41, 47, 85,
115, 101, 114, 115, 47, 101, 108, 108, 105, 101, 47, 115, 114, 99, 47, 103, 105, 116,
104, 117, 98, 46, 99, 111, 109, 47, 97, 116, 117, 105, 110, 115, 104, 47, 97, 116, 117,
105, 110, 217, 32, 48, 49, 56, 99, 100, 52, 102, 101, 97, 100, 56, 57, 55, 53, 57, 55,
56, 53, 50, 53, 50, 55, 97, 51, 49, 99, 57, 57, 56, 48, 53, 57, 170, 98, 111, 111, 112,
- 58, 101, 108, 108, 105, 101, 192,
+ 58, 101, 108, 108, 105, 101, 192, 165, 101, 108, 108, 105, 101,
];
let history = History {
@@ -369,6 +376,8 @@ mod tests {
cwd: "/Users/ellie/src/github.com/atuinsh/atuin".to_owned(),
session: "018cd4fead897597852527a31c998059".to_owned(),
hostname: "boop:ellie".to_owned(),
+ author: "ellie".to_owned(),
+ intent: None,
deleted_at: None,
};