use anyhow::{bail, Result};
use chrono::{Local, Utc};
use serde::{Deserialize, Serialize};
use serde_json::json;
use std::fs::{self, OpenOptions};
use std::io::{BufRead, Write};
use std::path::{Path, PathBuf};
use patina::git;
const ACTIVE_SESSION_PATH: &str = ".patina/local/active-session.md";
const LAST_SESSION_PATH: &str = ".patina/local/last-session.md";
const LAST_UPDATE_PATH: &str = ".patina/local/.last-update";
const SESSIONS_DIR: &str = "layer/sessions";
const IMPORTANCE_KEYWORDS: &[&str] =
&["breakthrough", "discovered", "solved", "fixed", "important"];
#[derive(Debug, Clone, Serialize, Deserialize)]
struct SessionFrontmatter {
r#type: String,
id: String,
title: String,
status: String,
llm: String,
created: String,
start_timestamp: i64,
git: SessionGit,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct SessionGit {
branch: String,
starting_commit: String,
start_tag: String,
}
pub fn start_session(project_root: &Path, title: &str, adapter: Option<&str>) -> Result<()> {
let adapter = resolve_adapter(adapter, project_root)?;
let session_path = project_root.join(ACTIVE_SESSION_PATH);
let last_update_path = project_root.join(LAST_UPDATE_PATH);
if session_path.exists() {
println!("Found incomplete session, cleaning up...");
let line_count = fs::read_to_string(&session_path)
.map(|s| s.lines().count())
.unwrap_or(0);
if line_count > 10 {
if let Ok(old_id) = read_session_id(&session_path) {
let archive_path = project_root
.join(SESSIONS_DIR)
.join(format!("{}.md", old_id));
fs::create_dir_all(project_root.join(SESSIONS_DIR))?;
let content = fs::read_to_string(&session_path)?;
let archived = content.replacen("status: active", "status: archived", 1);
fs::write(&archive_path, archived)?;
println!(" Archived to {}/{}.md", SESSIONS_DIR, old_id);
}
} else {
println!(" Removed empty session file");
}
fs::remove_file(&session_path)?;
}
let now = Local::now();
let session_id = now.format("%Y%m%d-%H%M%S").to_string();
let session_tag = format!("session-{}-{}-start", session_id, adapter);
let branch = git::current_branch().unwrap_or_else(|_| "none".to_string());
if !git::is_clean().unwrap_or(true) {
println!("Warning: Uncommitted changes exist");
println!(" Consider: git stash or git commit -am 'WIP: saving work'");
println!();
}
if git::is_git_repo().unwrap_or(false) {
let is_work_related = branch == "work" || is_ancestor_of_head("work");
if !is_work_related {
if branch == "main" || branch == "master" {
if git::branch_exists("work").unwrap_or(false) {
git::checkout("work")?;
println!("Switched to work branch from {}", branch);
} else {
git::checkout_new_branch("work", &branch)?;
println!("Created and switched to work branch from {}", branch);
}
} else {
println!("On unrelated branch: {}", branch);
println!(
" Consider: git checkout work or git checkout -b work/{}",
branch
);
}
} else if branch != "work" {
println!("Staying on work sub-branch: {}", branch);
}
}
let branch = git::current_branch().unwrap_or_else(|_| "none".to_string());
let starting_commit = git::head_sha().unwrap_or_else(|_| "none".to_string());
if git::is_git_repo().unwrap_or(false) {
if git::tag_exists(&session_tag).unwrap_or(false) {
bail!(
"Session tag {} already exists.\n\
A session with this ID was already started. Wait a moment and retry.",
session_tag
);
}
match git::create_tag(&session_tag, &format!("Session start: {}", title)) {
Ok(()) => println!("Session tagged: {}", session_tag),
Err(e) => bail!("Failed to create session tag {}: {}", session_tag, e),
}
}
let started_utc = Utc::now().format("%Y-%m-%dT%H:%M:%SZ").to_string();
let start_timestamp = Utc::now().timestamp_millis();
let time_str = now.format("%H:%M").to_string();
let frontmatter = SessionFrontmatter {
r#type: "session".to_string(),
id: session_id.clone(),
title: title.to_string(),
status: "active".to_string(),
llm: adapter.clone(),
created: started_utc,
start_timestamp,
git: SessionGit {
branch: branch.clone(),
starting_commit: starting_commit.clone(),
start_tag: session_tag.clone(),
},
};
let yaml = serde_yaml::to_string(&frontmatter)?;
let scaffold = format!(
"---\n{yaml}---\n\n\
## Previous Session Context\n\
<!-- AI: Summarize the last session from last-session.md -->\n\n\
## Goals\n\
- [ ] {title}\n\n\
## Activity Log\n\
### {time_str} - Session Start\n\
Session initialized with goal: {title}\n\
Working on branch: {branch}\n\
Tagged as: {session_tag}\n\n",
);
fs::create_dir_all(session_path.parent().unwrap())?;
fs::write(&session_path, &scaffold)?;
fs::write(&last_update_path, &time_str)?;
let db_path = project_root.join(patina::eventlog::PATINA_DB);
let conn = patina::eventlog::initialize(&db_path)?;
let timestamp = now.to_rfc3339();
let data = json!({
"session_id": session_id,
"title": title,
"adapter": adapter,
"branch": branch,
"starting_commit": starting_commit,
"tag": session_tag,
});
patina::eventlog::insert_event(
&conn,
"session.started",
×tamp,
&session_id,
Some(ACTIVE_SESSION_PATH),
&data.to_string(),
)?;
println!("Session started: {}", title);
println!(" ID: {}", session_id);
println!(" Branch: {}", branch);
println!(" Tag: {}", session_tag);
if git::is_git_repo().unwrap_or(false) {
println!();
println!("Session Strategy:");
if branch == "work" {
println!("- You're on the 'work' branch - all sessions happen here");
} else {
println!(
"- You're on '{}' (work sub-branch) - perfect for isolated experiments",
branch
);
}
println!("- Session tagged as: {}", session_tag);
println!("- Commit early and often - each commit is a checkpoint");
println!("- Failed attempts are valuable memory");
}
show_beliefs_context(project_root);
show_previous_session_beliefs(project_root);
println!();
let last_session_path = project_root.join(LAST_SESSION_PATH);
if last_session_path.exists() {
println!(
"Please read {} and fill in the Previous Session Context section above.",
LAST_SESSION_PATH
);
} else {
println!("No previous session found. Starting fresh.");
}
println!(
"Then ask: 'Would you like me to create todos for \"{}\"?'",
title
);
Ok(())
}
pub fn update_session(project_root: &Path) -> Result<()> {
let session_path = project_root.join(ACTIVE_SESSION_PATH);
let last_update_path = project_root.join(LAST_UPDATE_PATH);
if !session_path.exists() {
bail!(
"No active session found at {}\nStart one with: patina session start \"<title>\"",
ACTIVE_SESSION_PATH
);
}
let session_id = read_session_id(&session_path)?;
let starting_commit = read_session_field(&session_path, "**Starting Commit**: ")?;
let last_update = fs::read_to_string(&last_update_path)
.map(|s| s.trim().to_string())
.unwrap_or_else(|_| "session start".to_string());
let branch = git::current_branch().unwrap_or_else(|_| "detached".to_string());
let commits_this_session = git::commits_since_count(&starting_commit).unwrap_or(0);
let last_commit_time = git::last_commit_relative_time().unwrap_or_else(|_| "never".to_string());
let last_commit_msg =
git::last_commit_message().unwrap_or_else(|_| "no commits yet".to_string());
let porcelain = git::status_porcelain().unwrap_or_default();
let modified = porcelain.lines().filter(|l| l.starts_with(" M")).count();
let staged = porcelain.lines().filter(|l| l.starts_with('M')).count();
let untracked = porcelain.lines().filter(|l| l.starts_with("??")).count();
let total_changes = modified + staged + untracked;
let diff_summary = git::diff_stat_summary().unwrap_or_default();
let lines_changed = parse_insertions(&diff_summary);
println!("Git Status Check");
println!();
println!("Current branch: {}", branch);
let recent = git::log_oneline(5).unwrap_or_default();
if !recent.is_empty() {
println!();
println!("Recent commits:");
for line in recent.lines() {
println!(" {}", line);
}
}
println!();
println!("Working tree status:");
if total_changes == 0 {
println!(" Clean working tree - all changes committed");
println!(" Last commit: {} - {}", last_commit_time, last_commit_msg);
} else {
println!(" Modified files: {}", modified);
println!(" Staged files: {}", staged);
println!(" Untracked files: {}", untracked);
println!(" Lines changed: ~{}", lines_changed);
println!(" Last commit: {}", last_commit_time);
println!();
if last_commit_time.contains("hour") {
println!(" Last commit was {}", last_commit_time);
println!(" Strong recommendation: Commit your work soon");
println!(
" Suggested: git add -p && git commit -m \"checkpoint: progress on session goals\""
);
} else if lines_changed > 100 {
println!(" Large changes detected ({}+ lines)", lines_changed);
println!(" Consider: Breaking into smaller commits");
println!(" Use: git add -p to stage selectively");
}
}
if !diff_summary.is_empty() {
println!();
println!("Changes summary:");
println!(" {}", diff_summary);
}
println!();
if total_changes == 0 {
println!("Session Health: Excellent (clean working tree)");
} else if last_commit_time.contains("hour") {
println!("Session Health: Good (commit recommended)");
} else {
println!("Session Health: Good (active development)");
}
let now = Local::now();
let time_str = now.format("%H:%M").to_string();
let mut update_section = format!(
"\n### {} - Update (covering since {})\n",
time_str, last_update
);
update_section.push_str("\n**Git Activity:**\n");
update_section.push_str(&format!(
"- Commits this session: {}\n",
commits_this_session
));
update_section.push_str(&format!("- Files changed: {}\n", total_changes));
update_section.push_str(&format!("- Last commit: {}\n", last_commit_time));
update_section.push('\n');
let mut file = OpenOptions::new().append(true).open(&session_path)?;
file.write_all(update_section.as_bytes())?;
fs::write(&last_update_path, &time_str)?;
let db_path = project_root.join(patina::eventlog::PATINA_DB);
let conn = patina::eventlog::initialize(&db_path)?;
let timestamp = now.to_rfc3339();
let data = json!({
"session_id": session_id,
"commits_this_session": commits_this_session,
"files_changed": total_changes,
"last_commit_time": last_commit_time,
"lines_changed": lines_changed,
"branch": branch,
});
patina::eventlog::insert_event(
&conn,
"session.update",
×tamp,
&session_id,
Some(ACTIVE_SESSION_PATH),
&data.to_string(),
)?;
println!();
println!("Please fill in the update section in active-session.md with:");
println!("- Work completed since {}", last_update);
println!("- Key decisions and reasoning");
println!("- Patterns observed");
println!();
println!("Update marker added: {} -> {}", last_update, time_str);
Ok(())
}
pub fn note_session(project_root: &Path, content: &str) -> Result<()> {
let session_path = project_root.join(ACTIVE_SESSION_PATH);
if !session_path.exists() {
bail!(
"No active session found at {}\nStart one with: patina session start \"<title>\"",
ACTIVE_SESSION_PATH
);
}
let branch = git::current_branch().unwrap_or_else(|_| "detached".to_string());
let sha = git::short_sha().unwrap_or_else(|_| "no-commits".to_string());
let git_context = format!("[{}@{}]", branch, sha);
let now = Local::now();
let time_str = now.format("%H:%M").to_string();
let note_section = format!("\n### {} - Note {}\n{}\n", time_str, git_context, content);
let mut file = OpenOptions::new().append(true).open(&session_path)?;
file.write_all(note_section.as_bytes())?;
let session_id = read_session_id(&session_path)?;
let db_path = project_root.join(patina::eventlog::PATINA_DB);
let conn = patina::eventlog::initialize(&db_path)?;
let timestamp = now.to_rfc3339();
let data = json!({
"session_id": session_id,
"content": content,
"branch": branch,
"sha": sha,
});
patina::eventlog::insert_event(
&conn,
"session.observation",
×tamp,
&session_id,
Some(ACTIVE_SESSION_PATH),
&data.to_string(),
)?;
println!("Note added to session {}", git_context);
let content_lower = content.to_lowercase();
if IMPORTANCE_KEYWORDS
.iter()
.any(|kw| content_lower.contains(kw))
{
println!();
println!("Important insight detected!");
println!(" Consider committing current work to preserve this context:");
println!(" git commit -am \"checkpoint: {}\"", truncate(content, 60));
}
Ok(())
}
pub fn end_session(project_root: &Path) -> Result<()> {
let session_path = project_root.join(ACTIVE_SESSION_PATH);
let last_update_path = project_root.join(LAST_UPDATE_PATH);
let last_session_path = project_root.join(LAST_SESSION_PATH);
if !session_path.exists() {
bail!(
"No active session found at {}\nStart one with: patina session start \"<title>\"",
ACTIVE_SESSION_PATH
);
}
let session_id = read_session_id(&session_path)?;
let session_title = read_session_field(&session_path, "# Session: ")?;
let session_tag = read_session_field(&session_path, "**Session Tag**: ")?;
let starting_commit = read_session_field(&session_path, "**Starting Commit**: ")?;
let adapter = read_session_field(&session_path, "**LLM**: ")?;
let end_tag = format!("session-{}-{}-end", session_id, adapter);
if git::is_git_repo().unwrap_or(false) {
match git::create_tag(&end_tag, &format!("Session end: {}", session_title)) {
Ok(()) => println!("✅ Session end tagged: {}", end_tag),
Err(_) => println!("⚠️ Could not create end tag (may already exist)"),
}
}
let changed_files = git::files_changed_since(&session_tag).unwrap_or_default();
let files_changed = changed_files.len();
let commits_made = git::commits_since_count(&starting_commit).unwrap_or(0);
let patterns_modified = changed_files
.iter()
.filter(|f| {
f.starts_with("layer/core/")
|| f.starts_with("layer/surface/")
|| f.starts_with("layer/topics/")
})
.count();
let classification = classify_work(commits_made, files_changed, patterns_modified);
let uncommitted = git::status_count().unwrap_or(0);
if uncommitted > 0 {
println!();
println!("⚠️ Uncommitted changes detected!");
println!(" You have {} uncommitted files", uncommitted);
println!(" Strongly recommend: Commit or stash before ending session");
}
let branch = git::current_branch().unwrap_or_else(|_| "none".to_string());
println!();
println!("═══ Session Summary ═══");
println!();
println!("Working branch: {}", branch);
println!("Session range: {}..{}", session_tag, end_tag);
println!();
println!("Session Metrics:");
println!("- Files changed: {}", files_changed);
println!("- Commits made: {}", commits_made);
println!("- Patterns touched: {}", patterns_modified);
println!("- Classification: {}", classification_label(classification));
println!();
println!("Session Preserved:");
println!("View session work: git log {}..{}", session_tag, end_tag);
println!("Diff session: git diff {}..{}", session_tag, end_tag);
println!(
"Cherry-pick to main: git cherry-pick {}..{}",
session_tag, end_tag
);
let (beliefs_captured, beliefs_summary) = count_beliefs_captured(project_root, &changed_files);
println!();
println!("Beliefs Captured: {}", beliefs_captured);
if !beliefs_summary.is_empty() {
for line in &beliefs_summary {
println!("{}", line);
}
}
let mut appendix = String::new();
appendix.push_str(&format!("\n## Beliefs Captured: {}\n", beliefs_captured));
if beliefs_captured > 0 {
for line in &beliefs_summary {
appendix.push_str(&format!("{}\n", line));
}
} else {
appendix.push_str("_No beliefs captured this session_\n");
}
appendix.push_str("\n## Session Classification\n");
appendix.push_str(&format!("- Work Type: {}\n", classification));
appendix.push_str(&format!("- Files Changed: {}\n", files_changed));
appendix.push_str(&format!("- Commits: {}\n", commits_made));
appendix.push_str(&format!("- Patterns Modified: {}\n", patterns_modified));
appendix.push_str(&format!("- Beliefs Captured: {}\n", beliefs_captured));
appendix.push_str(&format!("- Session Tags: {}..{}\n", session_tag, end_tag));
let prompts = extract_user_prompts(project_root, &session_path);
if !prompts.is_empty() {
appendix.push_str(&format!("\n## User Prompts ({})\n\n", prompts.len()));
for (i, prompt) in prompts.iter().enumerate() {
let display = truncate(prompt, 97);
let display = display.replace('`', "\\`");
appendix.push_str(&format!("{}. `{}`\n", i + 1, display));
}
println!("✅ Captured {} user prompts", prompts.len());
}
{
let mut file = OpenOptions::new().append(true).open(&session_path)?;
file.write_all(appendix.as_bytes())?;
}
let archive_path = project_root
.join(SESSIONS_DIR)
.join(format!("{}.md", session_id));
fs::create_dir_all(project_root.join(SESSIONS_DIR))?;
let session_content = fs::read_to_string(&session_path)?;
let archived_content = if session_content.starts_with("---") {
session_content.replacen("status: active", "status: archived", 1)
} else {
session_content
};
fs::write(&archive_path, archived_content)?;
let last_session_content = format!(
"# Last Session: {title}\n\n\
See: {sessions_dir}/{id}.md\n\
Tags: {start_tag}..{end_tag}\n\
Classification: {classification}\n\n\
Quick start: /session-start \"continue from {title}\"\n",
title = session_title,
sessions_dir = SESSIONS_DIR,
id = session_id,
start_tag = session_tag,
end_tag = end_tag,
classification = classification,
);
fs::write(&last_session_path, &last_session_content)?;
let now = Local::now();
let db_path = project_root.join(patina::eventlog::PATINA_DB);
let conn = patina::eventlog::initialize(&db_path)?;
let timestamp = now.to_rfc3339();
let data = json!({
"session_id": session_id,
"title": session_title,
"adapter": adapter,
"classification": classification,
"files_changed": files_changed,
"commits_made": commits_made,
"patterns_modified": patterns_modified,
"beliefs_captured": beliefs_captured,
"end_tag": end_tag,
"session_tag": session_tag,
});
patina::eventlog::insert_event(
&conn,
"session.ended",
×tamp,
&session_id,
Some(&format!("{}/{}.md", SESSIONS_DIR, session_id)),
&data.to_string(),
)?;
fs::remove_file(&session_path)?;
if last_update_path.exists() {
fs::remove_file(&last_update_path)?;
}
println!();
println!("✓ Session archived:");
println!(" - {}/{}.md", SESSIONS_DIR, session_id);
println!(" - Updated last-session.md");
println!();
println!("✓ Session preserved via tags: {}..{}", session_tag, end_tag);
println!(" View work: git log {}..{}", session_tag, end_tag);
println!();
println!("Session Memory:");
println!(" Your work is preserved in Git history and can be found by:");
println!(" - git log --grep=\"{}\"", session_title);
println!(" - git tag | grep session");
Ok(())
}
fn read_session_id(session_path: &Path) -> Result<String> {
read_session_field(session_path, "**ID**: ")
}
fn read_session_field(session_path: &Path, prefix: &str) -> Result<String> {
let contents = fs::read_to_string(session_path)?;
if let Some(fm) = parse_session_frontmatter(&contents) {
let value = match prefix {
"**ID**: " => Some(fm.id),
"# Session: " => Some(fm.title),
"**Started**: " => Some(fm.created.clone()),
"**Start Timestamp**: " => Some(fm.start_timestamp.to_string()),
"**LLM**: " => Some(fm.llm),
"**Git Branch**: " => Some(fm.git.branch),
"**Session Tag**: " => Some(fm.git.start_tag),
"**Starting Commit**: " => Some(fm.git.starting_commit),
_ => None,
};
if let Some(v) = value {
return Ok(v);
}
}
for line in contents.lines() {
if let Some(value) = line.strip_prefix(prefix) {
return Ok(value.trim().to_string());
}
}
bail!(
"Could not find '{}' in {}",
prefix.trim(),
session_path.display()
)
}
fn parse_session_frontmatter(content: &str) -> Option<SessionFrontmatter> {
let rest = content.strip_prefix("---")?;
let end = rest.find("\n---")?;
let yaml_str = &rest[..end];
serde_yaml::from_str(yaml_str).ok()
}
fn parse_insertions(summary: &str) -> usize {
summary
.split(',')
.find(|s| s.contains("insertion"))
.and_then(|s| s.split_whitespace().next())
.and_then(|n| n.parse().ok())
.unwrap_or(0)
}
fn truncate(s: &str, max_len: usize) -> String {
if s.len() <= max_len {
s.to_string()
} else {
format!("{}...", &s[..max_len])
}
}
fn is_ancestor_of_head(branch: &str) -> bool {
std::process::Command::new("git")
.args(["merge-base", "--is-ancestor", branch, "HEAD"])
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
fn show_beliefs_context(project_root: &Path) {
let beliefs_dir = project_root.join("layer/surface/epistemic/beliefs");
if !beliefs_dir.is_dir() {
return;
}
let mut belief_files: Vec<PathBuf> = fs::read_dir(&beliefs_dir)
.into_iter()
.flatten()
.flatten()
.filter(|e| {
let name = e.file_name();
let name = name.to_string_lossy();
name.ends_with(".md") && name != "_index.md"
})
.map(|e| e.path())
.collect();
if belief_files.is_empty() {
return;
}
belief_files.sort_by(|a, b| {
let ma = a.metadata().and_then(|m| m.modified()).ok();
let mb = b.metadata().and_then(|m| m.modified()).ok();
mb.cmp(&ma)
});
println!();
println!("Epistemic Beliefs: {} total", belief_files.len());
println!(" Recent beliefs:");
for path in belief_files.iter().take(5) {
if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) {
println!(" - {}", stem);
}
}
}
fn show_previous_session_beliefs(project_root: &Path) {
let last_session_path = project_root.join(LAST_SESSION_PATH);
if !last_session_path.exists() {
return;
}
let content = match fs::read_to_string(&last_session_path) {
Ok(c) => c,
Err(_) => return,
};
let prev_path = content
.lines()
.find_map(|l| l.strip_prefix("See: "))
.map(|s| s.trim().to_string());
let prev_title = content
.lines()
.find_map(|l| l.strip_prefix("# Last Session: "))
.map(|s| s.trim().to_string());
let (Some(prev_path), Some(prev_title)) = (prev_path, prev_title) else {
return;
};
let full_path = project_root.join(&prev_path);
let prev_content = match fs::read_to_string(&full_path) {
Ok(c) => c,
Err(_) => return,
};
let beliefs_line = prev_content
.lines()
.find(|l| l.starts_with("## Beliefs Captured:"));
if let Some(line) = beliefs_line {
let count_str = line.trim_start_matches("## Beliefs Captured:").trim();
let count: usize = count_str.parse().unwrap_or(0);
println!();
if count > 0 {
println!(
"Previous session \"{}\" captured {} belief(s):",
prev_title, count
);
let mut in_section = false;
for line in prev_content.lines() {
if line.starts_with("## Beliefs Captured:") {
in_section = true;
continue;
}
if in_section && line.starts_with("## ") {
break;
}
if in_section && line.trim_start().starts_with('-') {
println!("{}", line);
}
}
} else {
println!("Previous session \"{}\": no beliefs captured", prev_title);
}
}
}
fn classify_work(commits: usize, files: usize, patterns: usize) -> &'static str {
if commits == 0 {
"exploration"
} else if patterns > 0 {
"pattern-work"
} else if files > 10 {
"major-feature"
} else if commits < 3 {
"experiment"
} else {
"feature"
}
}
fn classification_label(classification: &str) -> &'static str {
match classification {
"exploration" => "🧪 EXPLORATION (no commits)",
"pattern-work" => "📚 PATTERN-WORK (modified patterns)",
"major-feature" => "🚀 MAJOR-FEATURE (many files)",
"experiment" => "🔬 EXPERIMENT (few commits)",
"feature" => "✨ FEATURE (normal work)",
_ => "❓ UNKNOWN",
}
}
fn count_beliefs_captured(project_root: &Path, changed_files: &[String]) -> (usize, Vec<String>) {
let beliefs_dir = project_root.join("layer/surface/epistemic/beliefs");
if !beliefs_dir.is_dir() {
return (0, vec![]);
}
let entries = match fs::read_dir(&beliefs_dir) {
Ok(e) => e,
Err(_) => return (0, vec![]),
};
let mut count = 0;
let mut summaries = Vec::new();
for entry in entries.flatten() {
let name = entry.file_name();
let name_str = name.to_string_lossy();
if !name_str.ends_with(".md") || name_str.as_ref() == "_index.md" {
continue;
}
let relative_path = format!("layer/surface/epistemic/beliefs/{}", name_str);
if changed_files.iter().any(|f| f == &relative_path) {
count += 1;
let belief_id = name_str.trim_end_matches(".md");
let path = entry.path();
let statement = fs::read_to_string(&path).ok().and_then(|content| {
content
.lines()
.find(|l| l.starts_with("statement:"))
.map(|l| l.trim_start_matches("statement:").trim().to_string())
});
if let Some(stmt) = statement {
summaries.push(format!(" - **{}**: {}", belief_id, stmt));
}
}
}
(count, summaries)
}
fn extract_user_prompts(project_root: &Path, session_path: &Path) -> Vec<String> {
let start_ts: i64 = match read_session_field(session_path, "**Start Timestamp**: ") {
Ok(ts) => match ts.parse() {
Ok(v) => v,
Err(_) => return vec![],
},
Err(_) => return vec![],
};
let home = match std::env::var("HOME") {
Ok(h) => h,
Err(_) => return vec![],
};
let history_path = PathBuf::from(&home).join(".claude/history.jsonl");
if !history_path.exists() {
return vec![];
}
let project_path = match project_root.canonicalize() {
Ok(p) => p.to_string_lossy().to_string(),
Err(_) => return vec![],
};
let file = match fs::File::open(&history_path) {
Ok(f) => f,
Err(_) => return vec![],
};
let reader = std::io::BufReader::new(file);
let mut prompts = Vec::new();
for line in reader.lines() {
let line = match line {
Ok(l) => l,
Err(_) => continue,
};
if line.is_empty() {
continue;
}
let entry: serde_json::Value = match serde_json::from_str(&line) {
Ok(v) => v,
Err(_) => continue,
};
let ts = entry.get("timestamp").and_then(|v| v.as_i64()).unwrap_or(0);
let project = entry.get("project").and_then(|v| v.as_str()).unwrap_or("");
let display = entry.get("display").and_then(|v| v.as_str()).unwrap_or("");
if ts >= start_ts && project == project_path && !display.is_empty() {
prompts.push(display.to_string());
}
}
prompts
}
pub fn resolve_adapter(explicit: Option<&str>, project_root: &Path) -> Result<String> {
if let Some(name) = explicit {
return Ok(name.to_string());
}
let config = patina::project::load(project_root)?;
Ok(config.adapters.default)
}