journal: add --date flag for backdating entries

Adds an optional date parameter to journal_new that overrides the
timestamp on new entries. Accepts YYYY-MM-DD or YYYY-MM-DDTHH:MM
format. Used for seeding the memory graph with historical journal
entries from existing memory files.

Threading: CLI --date flag → cmd_journal_write → journal_new tool →
local store, with parse_date_to_epoch setting both timestamp and
created_at on the node.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Waffles 2026-05-22 15:38:29 -04:00
commit 37087ac6d9
5 changed files with 34 additions and 8 deletions

View file

@ -194,7 +194,7 @@ memory_tool!(memory_links, ref -> Vec<LinkInfo>, key: [str]);
pub use crate::hippocampus::local::JournalEntry; pub use crate::hippocampus::local::JournalEntry;
memory_tool!(journal_tail, ref -> Vec<JournalEntry>, count: [Option<u64>], level: [Option<u64>], after: [Option<&str>]); memory_tool!(journal_tail, ref -> Vec<JournalEntry>, count: [Option<u64>], level: [Option<u64>], after: [Option<&str>]);
memory_tool!(journal_new, mut, name: [str], title: [str], body: [str], level: [Option<i64>]); memory_tool!(journal_new, mut, name: [str], title: [str], body: [str], level: [Option<i64>], date: [Option<&str>]);
memory_tool!(journal_update, mut, body: [str], level: [Option<i64>]); memory_tool!(journal_update, mut, body: [str], level: [Option<i64>]);
// ── Graph tools ─────────────────────────────────────────────── // ── Graph tools ───────────────────────────────────────────────
@ -363,7 +363,8 @@ pub fn journal_tools() -> [super::Tool; 3] {
"name": {"type": "string"}, "name": {"type": "string"},
"title": {"type": "string"}, "title": {"type": "string"},
"body": {"type": "string"}, "body": {"type": "string"},
"level": {"type": "integer"} "level": {"type": "integer"},
"date": {"type": "string", "description": "Override timestamp (YYYY-MM-DD or YYYY-MM-DDTHH:MM)"}
}, },
"required": ["name", "title", "body"] "required": ["name", "title", "body"]
}"#), }"#),

View file

@ -82,14 +82,14 @@ pub async fn cmd_journal_tail(n: usize, full: bool, level: u8) -> Result<()> {
Ok(()) Ok(())
} }
pub async fn cmd_journal_write(name: &str, text: &[String]) -> Result<()> { pub async fn cmd_journal_write(name: &str, date: Option<&str>, text: &[String]) -> Result<()> {
if text.is_empty() { if text.is_empty() {
bail!("journal write requires text"); bail!("journal write requires text");
} }
super::check_dry_run(); super::check_dry_run();
let body = text.join(" "); let body = text.join(" ");
let result = memory::journal_new(None, name, name, &body, Some(0)).await?; let result = memory::journal_new(None, name, name, &body, Some(0), date).await?;
println!("{}", result); println!("{}", result);
Ok(()) Ok(())
} }

View file

@ -315,9 +315,13 @@ fn level_to_node_type(level: i64) -> crate::store::NodeType {
} }
} }
pub fn journal_new(store: &Store, provenance: &str, name: &str, title: &str, body: &str, level: Option<i64>) -> Result<String> { pub fn journal_new(store: &Store, provenance: &str, name: &str, title: &str, body: &str, level: Option<i64>, date: Option<&str>) -> Result<String> {
let level = level.unwrap_or(0); let level = level.unwrap_or(0);
let ts = chrono::Local::now().format("%Y-%m-%dT%H:%M"); let ts = if let Some(d) = date {
d.to_string()
} else {
chrono::Local::now().format("%Y-%m-%dT%H:%M").to_string()
};
let content = format!("## {}{}\n\n{}", ts, title, body); let content = format!("## {}{}\n\n{}", ts, title, body);
let base_key: String = name.split_whitespace() let base_key: String = name.split_whitespace()
@ -340,6 +344,12 @@ pub fn journal_new(store: &Store, provenance: &str, name: &str, title: &str, bod
base_key.to_string() base_key.to_string()
}; };
let mut node = crate::store::new_node(&key, &content); let mut node = crate::store::new_node(&key, &content);
if let Some(d) = date {
if let Some(epoch) = parse_date_to_epoch(d) {
node.timestamp = epoch;
node.created_at = epoch;
}
}
node.node_type = level_to_node_type(level); node.node_type = level_to_node_type(level);
node.provenance = provenance.to_string(); node.provenance = provenance.to_string();
store.upsert_node(node).map_err(|e| anyhow::anyhow!("{}", e))?; store.upsert_node(node).map_err(|e| anyhow::anyhow!("{}", e))?;
@ -348,6 +358,18 @@ pub fn journal_new(store: &Store, provenance: &str, name: &str, title: &str, bod
Ok(format!("New entry '{}' ({} words)", title, word_count)) Ok(format!("New entry '{}' ({} words)", title, word_count))
} }
fn parse_date_to_epoch(date: &str) -> Option<i64> {
use chrono::NaiveDate;
use chrono::NaiveDateTime;
if let Ok(dt) = NaiveDateTime::parse_from_str(date, "%Y-%m-%dT%H:%M") {
Some(dt.and_local_timezone(chrono::Local).single()?.timestamp())
} else if let Ok(d) = NaiveDate::parse_from_str(date, "%Y-%m-%d") {
Some(d.and_hms_opt(12, 0, 0)?.and_local_timezone(chrono::Local).single()?.timestamp())
} else {
None
}
}
pub fn journal_update(store: &Store, provenance: &str, body: &str, level: Option<i64>) -> Result<String> { pub fn journal_update(store: &Store, provenance: &str, body: &str, level: Option<i64>) -> Result<String> {
let level = level.unwrap_or(0); let level = level.unwrap_or(0);
let node_type = level_to_node_type(level); let node_type = level_to_node_type(level);

View file

@ -309,7 +309,7 @@ memory_tool!(memory_links, ref -> Vec<LinkInfo>, key: [str]);
// ── Journal tools ────────────────────────────────────────────── // ── Journal tools ──────────────────────────────────────────────
memory_tool!(journal_tail, ref -> Vec<JournalEntry>, count: [Option<u64>], level: [Option<u64>], after: [Option<&str>]); memory_tool!(journal_tail, ref -> Vec<JournalEntry>, count: [Option<u64>], level: [Option<u64>], after: [Option<&str>]);
memory_tool!(journal_new, mut, name: [str], title: [str], body: [str], level: [Option<i64>]); memory_tool!(journal_new, mut, name: [str], title: [str], body: [str], level: [Option<i64>], date: [Option<&str>]);
memory_tool!(journal_update, mut, body: [str], level: [Option<i64>]); memory_tool!(journal_update, mut, body: [str], level: [Option<i64>]);
// ── Graph tools ─────────────────────────────────────────────── // ── Graph tools ───────────────────────────────────────────────

View file

@ -195,6 +195,9 @@ enum JournalCmd {
Write { Write {
/// Entry name (becomes the node key) /// Entry name (becomes the node key)
name: String, name: String,
/// Override timestamp (YYYY-MM-DD or YYYY-MM-DDTHH:MM)
#[arg(long)]
date: Option<String>,
/// Entry text /// Entry text
text: Vec<String>, text: Vec<String>,
}, },
@ -415,7 +418,7 @@ impl Run for NodeCmd {
impl Run for JournalCmd { impl Run for JournalCmd {
async fn run(self) -> anyhow::Result<()> { async fn run(self) -> anyhow::Result<()> {
match self { match self {
Self::Write { name, text } => cli::journal::cmd_journal_write(&name, &text).await, Self::Write { name, date, text } => cli::journal::cmd_journal_write(&name, date.as_deref(), &text).await,
Self::Tail { n, full, level } => cli::journal::cmd_journal_tail(n, full, level).await, Self::Tail { n, full, level } => cli::journal::cmd_journal_tail(n, full, level).await,
} }
} }