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
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
use clap::Parser;
use double_o::{
Action, argv, 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,
};
use std::ffi::OsString;
// ---------------------------------------------------------------------------
// 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<OsString>,
}
// ---------------------------------------------------------------------------
// 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).
// Read argv with args_os and treat the payload path and directory as
// Path so a non-UTF-8 TMPDIR works fully (see #213).
let raw_args: Vec<OsString> = std::env::args_os().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(
std::path::Path::new(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();
let args = cli.args;
// Pre-validate the reserved subcommand's arguments (see #213): a
// non-UTF-8 first word is simply the command to run, while a non-UTF-8
// argument given to an oo-owned option or a reserved subcommand's own
// text argument is rejected with exit 2 and the stderr line
// `oo: argument is not valid UTF-8`.
if let Err(msg) = argv::reject_non_utf8_reserved_args(&args) {
eprintln!("{msg}");
std::process::exit(2);
}
// 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(&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);
}