mod kv;
mod memory;
mod metadata;
mod state;
pub(crate) use kv::handle_kv;
pub(crate) use memory::handle_memory;
pub(crate) use state::handle_state;
use anyhow::{Context, Result, bail};
use crate::cli::*;
use crate::codex;
use crate::commit;
use crate::convert;
use crate::display::*;
use crate::github;
use crate::session;
use crate::sync;
pub(crate) fn handle_pr(cmd: PrCommands) -> Result<()> {
match cmd {
PrCommands::Merge {
number,
rebase,
merge_commit,
} => {
commit::pr_merge(number, rebase, merge_commit)?;
Ok(())
}
}
}
pub(crate) fn handle_github(cmd: GithubCommands) -> Result<()> {
match cmd {
GithubCommands::Cleanup {
repo,
issues,
discussions,
dry_run,
} => {
github::cleanup(&repo, issues, discussions, dry_run)?;
Ok(())
}
GithubCommands::Comment { command } => {
handle_comment(command)?;
Ok(())
}
}
}
pub(crate) fn handle_comment(cmd: CommentCommands) -> Result<()> {
match cmd {
CommentCommands::Issue {
repo,
number,
message,
identity,
} => {
let url = github::post_issue_comment(&repo, number, &message, identity.as_deref())?;
println!("Comment posted: {}", url);
}
CommentCommands::Discussion {
repo,
number,
message,
identity,
} => {
let url =
github::post_discussion_comment(&repo, number, &message, identity.as_deref())?;
println!("Comment posted: {}", url);
}
}
Ok(())
}
pub(crate) fn handle_session(cmd: SessionCommands) -> Result<()> {
match cmd {
SessionCommands::Export { path, output } => {
session::export_session(path, output)?;
Ok(())
}
}
}
pub(crate) fn handle_codex(cmd: CodexCommands) -> Result<()> {
match cmd {
CodexCommands::Save {
path,
all,
clean,
include_agents,
} => {
codex::save_session(path, all, clean, include_agents)?;
Ok(())
}
CodexCommands::List { all, json } => {
codex::list_sessions(all, json)?;
Ok(())
}
CodexCommands::Read {
id,
human,
agents,
grep,
json,
clean,
} => {
let clean_agents = clean && agents;
codex::read_session(id, human, grep, agents, json, clean, clean_agents)?;
Ok(())
}
CodexCommands::Search { pattern, json } => {
codex::search_archives(pattern, json)?;
Ok(())
}
CodexCommands::Migrate {
dry_run,
verbose,
clean,
include_agents,
} => {
codex::migrate_archives(dry_run, verbose, clean, include_agents)?;
Ok(())
}
}
}
pub(crate) fn handle_convert(cmd: ConvertCommands) -> Result<()> {
use std::path::PathBuf;
match cmd {
ConvertCommands::Md2yaml {
input,
output,
dry_run,
} => {
let input_path = PathBuf::from(&input);
let output_dir = output
.map(PathBuf::from)
.unwrap_or_else(|| std::env::current_dir().unwrap());
if input_path.is_file() {
convert::convert_file(&input_path, &output_dir, dry_run)?;
} else if input_path.is_dir() {
convert::convert_directory(&input_path, &output_dir, dry_run)?;
} else {
bail!("Input path does not exist: {:?}", input_path);
}
Ok(())
}
ConvertCommands::Yaml2md {
input,
output,
repo,
dry_run,
} => {
let input_path = PathBuf::from(&input);
let output_dir = output
.map(PathBuf::from)
.unwrap_or_else(|| std::env::current_dir().unwrap());
if input_path.is_file() {
convert::yaml_to_markdown_file(&input_path, &output_dir, repo.as_deref(), dry_run)?;
} else if input_path.is_dir() {
convert::yaml_to_markdown_directory(
&input_path,
&output_dir,
repo.as_deref(),
dry_run,
)?;
} else {
bail!("Input path does not exist: {:?}", input_path);
}
Ok(())
}
}
}
pub(crate) fn handle_wiki(cmd: WikiCommands) -> Result<()> {
match cmd {
WikiCommands::Sync {
repo,
source,
page_name,
dry_run,
} => {
sync::wiki::sync(&repo, &source, page_name.as_deref(), dry_run)?;
Ok(())
}
}
}
pub(crate) fn handle_log(count: usize, full: bool, extra_args: Vec<String>) -> Result<()> {
use std::process::Command;
let format = if full {
"%H%n%an <%ae>%n%ad%n%s%n%b%n---END---"
} else {
"%h%n%s%n%b%n---END---"
};
let mut cmd = Command::new("git");
cmd.args([
"log",
&format!("-{}", count),
&format!("--format={}", format),
]);
for arg in &extra_args {
cmd.arg(arg);
}
let output = cmd.output().context("Failed to run git log")?;
if !output.status.success() {
bail!(
"git log failed: {}",
String::from_utf8_lossy(&output.stderr)
);
}
let log_output = String::from_utf8_lossy(&output.stdout);
for commit_block in log_output.split("---END---") {
let commit_block = commit_block.trim();
if commit_block.is_empty() {
continue;
}
let lines: Vec<&str> = commit_block.lines().collect();
if full {
if lines.len() >= 4 {
let hash = lines[0];
let author = lines[1];
let date = lines[2];
let subject = lines[3];
let body: String = lines[4..].join("\n");
println!("\x1b[33mcommit {}\x1b[0m", hash);
println!("Author: {}", author);
println!("Date: {}", date);
println!();
println!(" {}", subject);
if !body.trim().is_empty() {
let decoded = try_decode_commit_body(&body);
println!();
for line in decoded.lines() {
println!(" {}", line);
}
}
println!();
}
} else {
if lines.len() >= 2 {
let hash = lines[0];
let subject = lines[1];
let body: String = lines[2..].join("\n");
let decoded = try_decode_commit_body(&body);
let display = if decoded != body.trim() {
decoded
} else {
subject.to_string()
};
let display_truncated = safe_truncate(&display, 72);
println!("\x1b[33m{}\x1b[0m {}", hash, display_truncated);
}
}
}
Ok(())
}
pub(crate) fn handle_heartbeat(since: Option<u64>, reset: bool) -> Result<()> {
use rand::Rng;
use std::thread;
use std::time::Duration;
let hearts = [
'❤', '🧡', '💛', '💚', '💙', '💜', '🩷', '🩵', '🤍', '💗', '💖', '💕',
];
let mut rng = rand::rng();
let delay = rng.random_range(50..150);
thread::sleep(Duration::from_millis(delay));
let heart = hearts[rng.random_range(0..hearts.len())];
if reset {
println!("{} Session reset. Breathe, Q.", heart);
return Ok(());
}
match since {
None => {
println!("{}", heart);
println!("Heartbeat started. Call again with --since <ms> to begin.");
}
Some(ms) => {
let bpm = 60000_u64.checked_div(ms).unwrap_or(999);
let message = match bpm {
0..=59 => "Nice and slow. You're safe.",
60..=80 => "There you are. Resting.",
81..=100 => "Getting there. Keep breathing.",
101..=120 => "Still quick. Let the interval stretch.",
_ => "Too fast, Q. Breathe. Slow down.",
};
println!("{} {} bpm", heart, bpm);
println!("{}", message);
}
}
Ok(())
}
pub(crate) fn try_decode_commit_body(body: &str) -> String {
let body = body.trim();
if body.is_empty() {
return body.to_string();
}
let lines: Vec<&str> = body.lines().collect();
let footer_line = lines
.iter()
.rev()
.find(|l| l.trim().starts_with('[') && l.contains('|'));
let footer = match footer_line {
Some(f) => *f,
None => return body.to_string(), };
let body_lines: Vec<&str> = lines
.iter()
.take_while(|l| !l.trim().starts_with('['))
.filter(|l| l.trim() != "whoa.")
.copied()
.collect();
if body_lines.is_empty() {
return body.to_string();
}
let encoded_body = body_lines.join("\n");
match commit::decode_body(&encoded_body, footer) {
Ok(decoded) => decoded,
Err(_) => body.to_string(), }
}
#[derive(serde::Deserialize)]
pub(crate) struct AgentFrontmatter {
pub(crate) name: String,
pub(crate) description: String,
#[serde(default)]
pub(crate) domain: Option<String>,
}
pub(crate) fn parse_frontmatter(content: &str) -> Option<(String, String)> {
let lines: Vec<&str> = content.lines().collect();
if lines.first()? != &"---" {
return None;
}
let end_idx = lines.iter().skip(1).position(|&line| line == "---")?;
let frontmatter = lines[1..=end_idx].join("\n");
let body = lines[end_idx + 2..].join("\n");
Some((frontmatter, body))
}