1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
use clap::Parser;
use double_o::{
Action, cmd_forget, cmd_help, cmd_hook, cmd_init, cmd_learn, cmd_patterns, cmd_recall,
cmd_rewrite, cmd_run, learn, learn_status, parse_action, warn_ignored_overrides,
};
// ---------------------------------------------------------------------------
// CLI
// ---------------------------------------------------------------------------
#[derive(Parser)]
#[command(
name = "oo",
version,
about = "Context-efficient command runner for AI coding agents",
long_about = "oo\n\nContext-efficient command runner for AI coding agents."
)]
struct Cli {
/// A subcommand, or a command to run. Subcommands:
/// recall (recall indexed output), forget (clear indexed output for this project),
/// learn (auto-generate a compression pattern), help (cheat.sh sheet),
/// init (set up agent hooks: --format claude|generic, --agent pi|claude-code [--global]; see oo init --help), patterns (list compression patterns),
/// rewrite (print oo-prefixed command for agent hooks),
/// hook (rewrite Bash commands for agent hook processors, e.g. `oo hook claude`),
/// version (print oo version). Any other command runs as a shell command.
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
}
// ---------------------------------------------------------------------------
// Entry point
// ---------------------------------------------------------------------------
fn main() {
// Rust ignores SIGPIPE by default, so a write to a closed pipe (`oo patterns | head -1`)
// returns EPIPE and `println!` panics. Restoring the default makes the process exit
// quietly like other Unix tools. Must run before any output. This does not reach
// std TcpStream writes do not raise SIGPIPE: MSG_NOSIGNAL on Linux (library/std/src/sys/net/connection/socket/mod.rs:435),
// SO_NOSIGPIPE on Apple (socket/unix.rs:101-104), so a reset API connection in `_learn_bg` surfaces as an error.
#[cfg(unix)]
// SAFETY: signal(2) with SIG_DFL only changes the disposition for this process.
unsafe {
libc::signal(libc::SIGPIPE, libc::SIG_DFL);
}
// Before any dispatch, so the warning appears once on every code path,
// including the background learn child.
warn_ignored_overrides();
// Intercept _learn_bg before clap parsing (it's a hidden internal command)
let raw_args: Vec<String> = std::env::args().collect();
if raw_args.get(1).is_some_and(|a| a == "_learn_bg") {
let status = learn_status::learn_status_path();
match (raw_args.get(2), raw_args.get(3)) {
(Some(path), Some(dir)) => {
let _ = learn::run_background(path, std::path::Path::new(dir), &status);
}
// Older parents pass only the path: report it, and leave the file alone
// because argv is untrusted and the directory is what makes it a payload.
(Some(_), None) => {
let _ = learn_status::write_learn_status_failure(
&status,
"learn",
"_learn_bg needs a payload directory argument",
);
}
_ => {}
}
std::process::exit(0);
}
let cli = Cli::parse();
// Show any pending learn-status message from a previous background learn.
// Runs before command dispatch so the user sees the result on the next invocation.
{
let status_path = learn_status::learn_status_path();
learn_status::check_and_clear_learn_status(&status_path);
}
let exit_code = match parse_action(&cli.args) {
Action::Help(None) => {
println!("oo — Context-efficient command runner for AI coding agents");
println!();
println!("Usage: oo <command> [args...]");
println!();
println!("Commands:");
println!(" recall [--full] <query> Search indexed output");
println!(" forget Clear indexed output for this project");
println!(" learn [--hint <text>] <cmd> [args...] Learn output compression patterns");
println!(
" help [cmd] Show help (or cheat-sheet for cmd via cheat.sh)"
);
println!(
" init [--format claude|generic | --agent pi|claude-code [--global]] Set up hooks for agent frameworks (see: oo init --help)"
);
println!(" patterns List output compression patterns");
println!(" rewrite <command...> Print oo-prefixed form for agent hooks");
println!(
" hook <agent> Agent hook processor (e.g. `oo hook claude` reads PreToolUse JSON on stdin)"
);
println!(" version Show version");
println!();
println!("Any other command is run in the shell, e.g. oo git log");
0
}
Action::Help(Some(cmd)) => cmd_help(&cmd),
Action::Usage(text) => {
print!("{text}");
0
}
Action::Version => {
println!("oo {}", env!("CARGO_PKG_VERSION"));
0
}
Action::Run(args) => cmd_run(&args),
Action::Recall { query, full } => cmd_recall(&query, full),
Action::Forget => cmd_forget(),
Action::Learn(args, hint) => cmd_learn(&args, hint.as_deref()),
Action::Init(mode) => cmd_init(mode),
Action::Patterns => cmd_patterns(),
Action::Rewrite(command) => cmd_rewrite(&command),
Action::Hook(agent) => match agent {
Some(agent) => cmd_hook(&agent),
None => {
eprintln!("oo: hook requires an agent (e.g. `oo hook claude`)");
1
}
},
};
std::process::exit(exit_code);
}