mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
Redirect debug client output to a file (#17234)
In the app-server debug client, allow redirecting output to a file in addition to just stdout. Shell redirecting works OK but is a bit weird with the interactive mode of the debug client since a bunch of newlines get dumped into the shell. With async messages from MCPs starting it's also tricky to actually type in a prompt.
This commit is contained in:
committed by
GitHub
Unverified
parent
81c0bcc921
commit
d013576f8b
@@ -12,7 +12,8 @@ Start the app-server client (it will spawn `codex app-server` itself):
|
||||
```
|
||||
cargo run -p codex-debug-client -- \
|
||||
--codex-bin codex \
|
||||
--approval-policy on-request
|
||||
--approval-policy on-request \
|
||||
--output-file /tmp/app-server-server-json.jsonl
|
||||
```
|
||||
|
||||
You can resume a specific thread:
|
||||
@@ -29,6 +30,7 @@ cargo run -p codex-debug-client -- --thread-id thr_123
|
||||
- `--approval-policy <policy>`: `untrusted`, `on-failure` (deprecated), `on-request`, `never`.
|
||||
- `--auto-approve`: auto-approve command/file-change approvals (default: decline).
|
||||
- `--final-only`: only show completed assistant messages and tool items.
|
||||
- `--output-file <path>`: write raw server JSONL to this file instead of stdout.
|
||||
- `--model <name>`: optional model override for thread start/resume.
|
||||
- `--model-provider <name>`: optional provider override.
|
||||
- `--cwd <path>`: optional working directory override.
|
||||
@@ -46,7 +48,8 @@ Type a line to send it as a new turn. Commands are prefixed with `:`:
|
||||
|
||||
The prompt shows the active thread id. Client messages (help, errors, approvals)
|
||||
print to stderr; raw server JSON prints to stdout so you can pipe/record it
|
||||
unless `--final-only` is set.
|
||||
unless `--final-only` is set. Pass `--output-file <path>` to record raw server
|
||||
JSONL to a file instead of stdout.
|
||||
|
||||
## Notes
|
||||
|
||||
|
||||
@@ -299,8 +299,8 @@ impl AppServerClient {
|
||||
}
|
||||
|
||||
let line = buffer.trim_end_matches(['\n', '\r']);
|
||||
if !line.is_empty() && !self.filtered_output {
|
||||
let _ = output.server_line(line);
|
||||
if !line.is_empty() {
|
||||
let _ = output.server_json_line(line, self.filtered_output);
|
||||
}
|
||||
|
||||
let message = match serde_json::from_str::<JSONRPCMessage>(line) {
|
||||
|
||||
@@ -4,8 +4,10 @@ mod output;
|
||||
mod reader;
|
||||
mod state;
|
||||
|
||||
use std::fs::File;
|
||||
use std::io;
|
||||
use std::io::BufRead;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::mpsc;
|
||||
|
||||
use anyhow::Context;
|
||||
@@ -50,6 +52,10 @@ struct Cli {
|
||||
#[arg(long, default_value_t = false)]
|
||||
final_only: bool,
|
||||
|
||||
/// Write raw server JSONL to this file instead of stdout.
|
||||
#[arg(long, value_name = "PATH")]
|
||||
output_file: Option<PathBuf>,
|
||||
|
||||
/// Optional model override when starting/resuming a thread.
|
||||
#[arg(long)]
|
||||
model: Option<String>,
|
||||
@@ -65,7 +71,18 @@ struct Cli {
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let cli = Cli::parse();
|
||||
let output = Output::new();
|
||||
let jsonl_file = cli
|
||||
.output_file
|
||||
.as_ref()
|
||||
.map(File::create)
|
||||
.transpose()
|
||||
.with_context(|| {
|
||||
let Some(path) = cli.output_file.as_ref() else {
|
||||
return "open output file".to_string();
|
||||
};
|
||||
format!("open output file {}", path.display())
|
||||
})?;
|
||||
let output = Output::new(jsonl_file);
|
||||
let approval_policy = parse_approval_policy(&cli.approval_policy)?;
|
||||
|
||||
let mut client = AppServerClient::spawn(
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#![allow(clippy::expect_used)]
|
||||
use std::fs::File;
|
||||
use std::io;
|
||||
use std::io::IsTerminal;
|
||||
use std::io::Write;
|
||||
@@ -24,19 +25,41 @@ pub struct Output {
|
||||
lock: Arc<Mutex<()>>,
|
||||
prompt: Arc<Mutex<PromptState>>,
|
||||
color: bool,
|
||||
jsonl_file: Option<Arc<Mutex<File>>>,
|
||||
}
|
||||
|
||||
impl Output {
|
||||
pub fn new() -> Self {
|
||||
pub fn new(jsonl_file: Option<File>) -> Self {
|
||||
let no_color = std::env::var_os("NO_COLOR").is_some();
|
||||
let color = !no_color && io::stdout().is_terminal() && io::stderr().is_terminal();
|
||||
Self {
|
||||
lock: Arc::new(Mutex::new(())),
|
||||
prompt: Arc::new(Mutex::new(PromptState::default())),
|
||||
color,
|
||||
jsonl_file: jsonl_file.map(|file| Arc::new(Mutex::new(file))),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn server_json_line(&self, line: &str, filtered_output: bool) -> io::Result<()> {
|
||||
let _guard = self.lock.lock().expect("output lock poisoned");
|
||||
|
||||
if let Some(file) = self.jsonl_file.as_ref() {
|
||||
let mut file = file.lock().expect("jsonl file lock poisoned");
|
||||
writeln!(file, "{line}")?;
|
||||
file.flush()?;
|
||||
}
|
||||
|
||||
if self.jsonl_file.is_none() && !filtered_output {
|
||||
self.clear_prompt_line_locked()?;
|
||||
let mut stdout = io::stdout();
|
||||
writeln!(stdout, "{line}")?;
|
||||
stdout.flush()?;
|
||||
self.redraw_prompt_locked()?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn server_line(&self, line: &str) -> io::Result<()> {
|
||||
let _guard = self.lock.lock().expect("output lock poisoned");
|
||||
self.clear_prompt_line_locked()?;
|
||||
@@ -120,3 +143,33 @@ impl Output {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::fs;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn server_json_line_writes_to_configured_file() {
|
||||
let path = std::env::temp_dir().join(format!(
|
||||
"codex-debug-client-output-{}.jsonl",
|
||||
std::process::id()
|
||||
));
|
||||
let file = File::create(&path).expect("create output file");
|
||||
let output = Output::new(Some(file));
|
||||
|
||||
output
|
||||
.server_json_line(r#"{"id":1}"#, false)
|
||||
.expect("write unfiltered line");
|
||||
output
|
||||
.server_json_line(r#"{"id":2}"#, true)
|
||||
.expect("write filtered line");
|
||||
|
||||
assert_eq!(
|
||||
fs::read_to_string(&path).expect("read output file"),
|
||||
"{\"id\":1}\n{\"id\":2}\n"
|
||||
);
|
||||
let _ = fs::remove_file(path);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,8 +67,8 @@ pub fn start_reader(
|
||||
}
|
||||
|
||||
let line = buffer.trim_end_matches(['\n', '\r']);
|
||||
if !line.is_empty() && !filtered_output {
|
||||
let _ = output.server_line(line);
|
||||
if !line.is_empty() {
|
||||
let _ = output.server_json_line(line, filtered_output);
|
||||
}
|
||||
|
||||
let Ok(message) = serde_json::from_str::<JSONRPCMessage>(line) else {
|
||||
|
||||
Reference in New Issue
Block a user