mod agent;
mod audit_docs;
mod autoclaim;
mod boundary;
mod claim;
mod clean;
mod commands;
mod compact;
mod config;
mod convert;
mod parallel;
mod preflight;
mod diff;
mod extract;
mod focus;
mod git;
mod history;
mod init;
mod install;
mod layout;
mod mode;
mod outline;
mod patch;
mod plugin;
mod prompt;
mod recover;
mod rename;
mod reset;
mod resync;
mod route;
mod sessions;
mod skill;
mod snapshot;
mod start;
mod stream;
mod run;
mod sync;
mod terminal;
pub(crate) use agent_doc::ipc_socket;
mod undo;
mod upgrade;
mod watch;
mod worktree;
mod write;
pub(crate) use agent_doc::component;
pub(crate) use agent_doc::crdt;
pub(crate) use agent_doc::frontmatter;
pub(crate) use agent_doc::merge;
pub(crate) use agent_doc::template;
use anyhow::Context;
use clap::{Parser, Subcommand, ValueEnum};
use std::path::{Path, PathBuf};
#[derive(Clone, Debug, ValueEnum)]
pub enum AgentDocMode {
Append,
Template,
Stream,
}
#[derive(Parser)]
#[command(name = "agent-doc", version, about = "Interactive document sessions with AI agents")]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
Run {
file: PathBuf,
#[arg(short = 'b')]
branch: bool,
#[arg(long)]
agent: Option<String>,
#[arg(long)]
model: Option<String>,
#[arg(long)]
dry_run: bool,
#[arg(long)]
no_git: bool,
},
History {
file: PathBuf,
#[arg(long)]
restore: Option<String>,
},
Init {
file: Option<PathBuf>,
title: Option<String>,
#[arg(long)]
agent: Option<String>,
#[arg(long)]
mode: Option<String>,
},
Install {
#[arg(long)]
editor: Option<String>,
#[arg(long)]
skip_prereqs: bool,
#[arg(long)]
skip_plugins: bool,
},
Diff {
file: PathBuf,
#[arg(long)]
wait: bool,
},
Reset {
file: PathBuf,
},
Clean {
file: PathBuf,
},
AuditDocs {
#[arg(long)]
root: Option<PathBuf>,
},
Start {
file: PathBuf,
},
Route {
file: PathBuf,
#[arg(long)]
pane: Option<String>,
#[arg(long = "col")]
cols: Vec<String>,
#[arg(long)]
focus: Option<String>,
#[arg(long, default_value_t = 0)]
debounce: u64,
},
Prompt {
file: Option<PathBuf>,
#[arg(long)]
answer: Option<usize>,
#[arg(long)]
all: bool,
},
Commit {
file: PathBuf,
},
Claim {
file: PathBuf,
#[arg(long)]
position: Option<String>,
#[arg(long)]
pane: Option<String>,
#[arg(long)]
window: Option<String>,
#[arg(long)]
force: bool,
},
Focus {
file: PathBuf,
#[arg(long)]
pane: Option<String>,
},
Layout {
files: Vec<PathBuf>,
#[arg(long, short, default_value = "h")]
split: String,
#[arg(long)]
pane: Option<String>,
#[arg(long)]
window: Option<String>,
},
Sync {
#[arg(long = "col", required = true)]
columns: Vec<String>,
#[arg(long)]
window: Option<String>,
#[arg(long)]
focus: Option<String>,
},
Patch {
file: PathBuf,
component: String,
content: Option<String>,
},
Watch {
#[arg(long)]
stop: bool,
#[arg(long)]
status: bool,
#[arg(long, default_value = "500")]
debounce: u64,
#[arg(long, default_value = "3")]
max_cycles: u32,
},
Outline {
file: PathBuf,
#[arg(long)]
json: bool,
},
Resync {
#[arg(long)]
fix: bool,
},
Skill {
#[command(subcommand)]
command: SkillCommands,
},
Plugin {
#[command(subcommand)]
action: PluginAction,
},
Write {
file: PathBuf,
#[arg(long)]
baseline_file: Option<PathBuf>,
#[arg(long)]
template: bool,
#[arg(long)]
stream: bool,
#[arg(long)]
ipc: bool,
#[arg(long)]
force_disk: bool,
},
Stream {
file: PathBuf,
#[arg(long, default_value = "200")]
interval: u64,
#[arg(long)]
agent: Option<String>,
#[arg(long)]
model: Option<String>,
#[arg(long)]
no_git: bool,
},
TemplateInfo {
file: PathBuf,
},
Recover {
file: PathBuf,
},
Preflight {
file: PathBuf,
},
Compact {
file: PathBuf,
#[arg(long, default_value = "2")]
keep: usize,
#[arg(long)]
component: Option<String>,
#[arg(long)]
message: Option<String>,
},
Convert {
file: PathBuf,
#[arg(value_enum)]
mode: Option<AgentDocMode>,
#[arg(long, value_enum)]
agent_doc_format: Option<frontmatter::AgentDocFormat>,
#[arg(long, value_enum)]
agent_doc_write: Option<frontmatter::AgentDocWrite>,
},
Mode {
file: PathBuf,
#[arg(long)]
set: Option<String>,
},
Claims,
Parallel {
file: PathBuf,
#[arg(long = "task")]
tasks_explicit: Vec<String>,
#[arg(long)]
model: Option<String>,
#[arg(long)]
no_git: bool,
#[arg(long)]
no_worktree: bool,
#[arg(long, default_value = "600")]
timeout: u64,
#[arg(long)]
dry_run: bool,
},
Autoclaim,
Upgrade,
Undo {
file: PathBuf,
},
Extract {
source: PathBuf,
target: PathBuf,
#[arg(long)]
component: Option<String>,
},
Transfer {
source: PathBuf,
target: PathBuf,
component: String,
},
Rename {
old_path: PathBuf,
new_path: PathBuf,
},
Terminal {
file: PathBuf,
#[arg(long)]
session: Option<String>,
},
Boundary {
file: PathBuf,
#[arg(long)]
component: Option<String>,
},
LibPath,
#[command(name = "commands")]
#[allow(clippy::enum_variant_names)]
ListCommands,
}
#[derive(Subcommand)]
enum PluginAction {
Install {
editor: String,
#[clap(long)]
local: bool,
},
Update {
editor: String,
},
List,
}
#[derive(Subcommand)]
enum SkillCommands {
Install {
#[arg(long)]
reload: Option<String>,
},
Check,
}
fn main() -> anyhow::Result<()> {
let cli = Cli::parse();
if !matches!(cli.command, Commands::Upgrade) {
upgrade::warn_if_outdated();
}
let config = config::load()?;
match cli.command {
Commands::Run {
file,
branch,
agent,
model,
dry_run,
no_git,
} => run::run(&file, branch, agent.as_deref(), model.as_deref(), dry_run, no_git, &config),
Commands::History { file, restore } => match restore {
Some(commit) => history::restore(&file, &commit),
None => history::list(&file),
},
Commands::Init { file, title, agent, mode } => {
init::run(file.as_deref(), title.as_deref(), agent.as_deref(), mode.as_deref(), &config)
}
Commands::Install { editor, skip_prereqs, skip_plugins } => {
install::run(editor.as_deref(), skip_prereqs, skip_plugins)
}
Commands::Diff { file, wait } => diff::run(&file, wait),
Commands::Reset { file } => reset::run(&file),
Commands::Clean { file } => clean::run(&file),
Commands::AuditDocs { root } => audit_docs::run(root.as_deref()),
Commands::Start { file } => start::run(&file),
Commands::Route { file, pane, cols, focus, debounce } => {
let result = route::run(&file, pane.as_deref(), debounce, &cols);
if !cols.is_empty()
&& let Err(e) = sync::run_layout_only(&cols, None, focus.as_deref())
{
eprintln!("[route] layout sync failed: {}", e);
}
result
}
Commands::Prompt { file, answer, all } => {
if all {
return prompt::run_all();
}
let file = file.context("FILE required when not using --all")?;
match answer {
Some(option) => prompt::answer(&file, option),
None => prompt::run(&file),
}
}
Commands::Commit { file } => git::commit(&file),
Commands::Claim { file, position, pane, window, force } => claim::run(&file, position.as_deref(), pane.as_deref(), window.as_deref(), force),
Commands::Focus { file, pane } => focus::run(&file, pane.as_deref()),
Commands::Layout { files, split, pane, window } => {
let split = match split.as_str() {
"v" | "vertical" => layout::Split::Vertical,
_ => layout::Split::Horizontal,
};
let paths: Vec<&Path> = files.iter().map(|f| f.as_path()).collect();
layout::run(&paths, split, pane.as_deref(), window.as_deref())
}
Commands::Sync {
columns,
window,
focus,
} => sync::run(&columns, window.as_deref(), focus.as_deref()),
Commands::Patch {
file,
component,
content,
} => patch::run(&file, &component, content.as_deref()),
Commands::Watch {
stop,
status,
debounce,
max_cycles,
} => {
if stop {
watch::stop()
} else if status {
watch::status()
} else {
watch::start(
&config,
watch::WatchConfig {
debounce_ms: debounce,
max_cycles,
},
)
}
}
Commands::Outline { file, json } => outline::run(&file, json),
Commands::Resync { fix } => resync::run(fix),
Commands::Skill { command } => match command {
SkillCommands::Install { reload } => {
let updated = skill::install_and_check_updated()?;
if updated
&& let Some(ref mode) = reload
{
match mode.as_str() {
"restart" => {
println!("SKILL_RELOAD=restart");
println!("Skill updated. Please restart this session with --resume to reload the skill.");
}
_ => {
println!("SKILL_RELOAD=compact");
println!("Skill updated. Please run /compact to reload the updated skill instructions.");
}
}
}
Ok(())
}
SkillCommands::Check => skill::check(),
},
Commands::Plugin { action } => match action {
PluginAction::Install { editor, local } => {
if local {
plugin::install_local(&editor)
} else {
plugin::install(&editor)
}
}
PluginAction::Update { editor } => plugin::update(&editor),
PluginAction::List => plugin::list(),
},
Commands::Write { file, baseline_file, template: is_template, stream: is_stream, ipc: is_ipc, force_disk } => {
let baseline = baseline_file
.as_ref()
.map(std::fs::read_to_string)
.transpose()
.context("failed to read baseline file")?;
if is_ipc {
write::run_ipc(&file, baseline.as_deref())
} else if is_stream {
write::run_stream(&file, baseline.as_deref(), force_disk)
} else if is_template {
write::run_template(&file, baseline.as_deref())
} else {
let content = std::fs::read_to_string(&file)
.context("failed to read document for mode detection")?;
let (fm, _) = frontmatter::parse(&content)?;
if fm.resolve_mode().is_crdt() {
write::run_stream(&file, baseline.as_deref(), force_disk)
} else {
write::run(&file, baseline.as_deref())
}
}
}
Commands::Stream { file, interval, agent, model, no_git } => {
stream::run(&file, interval, agent.as_deref(), model.as_deref(), no_git, &config)
}
Commands::TemplateInfo { file } => {
let info = template::template_info(&file)?;
println!("{}", serde_json::to_string_pretty(&info)?);
Ok(())
}
Commands::Recover { file } => {
let recovered = recover::run(&file)?;
if !recovered {
eprintln!("[recover] No pending response found for {}", file.display());
}
Ok(())
}
Commands::Preflight { file } => preflight::run(&file),
Commands::Compact {
file,
keep,
component,
message,
} => compact::run(&file, keep, component.as_deref(), message.as_deref()),
Commands::Convert { file, mode, agent_doc_format, agent_doc_write } => {
convert::run(&file, mode.as_ref(), agent_doc_format, agent_doc_write)
}
Commands::Mode { file, set } => mode::run(&file, set.as_deref()),
Commands::Undo { file } => undo::run(&file),
Commands::Extract { source, target, component } => extract::run(&source, &target, component.as_deref()),
Commands::Transfer { source, target, component } => extract::transfer(&source, &target, &component),
Commands::Rename { old_path, new_path } => rename::run(&old_path, &new_path),
Commands::Claims => {
let cwd = std::env::current_dir()?;
if let Some(root) = snapshot::find_project_root(&cwd) {
let log_path = root.join(".agent-doc/claims.log");
if let Ok(contents) = std::fs::read_to_string(&log_path)
&& !contents.is_empty()
{
print!("{}", contents);
std::fs::write(&log_path, "")?;
}
}
Ok(())
}
Commands::Parallel { file, tasks_explicit, model, no_git, no_worktree, timeout, dry_run } => {
parallel::run(&file, parallel::ParallelConfig {
tasks: tasks_explicit,
model,
no_git,
no_worktree,
timeout_secs: timeout,
dry_run,
})
}
Commands::Boundary { file, component } => boundary::run(&file, component.as_deref()),
Commands::Terminal { file, session } => terminal::run(&file, session.as_deref()),
Commands::Autoclaim => autoclaim::run(),
Commands::Upgrade => upgrade::run(),
Commands::LibPath => {
let exe = std::env::current_exe()?;
let dir = exe.parent().unwrap();
#[cfg(target_os = "linux")]
let lib_name = "libagent_doc.so";
#[cfg(target_os = "macos")]
let lib_name = "libagent_doc.dylib";
#[cfg(target_os = "windows")]
let lib_name = "agent_doc.dll";
let lib_path = dir.join(lib_name);
if lib_path.exists() {
println!("{}", lib_path.display());
} else {
eprintln!("[lib-path] library not found at {}", lib_path.display());
eprintln!("[lib-path] build with: cargo build --release");
std::process::exit(1);
}
Ok(())
}
Commands::ListCommands => commands::run(),
}
}