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,
no_cleanup,
} => {
commit::pr_merge(number, rebase, merge_commit, no_cleanup)?;
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<()> {
let suppress_vault_warning = matches!(
cmd,
CodexCommands::Archive {
args: ArchiveArgs {
backfill: Some(_),
..
},
} | CodexCommands::Save {
args: ArchiveArgs {
backfill: Some(_),
..
},
}
);
codex::notices::warn_if_vault_present(suppress_vault_warning);
match cmd {
CodexCommands::Save { args } => {
eprintln!("note: `mx codex save` is deprecated; use `mx codex archive` instead.");
handle_codex_archive(args)
}
CodexCommands::Archive { args } => handle_codex_archive(args),
CodexCommands::Export {
session,
project,
date,
format,
include,
archive_first,
output,
} => handle_codex_export(
session,
project,
date,
format,
include,
archive_first,
output,
),
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(())
}
}
}
fn handle_codex_archive(args: ArchiveArgs) -> Result<()> {
let ArchiveArgs {
path,
all,
clean,
include_agents,
include,
backfill,
} = args;
let include_set = codex::IncludeSet::parse(&include)?;
if include_agents && !include_set.subagents {
anyhow::bail!(
"--include-agents requires `subagents` in --include (got --include='{}'). \
Either drop --include-agents or add `subagents` to --include.",
include
);
}
if let Some(vault_arg) = backfill {
let vault_path = if vault_arg.is_empty() {
crate::paths::wonka_vault_archives_dir()
} else {
std::path::PathBuf::from(vault_arg)
};
let options = codex::archive::ArchiveOptions {
clean,
include: include_set,
include_agents_in_clean_md: include_agents,
};
let report = codex::run_backfill(&vault_path, options)?;
println!(
"vault={} snapshots={} found={} archived={} skipped={} errors={}",
report.vault_path.display(),
report.vault_snapshots_walked,
report.sessions_found,
report.sessions_archived,
report.sessions_skipped_already_archived,
report.errors.len()
);
return Ok(());
}
codex::archive_session(path, all, clean, include_agents, include_set)?;
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn handle_codex_export(
session: Option<String>,
project: Option<String>,
date: Option<String>,
format: String,
include: String,
archive_first: bool,
output: Option<String>,
) -> Result<()> {
use std::path::PathBuf;
let selector = match (session, project, date) {
(Some(s), None, None) => codex::Selector::Session(codex::export::SessionRef(s)),
(None, Some(p), None) => codex::Selector::Project(p),
(None, None, Some(d)) => codex::Selector::Date(codex::export::DateRange::parse(&d)?),
(None, None, None) => codex::Selector::Latest,
_ => anyhow::bail!(
"--session, --project, and --date are mutually exclusive; pass at most one"
),
};
let format = codex::Format::parse(&format)?;
let include = codex::ExportIncludeSet::parse(&include)?;
let request = codex::ExportRequest {
selector,
format,
include,
archive_first,
output: output.map(PathBuf::from),
};
codex::run_export(request)?;
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 result = try_decode_commit_body(&body);
println!();
for line in result.subject.lines() {
println!(" {}", line);
}
if let Some(trailing) = result.trailing.as_deref() {
println!();
for line in trailing.lines() {
println!(" \x1b[2m{}\x1b[0m", line);
}
}
}
println!();
}
} else {
if lines.len() >= 2 {
let hash = lines[0];
let subject = lines[1];
let body: String = lines[2..].join("\n");
let result = try_decode_commit_body(&body);
let display = if result.was_decoded {
result.subject
} else {
subject.to_string()
};
let display_truncated = safe_truncate(&display, 72);
println!("\x1b[33m{}\x1b[0m {}", hash, display_truncated);
}
}
}
Ok(())
}
fn is_footer_line(line: &str) -> bool {
let trimmed = line.trim();
let Some(algo) = commit::parse_compress_algo(trimmed) else {
return false;
};
if !commit::is_known_compress_algo(&algo) {
return false;
}
commit::parse_body_dict(trimmed).is_some()
}
pub(crate) struct DecodedCommit {
pub(crate) subject: String,
pub(crate) trailing: Option<String>,
pub(crate) was_decoded: bool,
}
impl DecodedCommit {
fn passthrough(original: &str) -> Self {
Self {
subject: original.trim().to_string(),
trailing: None,
was_decoded: false,
}
}
}
pub(crate) fn try_decode_commit_body(body: &str) -> DecodedCommit {
let trimmed = body.trim();
if trimmed.is_empty() {
return DecodedCommit::passthrough(trimmed);
}
let lines: Vec<&str> = trimmed.lines().collect();
let footer_idx = lines
.iter()
.enumerate()
.rev()
.find_map(|(i, l)| if is_footer_line(l) { Some(i) } else { None });
let footer_idx = match footer_idx {
Some(i) => i,
None => return DecodedCommit::passthrough(trimmed),
};
let footer = lines[footer_idx];
let body_lines: Vec<&str> = lines[..footer_idx]
.iter()
.filter(|l| l.trim() != commit::DEJAVU_MARKER)
.copied()
.collect();
let trailing_raw = lines[footer_idx + 1..].join("\n");
let trailing_trimmed = trailing_raw.trim();
let trailing = if trailing_trimmed.is_empty() {
None
} else {
Some(trailing_trimmed.to_string())
};
if body_lines.iter().all(|l| l.trim().is_empty()) {
return DecodedCommit::passthrough(trimmed);
}
let encoded_body = body_lines.join("\n");
match commit::decode_body(&encoded_body, footer) {
Ok(decoded) => DecodedCommit {
subject: decoded,
trailing,
was_decoded: true,
},
Err(_) => DecodedCommit::passthrough(trimmed),
}
}
#[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))
}
#[cfg(test)]
mod try_decode_commit_body_tests {
use super::*;
use crate::commit::encode_commit;
fn encode_until<F: Fn(&crate::commit::EncodedCommit) -> bool>(
title: &str,
body: &str,
predicate: F,
) -> crate::commit::EncodedCommit {
for _ in 0..1000 {
let Ok(enc) = encode_commit(title, body) else {
continue;
};
if !predicate(&enc) {
continue;
}
if enc.body.lines().any(is_footer_line) {
continue;
}
let canonical = format!("{}\n\n{}", enc.body, enc.footer);
let result = try_decode_commit_body(&canonical);
if !result.was_decoded || result.subject != body {
continue;
}
return enc;
}
panic!("encoder failed to satisfy predicate after 1000 attempts");
}
#[test]
fn footer_at_bottom_decodes_existing_behavior() {
let enc = encode_until("title diff", "the quick brown fox", |e| !e.dejavu);
let body = format!("{}\n\n{}", enc.body, enc.footer);
let result = try_decode_commit_body(&body);
assert!(result.was_decoded);
assert_eq!(result.subject, "the quick brown fox");
assert!(result.trailing.is_none());
}
#[test]
fn footer_followed_by_dejavu_marker_decodes_and_preserves_marker() {
let enc = encode_until("title diff", "decoded subject under dejavu", |e| e.dejavu);
let body = format!("{}\n\n{}", enc.body, enc.footer);
let result = try_decode_commit_body(&body);
assert!(result.was_decoded);
assert_eq!(result.subject, "decoded subject under dejavu");
assert!(
result.trailing.is_some(),
"dejavu marker must be preserved in trailing, not silently dropped"
);
assert!(!result.trailing.as_deref().unwrap().is_empty());
}
#[test]
fn user_amended_text_after_footer_decodes_and_preserves_note() {
let enc = encode_until("title diff", "the original message", |e| !e.dejavu);
let body = format!(
"{}\n\n{}\n\nP.S. amended later by hand.",
enc.body, enc.footer
);
let result = try_decode_commit_body(&body);
assert!(result.was_decoded);
assert_eq!(result.subject, "the original message");
assert_eq!(
result.trailing.as_deref(),
Some("P.S. amended later by hand.")
);
}
#[test]
fn user_amended_text_after_dejavu_marker_decodes() {
let enc = encode_until("title diff", "buried treasure", |e| e.dejavu);
let body = format!(
"{}\n\n{}\n\nuser note added during amend",
enc.body, enc.footer
);
let result = try_decode_commit_body(&body);
assert!(result.was_decoded);
assert_eq!(result.subject, "buried treasure");
let trailing = result.trailing.expect("trailing must be present");
assert!(trailing.contains("user note added during amend"));
}
#[test]
fn no_footer_returns_original_unchanged() {
let raw = "fix: a perfectly normal git commit\n\nWith a body.";
let result = try_decode_commit_body(raw);
assert!(!result.was_decoded);
assert_eq!(result.subject, raw);
assert!(result.trailing.is_none());
}
#[test]
fn empty_body_returns_empty() {
let r1 = try_decode_commit_body("");
assert!(!r1.was_decoded);
assert_eq!(r1.subject, "");
assert!(r1.trailing.is_none());
let r2 = try_decode_commit_body(" \n ");
assert!(!r2.was_decoded);
assert_eq!(r2.subject, "");
assert!(r2.trailing.is_none());
}
#[test]
fn footer_shaped_substring_inside_text_line_is_ignored() {
let enc = encode_until("title diff", "still decodes", |e| !e.dejavu);
let body = format!(
"{}\n\n{}\n\nSee [sha384:base62|lzma:base62] for the format.",
enc.body, enc.footer
);
let result = try_decode_commit_body(&body);
assert!(result.was_decoded);
assert_eq!(result.subject, "still decodes");
}
#[test]
fn markdown_brackets_in_body_are_not_mistaken_for_footer() {
let enc = encode_until("title diff", "round trip through markdown", |e| !e.dejavu);
let body = format!(
"{}\n\n{}\n\nSee the [link|here] for details.",
enc.body, enc.footer
);
let result = try_decode_commit_body(&body);
assert!(result.was_decoded);
assert_eq!(result.subject, "round trip through markdown");
}
#[test]
fn footer_shaped_with_unknown_algo_is_rejected() {
let enc = encode_until("title diff", "shape-only decoy ignored", |e| !e.dejavu);
let body = format!(
"{}\n\n{}\n\n[madeup:fakedict|notreal:alsofake]",
enc.body, enc.footer
);
let result = try_decode_commit_body(&body);
assert!(result.was_decoded);
assert_eq!(result.subject, "shape-only decoy ignored");
assert!(result.trailing.is_some());
}
#[test]
fn fixture_real_dejavu_commit_decodes() {
let body = format!(
"8NO48P3FCDPIGSJ5C5I6QP9978G76R39DKG46RRECPKMETBIC5Q6IRRE41Q6U83141Q6AOBJCLP20R39DPLMIRJ741Q6U834DTHN6BRGC5Q6GSPEDLI0====\n\n[blake2s:base32hex|snappy:base32hex]\n{}",
commit::DEJAVU_MARKER
);
let result = try_decode_commit_body(&body);
assert!(result.was_decoded);
assert_eq!(
result.subject,
"docs(readme): slim Configuration to a teaser linking to docs/paths.md"
);
assert!(result.trailing.is_some());
}
#[test]
fn footer_as_only_line_returns_passthrough() {
let body = "[sha384:base62|lzma:uuencode]";
let result = try_decode_commit_body(body);
assert!(!result.was_decoded);
assert_eq!(result.subject, body);
assert!(result.trailing.is_none());
}
#[test]
fn footer_as_first_line_with_trailing_only_passes_through() {
let body = "[sha384:base62|lzma:uuencode]\n\nA stray note with no encoded payload.";
let result = try_decode_commit_body(body);
assert!(!result.was_decoded);
assert!(result.trailing.is_none());
}
#[test]
fn whitespace_only_line_between_body_and_footer_decodes() {
let enc = encode_until("title diff", "tolerates extra blanks", |e| !e.dejavu);
let body = format!("{}\n\n \n\n{}", enc.body, enc.footer);
let result = try_decode_commit_body(&body);
assert!(result.was_decoded);
assert_eq!(result.subject, "tolerates extra blanks");
}
#[test]
fn trailing_whitespace_only_after_footer_yields_no_trailing() {
let enc = encode_until("title diff", "no trailing whitespace", |e| !e.dejavu);
let body = format!("{}\n\n{}\n \n ", enc.body, enc.footer);
let result = try_decode_commit_body(&body);
assert!(result.was_decoded);
assert_eq!(result.subject, "no trailing whitespace");
assert!(
result.trailing.is_none(),
"whitespace-only trailing must not produce a Some"
);
}
#[test]
fn is_footer_line_accepts_real_footer() {
assert!(is_footer_line("[sha384:base62|lzma:uuencode]"));
}
#[test]
fn is_footer_line_accepts_with_whitespace() {
assert!(is_footer_line(" [sha384:base62|lzma:uuencode] "));
}
#[test]
fn is_footer_line_rejects_markdown_link() {
assert!(!is_footer_line("[link|here]"));
}
#[test]
fn is_footer_line_rejects_plain_text() {
assert!(!is_footer_line("just some words"));
}
#[test]
fn is_footer_line_rejects_empty() {
assert!(!is_footer_line(""));
}
#[test]
fn is_footer_line_rejects_unknown_compress_algo() {
assert!(!is_footer_line("[sha384:base62|notarealalgo:uuencode]"));
assert!(!is_footer_line("[anything:anything|anything:anything]"));
}
#[test]
fn is_footer_line_accepts_each_known_algo() {
for algo in ["lzma", "zstd", "brotli", "gzip", "gz", "lz4", "snappy"] {
let line = format!("[sha384:base62|{}:uuencode]", algo);
assert!(
is_footer_line(&line),
"is_footer_line must accept known algo {}",
algo
);
}
}
}
#[cfg(test)]
mod codex_archive_validation_tests {
use super::*;
use crate::cli::CodexCommands;
fn archive_cmd(include_agents: bool, include: &str) -> CodexCommands {
CodexCommands::Archive {
args: ArchiveArgs {
path: None,
all: false,
clean: true,
include_agents,
include: include.to_string(),
backfill: None,
},
}
}
#[test]
fn include_agents_without_subagents_errors() {
let result = handle_codex(archive_cmd(true, "none"));
let err = result.expect_err("--include-agents + --include none must error");
let msg = format!("{err:#}");
assert!(
msg.contains("--include-agents") && msg.contains("subagents"),
"error must explain the constraint, got: {msg}"
);
}
#[test]
fn include_agents_without_subagents_token_errors_even_with_others() {
let result = handle_codex(archive_cmd(true, "mcp,history"));
assert!(
result.is_err(),
"--include-agents + --include without subagents must error"
);
}
#[test]
fn save_alias_routes_to_same_handler_as_archive() {
let archive_result = handle_codex(CodexCommands::Archive {
args: ArchiveArgs {
path: None,
all: false,
clean: true,
include_agents: true,
include: "none".to_string(),
backfill: None,
},
});
let save_result = handle_codex(CodexCommands::Save {
args: ArchiveArgs {
path: None,
all: false,
clean: true,
include_agents: true,
include: "none".to_string(),
backfill: None,
},
});
let archive_err = archive_result
.expect_err("archive with invalid args must error")
.to_string();
let save_err = save_result
.expect_err("save with invalid args must error")
.to_string();
assert_eq!(
archive_err, save_err,
"save alias must produce the same error as archive: \
archive={archive_err:?}, save={save_err:?}"
);
}
}