Skip to content

Commit fc6cfd5

Browse files
authored
protocol-ts (#2425)
1 parent c283f9f commit fc6cfd5

File tree

12 files changed

+229
-33
lines changed

12 files changed

+229
-33
lines changed

codex-rs/Cargo.lock

Lines changed: 13 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

codex-rs/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ members = [
1616
"mcp-types",
1717
"ollama",
1818
"protocol",
19+
"protocol-ts",
1920
"tui",
2021
]
2122
resolver = "2"

codex-rs/cli/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,4 @@ tokio = { version = "1", features = [
3737
] }
3838
tracing = "0.1.41"
3939
tracing-subscriber = "0.3.19"
40+
codex-protocol-ts = { path = "../protocol-ts" }

codex-rs/cli/src/main.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,10 @@ enum Subcommand {
7272
/// Apply the latest diff produced by Codex agent as a `git apply` to your local working tree.
7373
#[clap(visible_alias = "a")]
7474
Apply(ApplyCommand),
75+
76+
/// Internal: generate TypeScript protocol bindings.
77+
#[clap(hide = true)]
78+
GenerateTs(GenerateTsCommand),
7579
}
7680

7781
#[derive(Debug, Parser)]
@@ -120,6 +124,17 @@ struct LogoutCommand {
120124
config_overrides: CliConfigOverrides,
121125
}
122126

127+
#[derive(Debug, Parser)]
128+
struct GenerateTsCommand {
129+
/// Output directory where .ts files will be written
130+
#[arg(short = 'o', long = "out", value_name = "DIR")]
131+
out_dir: PathBuf,
132+
133+
/// Optional path to the Prettier executable to format generated files
134+
#[arg(short = 'p', long = "prettier", value_name = "PRETTIER_BIN")]
135+
prettier: Option<PathBuf>,
136+
}
137+
123138
fn main() -> anyhow::Result<()> {
124139
arg0_dispatch_or_else(|codex_linux_sandbox_exe| async move {
125140
cli_main(codex_linux_sandbox_exe).await?;
@@ -194,6 +209,9 @@ async fn cli_main(codex_linux_sandbox_exe: Option<PathBuf>) -> anyhow::Result<()
194209
prepend_config_flags(&mut apply_cli.config_overrides, cli.config_overrides);
195210
run_apply_command(apply_cli, None).await?;
196211
}
212+
Some(Subcommand::GenerateTs(gen_cli)) => {
213+
codex_protocol_ts::generate_ts(&gen_cli.out_dir, gen_cli.prettier.as_deref())?;
214+
}
197215
}
198216

199217
Ok(())

codex-rs/protocol-ts/Cargo.toml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
[package]
2+
edition = "2024"
3+
name = "codex-protocol-ts"
4+
version = { workspace = true }
5+
6+
[lints]
7+
workspace = true
8+
9+
[lib]
10+
name = "codex_protocol_ts"
11+
path = "src/lib.rs"
12+
13+
[[bin]]
14+
name = "codex-protocol-ts"
15+
path = "src/main.rs"
16+
17+
[dependencies]
18+
anyhow = "1"
19+
codex-protocol = { path = "../protocol" }
20+
ts-rs = "11"
21+
clap = { version = "4", features = ["derive"] }

codex-rs/protocol-ts/generate-ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
#!/bin/bash
2+
3+
set -euo pipefail
4+
5+
cd "$(dirname "$0")"/..
6+
7+
tmpdir=$(mktemp -d)
8+
just codex generate-ts --prettier ../node_modules/.bin/prettier --out "$tmpdir"
9+
10+
echo "wrote output to $tmpdir"

codex-rs/protocol-ts/src/lib.rs

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
use anyhow::Context;
2+
use anyhow::Result;
3+
use anyhow::anyhow;
4+
use std::ffi::OsStr;
5+
use std::fs;
6+
use std::io::Read;
7+
use std::io::Write;
8+
use std::path::Path;
9+
use std::path::PathBuf;
10+
use std::process::Command;
11+
use ts_rs::TS;
12+
13+
const HEADER: &str = "// GENERATED CODE! DO NOT MODIFY BY HAND!\n\n";
14+
15+
pub fn generate_ts(out_dir: &Path, prettier: Option<&Path>) -> Result<()> {
16+
ensure_dir(out_dir)?;
17+
18+
// Generate TS bindings
19+
codex_protocol::mcp_protocol::ConversationId::export_all_to(out_dir)?;
20+
codex_protocol::mcp_protocol::InputItem::export_all_to(out_dir)?;
21+
codex_protocol::mcp_protocol::ClientRequest::export_all_to(out_dir)?;
22+
codex_protocol::mcp_protocol::ServerRequest::export_all_to(out_dir)?;
23+
codex_protocol::mcp_protocol::NewConversationParams::export_all_to(out_dir)?;
24+
codex_protocol::mcp_protocol::NewConversationResponse::export_all_to(out_dir)?;
25+
codex_protocol::mcp_protocol::AddConversationListenerParams::export_all_to(out_dir)?;
26+
codex_protocol::mcp_protocol::AddConversationSubscriptionResponse::export_all_to(out_dir)?;
27+
codex_protocol::mcp_protocol::RemoveConversationListenerParams::export_all_to(out_dir)?;
28+
codex_protocol::mcp_protocol::RemoveConversationSubscriptionResponse::export_all_to(out_dir)?;
29+
codex_protocol::mcp_protocol::SendUserMessageParams::export_all_to(out_dir)?;
30+
codex_protocol::mcp_protocol::SendUserMessageResponse::export_all_to(out_dir)?;
31+
codex_protocol::mcp_protocol::SendUserTurnParams::export_all_to(out_dir)?;
32+
codex_protocol::mcp_protocol::SendUserTurnResponse::export_all_to(out_dir)?;
33+
codex_protocol::mcp_protocol::InterruptConversationParams::export_all_to(out_dir)?;
34+
codex_protocol::mcp_protocol::InterruptConversationResponse::export_all_to(out_dir)?;
35+
codex_protocol::mcp_protocol::LoginChatGptResponse::export_all_to(out_dir)?;
36+
codex_protocol::mcp_protocol::LoginChatGptCompleteNotification::export_all_to(out_dir)?;
37+
codex_protocol::mcp_protocol::CancelLoginChatGptParams::export_all_to(out_dir)?;
38+
codex_protocol::mcp_protocol::CancelLoginChatGptResponse::export_all_to(out_dir)?;
39+
codex_protocol::mcp_protocol::ApplyPatchApprovalParams::export_all_to(out_dir)?;
40+
codex_protocol::mcp_protocol::ApplyPatchApprovalResponse::export_all_to(out_dir)?;
41+
codex_protocol::mcp_protocol::ExecCommandApprovalParams::export_all_to(out_dir)?;
42+
codex_protocol::mcp_protocol::ExecCommandApprovalResponse::export_all_to(out_dir)?;
43+
44+
// Prepend header to each generated .ts file
45+
let ts_files = ts_files_in(out_dir)?;
46+
for file in &ts_files {
47+
prepend_header_if_missing(file)?;
48+
}
49+
50+
// Format with Prettier by passing individual files (no shell globbing)
51+
if let Some(prettier_bin) = prettier {
52+
if !ts_files.is_empty() {
53+
let status = Command::new(prettier_bin)
54+
.arg("--write")
55+
.args(ts_files.iter().map(|p| p.as_os_str()))
56+
.status()
57+
.with_context(|| {
58+
format!("Failed to invoke Prettier at {}", prettier_bin.display())
59+
})?;
60+
if !status.success() {
61+
return Err(anyhow!("Prettier failed with status {}", status));
62+
}
63+
}
64+
}
65+
66+
Ok(())
67+
}
68+
69+
fn ensure_dir(dir: &Path) -> Result<()> {
70+
fs::create_dir_all(dir)
71+
.with_context(|| format!("Failed to create output directory {}", dir.display()))
72+
}
73+
74+
fn prepend_header_if_missing(path: &Path) -> Result<()> {
75+
let mut content = String::new();
76+
{
77+
let mut f = fs::File::open(path)
78+
.with_context(|| format!("Failed to open {} for reading", path.display()))?;
79+
f.read_to_string(&mut content)
80+
.with_context(|| format!("Failed to read {}", path.display()))?;
81+
}
82+
83+
if content.starts_with(HEADER) {
84+
return Ok(());
85+
}
86+
87+
let mut f = fs::File::create(path)
88+
.with_context(|| format!("Failed to open {} for writing", path.display()))?;
89+
f.write_all(HEADER.as_bytes())
90+
.with_context(|| format!("Failed to write header to {}", path.display()))?;
91+
f.write_all(content.as_bytes())
92+
.with_context(|| format!("Failed to write content to {}", path.display()))?;
93+
Ok(())
94+
}
95+
96+
fn ts_files_in(dir: &Path) -> Result<Vec<PathBuf>> {
97+
let mut files = Vec::new();
98+
for entry in
99+
fs::read_dir(dir).with_context(|| format!("Failed to read dir {}", dir.display()))?
100+
{
101+
let entry = entry?;
102+
let path = entry.path();
103+
if path.is_file() && path.extension() == Some(OsStr::new("ts")) {
104+
files.push(path);
105+
}
106+
}
107+
Ok(files)
108+
}

codex-rs/protocol-ts/src/main.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
use anyhow::Result;
2+
use clap::Parser;
3+
use std::path::PathBuf;
4+
5+
#[derive(Parser, Debug)]
6+
#[command(about = "Generate TypeScript bindings for the Codex protocol")]
7+
struct Args {
8+
/// Output directory where .ts files will be written
9+
#[arg(short = 'o', long = "out", value_name = "DIR")]
10+
out_dir: PathBuf,
11+
12+
/// Optional path to the Prettier executable to format generated files
13+
#[arg(short = 'p', long = "prettier", value_name = "PRETTIER_BIN")]
14+
prettier: Option<PathBuf>,
15+
}
16+
17+
fn main() -> Result<()> {
18+
let args = Args::parse();
19+
codex_protocol_ts::generate_ts(&args.out_dir, args.prettier.as_deref())
20+
}

codex-rs/protocol/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ serde_bytes = "0.11"
1717
serde_json = "1"
1818
strum = "0.27.2"
1919
strum_macros = "0.27.2"
20+
ts-rs = { version = "11", features = ["uuid-impl", "serde-json-impl"] }
2021
uuid = { version = "1", features = ["serde", "v4"] }
2122

2223
[dev-dependencies]

codex-rs/protocol/src/config_types.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
use serde::Deserialize;
22
use serde::Serialize;
33
use strum_macros::Display;
4+
use ts_rs::TS;
45

56
/// See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning
6-
#[derive(Debug, Serialize, Deserialize, Default, Clone, Copy, PartialEq, Eq, Display)]
7+
#[derive(Debug, Serialize, Deserialize, Default, Clone, Copy, PartialEq, Eq, Display, TS)]
78
#[serde(rename_all = "lowercase")]
89
#[strum(serialize_all = "lowercase")]
910
pub enum ReasoningEffort {
@@ -19,7 +20,7 @@ pub enum ReasoningEffort {
1920
/// A summary of the reasoning performed by the model. This can be useful for
2021
/// debugging and understanding the model's reasoning process.
2122
/// See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#reasoning-summaries
22-
#[derive(Debug, Serialize, Deserialize, Default, Clone, Copy, PartialEq, Eq, Display)]
23+
#[derive(Debug, Serialize, Deserialize, Default, Clone, Copy, PartialEq, Eq, Display, TS)]
2324
#[serde(rename_all = "lowercase")]
2425
#[strum(serialize_all = "lowercase")]
2526
pub enum ReasoningSummary {
@@ -31,7 +32,7 @@ pub enum ReasoningSummary {
3132
None,
3233
}
3334

34-
#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Default, Serialize, Display)]
35+
#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Default, Serialize, Display, TS)]
3536
#[serde(rename_all = "kebab-case")]
3637
#[strum(serialize_all = "kebab-case")]
3738
pub enum SandboxMode {

0 commit comments

Comments
 (0)